From 5beeb08393bee4ee70b15c64d3fb03159321dfaa Mon Sep 17 00:00:00 2001 From: Brandon Date: Wed, 3 Jun 2026 09:52:34 -0400 Subject: [PATCH 1/5] entra integration tests --- ...a-id-directory.service.integration.spec.ts | 103 ++++++++++++++++++ ...okta-directory.service.integration.spec.ts | 99 +++++++++++++++++ ...ogin-directory.service.integration.spec.ts | 102 +++++++++++++++++ utils/entra/config-fixtures.ts | 53 +++++++++ utils/entra/group-fixtures.ts | 46 ++++++++ utils/entra/user-fixtures.ts | 78 +++++++++++++ 6 files changed, 481 insertions(+) create mode 100644 libs/services/directory-services/entra-id-directory.service.integration.spec.ts create mode 100644 libs/services/directory-services/okta-directory.service.integration.spec.ts create mode 100644 libs/services/directory-services/onelogin-directory.service.integration.spec.ts create mode 100644 utils/entra/config-fixtures.ts create mode 100644 utils/entra/group-fixtures.ts create mode 100644 utils/entra/user-fixtures.ts diff --git a/libs/services/directory-services/entra-id-directory.service.integration.spec.ts b/libs/services/directory-services/entra-id-directory.service.integration.spec.ts new file mode 100644 index 000000000..6f0627820 --- /dev/null +++ b/libs/services/directory-services/entra-id-directory.service.integration.spec.ts @@ -0,0 +1,103 @@ +/** + * @jest-environment node + */ +import { config as dotenvConfig } from "dotenv"; +import { mock, MockProxy } from "jest-mock-extended"; + +import { I18nService } from "@/libs/abstractions/i18n.service"; +import { LogService } from "@/libs/abstractions/log.service"; +import { StateService } from "@/libs/abstractions/state.service"; + +import { + getEntraIdConfiguration, + getSyncConfiguration, +} from "../../../utils/entra/config-fixtures"; +import { allGroupFixtures, filteredGroupFixtures } from "../../../utils/entra/group-fixtures"; +import { allUserFixtures, groupAUserFixtures } from "../../../utils/entra/user-fixtures"; +import { DirectoryType } from "../../enums/directoryType"; + +import { EntraIdDirectoryService } from "./entra-id-directory.service"; + +// These tests integrate with a test Microsoft Entra ID tenant. +// Credentials are located in the shared Bitwarden collection for Directory Connector testing. +// Place the .env file attachment in the utils folder. + +// Load .env variables +dotenvConfig({ path: "utils/.env" }); + +// This filter targets integration test data. +// It should return data that matches the fixtures exactly. +const INTEGRATION_GROUP_FILTER = "include: Integration Testing Group A"; + +// These tests are slow! +// Increase the default timeout from 5s to 30s +jest.setTimeout(30000); + +describe("entraIdDirectoryService", () => { + let logService: MockProxy; + let i18nService: MockProxy; + let stateService: MockProxy; + + let directoryService: EntraIdDirectoryService; + + beforeEach(() => { + logService = mock(); + i18nService = mock(); + stateService = mock(); + + stateService.getDirectoryType.mockResolvedValue(DirectoryType.EntraID); + stateService.getDirectory + .calledWith(DirectoryType.EntraID) + .mockResolvedValue(getEntraIdConfiguration()); + stateService.getUserDelta.mockResolvedValue(null); // do not use delta link, fetch from scratch + i18nService.t.mockImplementation((id) => id); // passthrough for error messages + + directoryService = new EntraIdDirectoryService(logService, i18nService, stateService); + }); + + it("syncs without filters (includes integration test data)", async () => { + stateService.getSync.mockResolvedValue(getSyncConfiguration({ users: true, groups: true })); + + const [groups, users] = await directoryService.getEntries(true, true); + + expect(groups).toEqual(expect.arrayContaining(allGroupFixtures)); + expect(users).toEqual(expect.arrayContaining(allUserFixtures)); + }); + + it("syncs users only (no groups)", async () => { + stateService.getSync.mockResolvedValue(getSyncConfiguration({ users: true, groups: false })); + + const [groups, users] = await directoryService.getEntries(true, true); + + expect(groups).toBeUndefined(); + expect(users).toEqual(expect.arrayContaining(allUserFixtures)); + }); + + it("syncs groups only (no users)", async () => { + stateService.getSync.mockResolvedValue(getSyncConfiguration({ users: false, groups: true })); + + const [groups, users] = await directoryService.getEntries(true, true); + + expect(groups).toEqual(expect.arrayContaining(allGroupFixtures)); + expect(users).toBeUndefined(); + }); + + it("syncs using group filter (exact match for integration test data)", async () => { + stateService.getSync.mockResolvedValue( + getSyncConfiguration({ users: true, groups: true, groupFilter: INTEGRATION_GROUP_FILTER }), + ); + + const result = await directoryService.getEntries(true, true); + + expect(result).toEqual([filteredGroupFixtures, groupAUserFixtures]); + }); + + it("throws when credentials are invalid", async () => { + stateService.getDirectory + .calledWith(DirectoryType.EntraID) + .mockResolvedValue(getEntraIdConfiguration({ key: "bad-secret" })); + stateService.getSync.mockResolvedValue(getSyncConfiguration({ users: true, groups: true })); + + await expect(directoryService.getEntries(true, true)).rejects.toThrow(); + }); +}); diff --git a/libs/services/directory-services/okta-directory.service.integration.spec.ts b/libs/services/directory-services/okta-directory.service.integration.spec.ts new file mode 100644 index 000000000..8c1b0b34e --- /dev/null +++ b/libs/services/directory-services/okta-directory.service.integration.spec.ts @@ -0,0 +1,99 @@ +///** +// * @jest-environment node +// */ +//import { config as dotenvConfig } from "dotenv"; +//import { mock, MockProxy } from "jest-mock-extended"; +// +//import { I18nService } from "@/libs/abstractions/i18n.service"; +//import { LogService } from "@/libs/abstractions/log.service"; +//import { StateService } from "@/libs/abstractions/state.service"; +// +//import { getOktaConfiguration, getSyncConfiguration } from "../../../utils/okta/config-fixtures"; +//import { DirectoryType } from "../../enums/directoryType"; +// +//import { OktaDirectoryService } from "./okta-directory.service"; +// +//// These tests integrate with a test Okta organization. +//// Credentials are located in the shared Bitwarden collection for Directory Connector testing. +//// Place the .env file attachment in the utils folder. +// +//// Load .env variables +//dotenvConfig({ path: "utils/.env" }); +// +//// This filter targets integration test data. +//const INTEGRATION_GROUP_FILTER = "include: Integration Test Group"; +// +//// These tests are slow! +//// Increase the default timeout from 5s to 30s +//jest.setTimeout(30000); +// +//describe("oktaDirectoryService", () => { +// let logService: MockProxy; +// let i18nService: MockProxy; +// let stateService: MockProxy; +// +// let directoryService: OktaDirectoryService; +// +// beforeEach(() => { +// logService = mock(); +// i18nService = mock(); +// stateService = mock(); +// +// stateService.getDirectoryType.mockResolvedValue(DirectoryType.Okta); +// stateService.getDirectory +// .calledWith(DirectoryType.Okta) +// .mockResolvedValue(getOktaConfiguration()); +// stateService.getLastUserSync.mockResolvedValue(null); // do not filter results by last modified date +// stateService.getLastGroupSync.mockResolvedValue(null); // do not filter results by last modified date +// i18nService.t.mockImplementation((id) => id); // passthrough for error messages +// +// directoryService = new OktaDirectoryService(logService, i18nService, stateService); +// }); +// +// it("syncs without filters (includes integration test data)", async () => { +// stateService.getSync.mockResolvedValue(getSyncConfiguration({ users: true, groups: true })); +// +// const [groups, users] = await directoryService.getEntries(true, true); +// +// expect(groups).toBeDefined(); +// expect(users).toBeDefined(); +// }); +// +// it("syncs users only (no groups)", async () => { +// stateService.getSync.mockResolvedValue(getSyncConfiguration({ users: true, groups: false })); +// +// const [groups, users] = await directoryService.getEntries(true, true); +// +// expect(groups).toBeUndefined(); +// expect(users).toBeDefined(); +// }); +// +// it("syncs groups only (no users)", async () => { +// stateService.getSync.mockResolvedValue(getSyncConfiguration({ users: false, groups: true })); +// +// const [groups, users] = await directoryService.getEntries(true, true); +// +// expect(groups).toBeDefined(); +// expect(users).toBeUndefined(); +// }); +// +// it("syncs using group filter", async () => { +// stateService.getSync.mockResolvedValue( +// getSyncConfiguration({ users: true, groups: true, groupFilter: INTEGRATION_GROUP_FILTER }), +// ); +// +// const [groups, users] = await directoryService.getEntries(true, true); +// +// expect(groups).toBeDefined(); +// expect(users).toBeDefined(); +// }); +// +// it("throws when credentials are invalid", async () => { +// stateService.getDirectory +// .calledWith(DirectoryType.Okta) +// .mockResolvedValue(getOktaConfiguration({ token: "bad-token-00000" })); +// stateService.getSync.mockResolvedValue(getSyncConfiguration({ users: true, groups: true })); +// +// await expect(directoryService.getEntries(true, true)).rejects.toThrow(); +// }); +//}); diff --git a/libs/services/directory-services/onelogin-directory.service.integration.spec.ts b/libs/services/directory-services/onelogin-directory.service.integration.spec.ts new file mode 100644 index 000000000..c1ac41fb5 --- /dev/null +++ b/libs/services/directory-services/onelogin-directory.service.integration.spec.ts @@ -0,0 +1,102 @@ +///** +// * @jest-environment node +// */ +//import { config as dotenvConfig } from "dotenv"; +//import { mock, MockProxy } from "jest-mock-extended"; +// +//import { I18nService } from "@/libs/abstractions/i18n.service"; +//import { LogService } from "@/libs/abstractions/log.service"; +//import { StateService } from "@/libs/abstractions/state.service"; +// +//import { +// getOneLoginConfiguration, +// getSyncConfiguration, +//} from "../../../utils/onelogin/config-fixtures"; +//import { DirectoryType } from "../../enums/directoryType"; +// +//import { OneLoginDirectoryService } from "./onelogin-directory.service"; +// +//// These tests integrate with a test OneLogin organization. +//// Credentials are located in the shared Bitwarden collection for Directory Connector testing. +//// Place the .env file attachment in the utils folder. +// +//// Load .env variables +//dotenvConfig({ path: "utils/.env" }); +// +//// This filter targets integration test data. +//const INTEGRATION_GROUP_FILTER = "include: Integration Test Role"; +// +//// These tests are slow! +//// Increase the default timeout from 5s to 30s +//jest.setTimeout(30000); +// +//describe("oneLoginDirectoryService", () => { +// let logService: MockProxy; +// let i18nService: MockProxy; +// let stateService: MockProxy; +// +// let directoryService: OneLoginDirectoryService; +// +// beforeEach(() => { +// logService = mock(); +// i18nService = mock(); +// stateService = mock(); +// +// stateService.getDirectoryType.mockResolvedValue(DirectoryType.OneLogin); +// stateService.getDirectory +// .calledWith(DirectoryType.OneLogin) +// .mockResolvedValue(getOneLoginConfiguration()); +// i18nService.t.mockImplementation((id) => id); // passthrough for error messages +// +// directoryService = new OneLoginDirectoryService(logService, i18nService, stateService); +// }); +// +// it("syncs without filters (includes integration test data)", async () => { +// stateService.getSync.mockResolvedValue(getSyncConfiguration({ users: true, groups: true })); +// +// const [groups, users] = await directoryService.getEntries(true, true); +// +// expect(groups).toBeDefined(); +// expect(users).toBeDefined(); +// }); +// +// it("syncs users only (no groups)", async () => { +// stateService.getSync.mockResolvedValue(getSyncConfiguration({ users: true, groups: false })); +// +// const [groups, users] = await directoryService.getEntries(true, true); +// +// expect(groups).toBeUndefined(); +// expect(users).toBeDefined(); +// }); +// +// it("syncs groups only (no users)", async () => { +// stateService.getSync.mockResolvedValue(getSyncConfiguration({ users: false, groups: true })); +// +// const [groups, users] = await directoryService.getEntries(true, true); +// +// expect(groups).toBeDefined(); +// expect(users).toBeUndefined(); +// }); +// +// it("syncs using group filter", async () => { +// stateService.getSync.mockResolvedValue( +// getSyncConfiguration({ users: true, groups: true, groupFilter: INTEGRATION_GROUP_FILTER }), +// ); +// +// const [groups, users] = await directoryService.getEntries(true, true); +// +// expect(groups).toBeDefined(); +// expect(users).toBeDefined(); +// }); +// +// it("throws when credentials are invalid", async () => { +// stateService.getDirectory +// .calledWith(DirectoryType.OneLogin) +// .mockResolvedValue( +// getOneLoginConfiguration({ clientId: "bad-client-id", clientSecret: "bad-client-secret" }), +// ); +// stateService.getSync.mockResolvedValue(getSyncConfiguration({ users: true, groups: true })); +// +// await expect(directoryService.getEntries(true, true)).rejects.toThrow(); +// }); +//}); diff --git a/utils/entra/config-fixtures.ts b/utils/entra/config-fixtures.ts new file mode 100644 index 000000000..e12cd3d20 --- /dev/null +++ b/utils/entra/config-fixtures.ts @@ -0,0 +1,53 @@ +import { EntraIdConfiguration } from "@/libs/models/entraIdConfiguration"; +import { SyncConfiguration } from "@/libs/models/syncConfiguration"; + +/** + * @returns a basic Entra ID configuration. Can be overridden by passing in a partial configuration. + */ +export const getEntraIdConfiguration = ( + config?: Partial, +): EntraIdConfiguration => { + const tenant = process.env.ENTRA_TENANT_ID; + const applicationId = process.env.ENTRA_APPLICATION_ID; + const key = process.env.ENTRA_KEY; + + if (!tenant || !applicationId || !key) { + throw new Error("Entra ID integration test credentials not configured."); + } + + return { + identityAuthority: "login.microsoftonline.com", + tenant, + applicationId, + key, + ...(config ?? {}), + }; +}; + +/** + * @returns a basic sync configuration. Can be overridden by passing in a partial configuration. + */ +export const getSyncConfiguration = (config?: Partial): SyncConfiguration => ({ + users: false, + groups: false, + interval: 5, + userFilter: "", + groupFilter: "", + removeDisabled: false, + overwriteExisting: false, + largeImport: false, + // Ldap properties - not optional for some reason + groupObjectClass: "", + userObjectClass: "", + groupPath: null, + userPath: null, + groupNameAttribute: "", + userEmailAttribute: "", + memberAttribute: "", + useEmailPrefixSuffix: false, + emailPrefixAttribute: "", + emailSuffix: null, + creationDateAttribute: "", + revisionDateAttribute: "", + ...(config ?? {}), +}); diff --git a/utils/entra/group-fixtures.ts b/utils/entra/group-fixtures.ts new file mode 100644 index 000000000..444c06910 --- /dev/null +++ b/utils/entra/group-fixtures.ts @@ -0,0 +1,46 @@ +import { Jsonify } from "type-fest"; + +import { GroupEntry } from "@/libs/models/groupEntry"; + +// These must match the Entra ID test tenant seed data + +const allGroupsData: Jsonify[] = [ + { + externalId: "573783bf-d086-4e1e-9084-4b7b0d2a5969", + groupMemberReferenceIds: [], + name: "Integration Testing Group A", + referenceId: "573783bf-d086-4e1e-9084-4b7b0d2a5969", + userMemberExternalIds: [ + "34e12500-13ff-4700-afa0-285757ab7695", + "051c89c3-1788-44ce-9ee5-35dc1e6cb794", + "50fff556-010d-46e2-826a-c86ddf5816dc", + ], + users: [], + }, + { + externalId: "66578c29-73af-448b-b352-301f9588772b", + groupMemberReferenceIds: [], + name: "Integration Testing Group B", + referenceId: "66578c29-73af-448b-b352-301f9588772b", + userMemberExternalIds: [], + users: [], + }, +]; + +const filteredGroupsData: Jsonify[] = [ + { + externalId: "573783bf-d086-4e1e-9084-4b7b0d2a5969", + groupMemberReferenceIds: [], + name: "Integration Testing Group A", + referenceId: "573783bf-d086-4e1e-9084-4b7b0d2a5969", + userMemberExternalIds: [ + "34e12500-13ff-4700-afa0-285757ab7695", + "051c89c3-1788-44ce-9ee5-35dc1e6cb794", + "50fff556-010d-46e2-826a-c86ddf5816dc", + ], + users: [], + }, +]; + +export const allGroupFixtures = allGroupsData.map((g) => GroupEntry.fromJSON(g)); +export const filteredGroupFixtures = filteredGroupsData.map((g) => GroupEntry.fromJSON(g)); diff --git a/utils/entra/user-fixtures.ts b/utils/entra/user-fixtures.ts new file mode 100644 index 000000000..f3abab4e7 --- /dev/null +++ b/utils/entra/user-fixtures.ts @@ -0,0 +1,78 @@ +import { Jsonify } from "type-fest"; + +import { UserEntry } from "@/libs/models/userEntry"; + +// These must match the Entra ID test tenant seed data + +const allUsersData: Jsonify[] = [ + { + deleted: false, + disabled: false, + email: "amarshall@bwrox.dev", + externalId: "051c89c3-1788-44ce-9ee5-35dc1e6cb794", + referenceId: "051c89c3-1788-44ce-9ee5-35dc1e6cb794", + }, + { + deleted: false, + disabled: true, + email: "anders@bwrox.dev", + externalId: "50fff556-010d-46e2-826a-c86ddf5816dc", + referenceId: "50fff556-010d-46e2-826a-c86ddf5816dc", + }, + { + deleted: false, + disabled: true, + email: "disableduser@bwrox.dev", + externalId: "9e59553d-217e-4010-9b2d-f4458cc861bd", + referenceId: "9e59553d-217e-4010-9b2d-f4458cc861bd", + }, + { + deleted: false, + disabled: true, + email: "jcooks@bwrox.dev", + externalId: "34e12500-13ff-4700-afa0-285757ab7695", + referenceId: "34e12500-13ff-4700-afa0-285757ab7695", + }, + { + deleted: false, + disabled: false, + email: "kreynolds@bwrox.dev", + externalId: "c5d0fdcd-ea09-4ddb-b05d-42c94af705f4", + referenceId: "c5d0fdcd-ea09-4ddb-b05d-42c94af705f4", + }, + { + deleted: false, + disabled: false, + email: "trittson@bwrox.dev", + externalId: "13089061-5d1c-4bb2-bb2a-0d058e4898bc", + referenceId: "13089061-5d1c-4bb2-bb2a-0d058e4898bc", + }, +]; + +// Members of Integration Testing Group A: Jordan Cooks, Aaron Marshall, Anders Ã…berg +const groupAUsersData: Jsonify[] = [ + { + deleted: false, + disabled: false, + email: "amarshall@bwrox.dev", + externalId: "051c89c3-1788-44ce-9ee5-35dc1e6cb794", + referenceId: "051c89c3-1788-44ce-9ee5-35dc1e6cb794", + }, + { + deleted: false, + disabled: true, + email: "anders@bwrox.dev", + externalId: "50fff556-010d-46e2-826a-c86ddf5816dc", + referenceId: "50fff556-010d-46e2-826a-c86ddf5816dc", + }, + { + deleted: false, + disabled: true, + email: "jcooks@bwrox.dev", + externalId: "34e12500-13ff-4700-afa0-285757ab7695", + referenceId: "34e12500-13ff-4700-afa0-285757ab7695", + }, +]; + +export const allUserFixtures = allUsersData.map((u) => UserEntry.fromJSON(u)); +export const groupAUserFixtures = groupAUsersData.map((u) => UserEntry.fromJSON(u)); From 8dcbf20ceb66082b8c85be57938f36e2d1d6f09b Mon Sep 17 00:00:00 2001 From: Brandon Date: Fri, 5 Jun 2026 17:20:28 -0400 Subject: [PATCH 2/5] okta integration tests + fixtures --- ...okta-directory.service.integration.spec.ts | 201 +++++++++--------- utils/okta/config-fixtures.ts | 48 +++++ utils/okta/group-fixtures.ts | 69 ++++++ utils/okta/user-fixtures.ts | 64 ++++++ 4 files changed, 283 insertions(+), 99 deletions(-) create mode 100644 utils/okta/config-fixtures.ts create mode 100644 utils/okta/group-fixtures.ts create mode 100644 utils/okta/user-fixtures.ts diff --git a/libs/services/directory-services/okta-directory.service.integration.spec.ts b/libs/services/directory-services/okta-directory.service.integration.spec.ts index 8c1b0b34e..43ad33d76 100644 --- a/libs/services/directory-services/okta-directory.service.integration.spec.ts +++ b/libs/services/directory-services/okta-directory.service.integration.spec.ts @@ -1,99 +1,102 @@ -///** -// * @jest-environment node -// */ -//import { config as dotenvConfig } from "dotenv"; -//import { mock, MockProxy } from "jest-mock-extended"; -// -//import { I18nService } from "@/libs/abstractions/i18n.service"; -//import { LogService } from "@/libs/abstractions/log.service"; -//import { StateService } from "@/libs/abstractions/state.service"; -// -//import { getOktaConfiguration, getSyncConfiguration } from "../../../utils/okta/config-fixtures"; -//import { DirectoryType } from "../../enums/directoryType"; -// -//import { OktaDirectoryService } from "./okta-directory.service"; -// -//// These tests integrate with a test Okta organization. -//// Credentials are located in the shared Bitwarden collection for Directory Connector testing. -//// Place the .env file attachment in the utils folder. -// -//// Load .env variables -//dotenvConfig({ path: "utils/.env" }); -// -//// This filter targets integration test data. -//const INTEGRATION_GROUP_FILTER = "include: Integration Test Group"; -// -//// These tests are slow! -//// Increase the default timeout from 5s to 30s -//jest.setTimeout(30000); -// -//describe("oktaDirectoryService", () => { -// let logService: MockProxy; -// let i18nService: MockProxy; -// let stateService: MockProxy; -// -// let directoryService: OktaDirectoryService; -// -// beforeEach(() => { -// logService = mock(); -// i18nService = mock(); -// stateService = mock(); -// -// stateService.getDirectoryType.mockResolvedValue(DirectoryType.Okta); -// stateService.getDirectory -// .calledWith(DirectoryType.Okta) -// .mockResolvedValue(getOktaConfiguration()); -// stateService.getLastUserSync.mockResolvedValue(null); // do not filter results by last modified date -// stateService.getLastGroupSync.mockResolvedValue(null); // do not filter results by last modified date -// i18nService.t.mockImplementation((id) => id); // passthrough for error messages -// -// directoryService = new OktaDirectoryService(logService, i18nService, stateService); -// }); -// -// it("syncs without filters (includes integration test data)", async () => { -// stateService.getSync.mockResolvedValue(getSyncConfiguration({ users: true, groups: true })); -// -// const [groups, users] = await directoryService.getEntries(true, true); -// -// expect(groups).toBeDefined(); -// expect(users).toBeDefined(); -// }); -// -// it("syncs users only (no groups)", async () => { -// stateService.getSync.mockResolvedValue(getSyncConfiguration({ users: true, groups: false })); -// -// const [groups, users] = await directoryService.getEntries(true, true); -// -// expect(groups).toBeUndefined(); -// expect(users).toBeDefined(); -// }); -// -// it("syncs groups only (no users)", async () => { -// stateService.getSync.mockResolvedValue(getSyncConfiguration({ users: false, groups: true })); -// -// const [groups, users] = await directoryService.getEntries(true, true); -// -// expect(groups).toBeDefined(); -// expect(users).toBeUndefined(); -// }); -// -// it("syncs using group filter", async () => { -// stateService.getSync.mockResolvedValue( -// getSyncConfiguration({ users: true, groups: true, groupFilter: INTEGRATION_GROUP_FILTER }), -// ); -// -// const [groups, users] = await directoryService.getEntries(true, true); -// -// expect(groups).toBeDefined(); -// expect(users).toBeDefined(); -// }); -// -// it("throws when credentials are invalid", async () => { -// stateService.getDirectory -// .calledWith(DirectoryType.Okta) -// .mockResolvedValue(getOktaConfiguration({ token: "bad-token-00000" })); -// stateService.getSync.mockResolvedValue(getSyncConfiguration({ users: true, groups: true })); -// -// await expect(directoryService.getEntries(true, true)).rejects.toThrow(); -// }); -//}); +/** + * @jest-environment node + */ +import { config as dotenvConfig } from "dotenv"; +import { mock, MockProxy } from "jest-mock-extended"; + +import { I18nService } from "@/libs/abstractions/i18n.service"; +import { LogService } from "@/libs/abstractions/log.service"; +import { StateService } from "@/libs/abstractions/state.service"; + +import { getOktaConfiguration, getSyncConfiguration } from "../../../utils/okta/config-fixtures"; +import { allGroupFixtures, integrationGroupFixtures } from "../../../utils/okta/group-fixtures"; +import { allUserFixtures, groupAUserFixtures } from "../../../utils/okta/user-fixtures"; +import { DirectoryType } from "../../enums/directoryType"; + +import { OktaDirectoryService } from "./okta-directory.service"; + +// These tests integrate with a test Okta organization. +// Credentials are located in the shared Bitwarden collection for Directory Connector testing. +// Place the .env file attachment in the utils folder. + +// Load .env variables +dotenvConfig({ path: "utils/.env" }); + +// This filter targets the integration test groups exactly. +const INTEGRATION_GROUP_FILTER = "include: Integration Test Group A, Integration Test Group B"; + +// These tests are slow! +// Increase the default timeout from 5s to 30s +jest.setTimeout(30000); + +describe("oktaDirectoryService", () => { + let logService: MockProxy; + let i18nService: MockProxy; + let stateService: MockProxy; + + let directoryService: OktaDirectoryService; + + beforeEach(() => { + logService = mock(); + i18nService = mock(); + stateService = mock(); + + stateService.getDirectoryType.mockResolvedValue(DirectoryType.Okta); + stateService.getDirectory + .calledWith(DirectoryType.Okta) + .mockResolvedValue(getOktaConfiguration()); + stateService.getLastUserSync.mockResolvedValue(null); // do not filter results by last modified date + stateService.getLastGroupSync.mockResolvedValue(null); // do not filter results by last modified date + i18nService.t.mockImplementation((id) => id); // passthrough for error messages + + directoryService = new OktaDirectoryService(logService, i18nService, stateService); + }); + + it("syncs without filters (includes integration test data)", async () => { + stateService.getSync.mockResolvedValue(getSyncConfiguration({ users: true, groups: true })); + + const [groups, users] = await directoryService.getEntries(true, true); + + expect(groups).toEqual(expect.arrayContaining(allGroupFixtures)); + expect(users).toEqual(expect.arrayContaining(allUserFixtures)); + }); + + it("syncs users only (no groups)", async () => { + stateService.getSync.mockResolvedValue(getSyncConfiguration({ users: true, groups: false })); + + const [groups, users] = await directoryService.getEntries(true, true); + + expect(groups).toBeUndefined(); + expect(users).toEqual(expect.arrayContaining(allUserFixtures)); + }); + + it("syncs groups only (no users)", async () => { + stateService.getSync.mockResolvedValue(getSyncConfiguration({ users: false, groups: true })); + + const [groups, users] = await directoryService.getEntries(true, true); + + expect(groups).toEqual(expect.arrayContaining(allGroupFixtures)); + expect(users).toBeUndefined(); + }); + + it("syncs using group filter (exact match for integration test data)", async () => { + stateService.getSync.mockResolvedValue( + getSyncConfiguration({ users: true, groups: true, groupFilter: INTEGRATION_GROUP_FILTER }), + ); + + const [groups, users] = await directoryService.getEntries(true, true); + + expect(groups).toEqual(expect.arrayContaining(integrationGroupFixtures)); + expect(groups).toHaveLength(integrationGroupFixtures.length); + expect(users).toEqual(expect.arrayContaining(groupAUserFixtures)); + }); + + it("throws when credentials are invalid", async () => { + stateService.getDirectory + .calledWith(DirectoryType.Okta) + .mockResolvedValue(getOktaConfiguration({ token: "bad-token-00000" })); + stateService.getSync.mockResolvedValue(getSyncConfiguration({ users: true, groups: true })); + + await expect(directoryService.getEntries(true, true)).rejects.toThrow(); + }); +}); diff --git a/utils/okta/config-fixtures.ts b/utils/okta/config-fixtures.ts new file mode 100644 index 000000000..e1ed23557 --- /dev/null +++ b/utils/okta/config-fixtures.ts @@ -0,0 +1,48 @@ +import { OktaConfiguration } from "@/libs/models/oktaConfiguration"; +import { SyncConfiguration } from "@/libs/models/syncConfiguration"; + +/** + * @returns a basic Okta configuration. Can be overridden by passing in a partial configuration. + */ +export const getOktaConfiguration = (config?: Partial): OktaConfiguration => { + const orgUrl = process.env.OKTA_ORG_URL; + const token = process.env.OKTA_TOKEN; + + if (!orgUrl || !token) { + throw new Error("Okta integration test credentials not configured."); + } + + return { + orgUrl, + token, + ...(config ?? {}), + }; +}; + +/** + * @returns a basic sync configuration. Can be overridden by passing in a partial configuration. + */ +export const getSyncConfiguration = (config?: Partial): SyncConfiguration => ({ + users: false, + groups: false, + interval: 5, + userFilter: "", + groupFilter: "", + removeDisabled: false, + overwriteExisting: false, + largeImport: false, + // Ldap properties - not optional for some reason + groupObjectClass: "", + userObjectClass: "", + groupPath: null, + userPath: null, + groupNameAttribute: "", + userEmailAttribute: "", + memberAttribute: "", + useEmailPrefixSuffix: false, + emailPrefixAttribute: "", + emailSuffix: null, + creationDateAttribute: "", + revisionDateAttribute: "", + ...(config ?? {}), +}); diff --git a/utils/okta/group-fixtures.ts b/utils/okta/group-fixtures.ts new file mode 100644 index 000000000..a4e4b3737 --- /dev/null +++ b/utils/okta/group-fixtures.ts @@ -0,0 +1,69 @@ +import { Jsonify } from "type-fest"; + +import { GroupEntry } from "@/libs/models/groupEntry"; + +// These must match the Okta test organization seed data + +const allGroupsData: Jsonify[] = [ + { + externalId: "00g13jy2er60PtjR6698", + groupMemberReferenceIds: [], + name: "Everyone", + referenceId: "00g13jy2er60PtjR6698", + userMemberExternalIds: [ + "00u13jy2et80wo2TC698", + "00u13t7eifbXFPQKW698", + "00u13t7bhpibaNW40698", + "00u13t7c00brmAXIO698", + "00u13t7bh9uQY8CwD698", + ], + users: [], + }, + { + externalId: "00g13t7g25qkwrnUA698", + groupMemberReferenceIds: [], + name: "Integration Test Group A", + referenceId: "00g13t7g25qkwrnUA698", + userMemberExternalIds: ["00u13t7eifbXFPQKW698", "00u13t7bh9uQY8CwD698"], + users: [], + }, + { + externalId: "00g13t7h40rER5bQH698", + groupMemberReferenceIds: [], + name: "Integration Test Group B", + referenceId: "00g13t7h40rER5bQH698", + userMemberExternalIds: ["00u13t7eifbXFPQKW698", "00u13t7bhpibaNW40698", "00u13t7c00brmAXIO698"], + users: [], + }, + { + externalId: "00g13jy2er7W7aWYO698", + groupMemberReferenceIds: [], + name: "Okta Administrators", + referenceId: "00g13jy2er7W7aWYO698", + userMemberExternalIds: [], + users: [], + }, +]; + +// Filtered by "include: Integration Test Group A,Integration Test Group B" +const integrationGroupsData: Jsonify[] = [ + { + externalId: "00g13t7g25qkwrnUA698", + groupMemberReferenceIds: [], + name: "Integration Test Group A", + referenceId: "00g13t7g25qkwrnUA698", + userMemberExternalIds: ["00u13t7eifbXFPQKW698", "00u13t7bh9uQY8CwD698"], + users: [], + }, + { + externalId: "00g13t7h40rER5bQH698", + groupMemberReferenceIds: [], + name: "Integration Test Group B", + referenceId: "00g13t7h40rER5bQH698", + userMemberExternalIds: ["00u13t7eifbXFPQKW698", "00u13t7bhpibaNW40698", "00u13t7c00brmAXIO698"], + users: [], + }, +]; + +export const allGroupFixtures = allGroupsData.map((g) => GroupEntry.fromJSON(g)); +export const integrationGroupFixtures = integrationGroupsData.map((g) => GroupEntry.fromJSON(g)); diff --git a/utils/okta/user-fixtures.ts b/utils/okta/user-fixtures.ts new file mode 100644 index 000000000..c3c37760d --- /dev/null +++ b/utils/okta/user-fixtures.ts @@ -0,0 +1,64 @@ +import { Jsonify } from "type-fest"; + +import { UserEntry } from "@/libs/models/userEntry"; + +// These must match the Okta test organization seed data + +const allUsersData: Jsonify[] = [ + { + deleted: false, + disabled: false, + email: "btreston@bitwarden.com", + externalId: "00u13jy2et80wo2TC698", + referenceId: "00u13jy2et80wo2TC698", + }, + { + deleted: false, + disabled: false, + email: "timo@test.com", + externalId: "00u13t7bh9uQY8CwD698", + referenceId: "00u13t7bh9uQY8CwD698", + }, + { + deleted: false, + disabled: false, + email: "max@test.com", + externalId: "00u13t7bhpibaNW40698", + referenceId: "00u13t7bhpibaNW40698", + }, + { + deleted: false, + disabled: false, + email: "oscar@test.com", + externalId: "00u13t7c00brmAXIO698", + referenceId: "00u13t7c00brmAXIO698", + }, + { + deleted: false, + disabled: false, + email: "jimmy@test.com", + externalId: "00u13t7eifbXFPQKW698", + referenceId: "00u13t7eifbXFPQKW698", + }, +]; + +// Members of Integration Test Group A: Jimmy, Timo +const groupAUsersData: Jsonify[] = [ + { + deleted: false, + disabled: false, + email: "timo@test.com", + externalId: "00u13t7bh9uQY8CwD698", + referenceId: "00u13t7bh9uQY8CwD698", + }, + { + deleted: false, + disabled: false, + email: "jimmy@test.com", + externalId: "00u13t7eifbXFPQKW698", + referenceId: "00u13t7eifbXFPQKW698", + }, +]; + +export const allUserFixtures = allUsersData.map((u) => UserEntry.fromJSON(u)); +export const groupAUserFixtures = groupAUsersData.map((u) => UserEntry.fromJSON(u)); From 5b1c8506987d4ab91625fde2be82874f34bde6ab Mon Sep 17 00:00:00 2001 From: Brandon Date: Wed, 10 Jun 2026 13:26:03 -0400 Subject: [PATCH 3/5] remove WIP oneLogin service --- ...ogin-directory.service.integration.spec.ts | 102 ------------------ 1 file changed, 102 deletions(-) delete mode 100644 libs/services/directory-services/onelogin-directory.service.integration.spec.ts diff --git a/libs/services/directory-services/onelogin-directory.service.integration.spec.ts b/libs/services/directory-services/onelogin-directory.service.integration.spec.ts deleted file mode 100644 index c1ac41fb5..000000000 --- a/libs/services/directory-services/onelogin-directory.service.integration.spec.ts +++ /dev/null @@ -1,102 +0,0 @@ -///** -// * @jest-environment node -// */ -//import { config as dotenvConfig } from "dotenv"; -//import { mock, MockProxy } from "jest-mock-extended"; -// -//import { I18nService } from "@/libs/abstractions/i18n.service"; -//import { LogService } from "@/libs/abstractions/log.service"; -//import { StateService } from "@/libs/abstractions/state.service"; -// -//import { -// getOneLoginConfiguration, -// getSyncConfiguration, -//} from "../../../utils/onelogin/config-fixtures"; -//import { DirectoryType } from "../../enums/directoryType"; -// -//import { OneLoginDirectoryService } from "./onelogin-directory.service"; -// -//// These tests integrate with a test OneLogin organization. -//// Credentials are located in the shared Bitwarden collection for Directory Connector testing. -//// Place the .env file attachment in the utils folder. -// -//// Load .env variables -//dotenvConfig({ path: "utils/.env" }); -// -//// This filter targets integration test data. -//const INTEGRATION_GROUP_FILTER = "include: Integration Test Role"; -// -//// These tests are slow! -//// Increase the default timeout from 5s to 30s -//jest.setTimeout(30000); -// -//describe("oneLoginDirectoryService", () => { -// let logService: MockProxy; -// let i18nService: MockProxy; -// let stateService: MockProxy; -// -// let directoryService: OneLoginDirectoryService; -// -// beforeEach(() => { -// logService = mock(); -// i18nService = mock(); -// stateService = mock(); -// -// stateService.getDirectoryType.mockResolvedValue(DirectoryType.OneLogin); -// stateService.getDirectory -// .calledWith(DirectoryType.OneLogin) -// .mockResolvedValue(getOneLoginConfiguration()); -// i18nService.t.mockImplementation((id) => id); // passthrough for error messages -// -// directoryService = new OneLoginDirectoryService(logService, i18nService, stateService); -// }); -// -// it("syncs without filters (includes integration test data)", async () => { -// stateService.getSync.mockResolvedValue(getSyncConfiguration({ users: true, groups: true })); -// -// const [groups, users] = await directoryService.getEntries(true, true); -// -// expect(groups).toBeDefined(); -// expect(users).toBeDefined(); -// }); -// -// it("syncs users only (no groups)", async () => { -// stateService.getSync.mockResolvedValue(getSyncConfiguration({ users: true, groups: false })); -// -// const [groups, users] = await directoryService.getEntries(true, true); -// -// expect(groups).toBeUndefined(); -// expect(users).toBeDefined(); -// }); -// -// it("syncs groups only (no users)", async () => { -// stateService.getSync.mockResolvedValue(getSyncConfiguration({ users: false, groups: true })); -// -// const [groups, users] = await directoryService.getEntries(true, true); -// -// expect(groups).toBeDefined(); -// expect(users).toBeUndefined(); -// }); -// -// it("syncs using group filter", async () => { -// stateService.getSync.mockResolvedValue( -// getSyncConfiguration({ users: true, groups: true, groupFilter: INTEGRATION_GROUP_FILTER }), -// ); -// -// const [groups, users] = await directoryService.getEntries(true, true); -// -// expect(groups).toBeDefined(); -// expect(users).toBeDefined(); -// }); -// -// it("throws when credentials are invalid", async () => { -// stateService.getDirectory -// .calledWith(DirectoryType.OneLogin) -// .mockResolvedValue( -// getOneLoginConfiguration({ clientId: "bad-client-id", clientSecret: "bad-client-secret" }), -// ); -// stateService.getSync.mockResolvedValue(getSyncConfiguration({ users: true, groups: true })); -// -// await expect(directoryService.getEntries(true, true)).rejects.toThrow(); -// }); -//}); From 887dbbc018b3839189b666c13f318e9f4dc1148e Mon Sep 17 00:00:00 2001 From: Brandon Date: Wed, 10 Jun 2026 15:56:03 -0400 Subject: [PATCH 4/5] update CI tests --- .github/workflows/integration-test.yml | 31 +++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index fdaea0ccd..c38bb2c36 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -17,6 +17,8 @@ on: - "libs/services/sync.service.ts" # core sync service used by all directory services - "libs/services/directory-services/ldap-directory.service*" # LDAP directory service - "libs/services/directory-services/gsuite-directory.service*" # Google Workspace directory service + - "libs/services/directory-services/entra-id-directory.service*" # Microsoft Entra ID directory service + - "libs/services/directory-services/okta-directory.service*" # Okta directory service # Add directory services here as we add test coverage pull_request: paths: @@ -27,6 +29,8 @@ on: - "libs/services/sync.service.ts" # core sync service used by all directory services - "libs/services/directory-services/ldap-directory.service*" # LDAP directory service - "libs/services/directory-services/gsuite-directory.service*" # Google Workspace directory service + - "libs/services/directory-services/entra-id-directory.service*" # Microsoft Entra ID directory service + - "libs/services/directory-services/okta-directory.service*" # Okta directory service # Add directory services here as we add test coverage permissions: contents: read @@ -74,7 +78,7 @@ jobs: uses: bitwarden/gh-actions/get-keyvault-secrets@main with: keyvault: gh-directory-connector - secrets: "GOOGLE-ADMIN-USER,GOOGLE-CLIENT-EMAIL,GOOGLE-DOMAIN,GOOGLE-PRIVATE-KEY" + secrets: "GOOGLE-ADMIN-USER,GOOGLE-CLIENT-EMAIL,GOOGLE-DOMAIN,GOOGLE-PRIVATE-KEY,ENTRA-TENANT-ID,ENTRA-APPLICATION-ID,ENTRA-KEY,OKTA-ORG-URL,OKTA-TOKEN" - name: Azure Logout uses: bitwarden/gh-actions/azure-logout@main @@ -100,6 +104,10 @@ jobs: - 'libs/services/directory-services/ldap-directory.service*' google: - 'libs/services/directory-services/gsuite-directory.service*' + entra: + - 'libs/services/directory-services/entra-id-directory.service*' + okta: + - 'libs/services/directory-services/okta-directory.service*' # LDAP - name: Setup LDAP integration tests @@ -128,6 +136,27 @@ jobs: run: | node --experimental-vm-modules node_modules/.bin/jest gsuite-directory.service.integration.spec.ts --coverage --coverageDirectory=coverage-google + # Microsoft Entra ID + - name: Run Entra ID integration tests + if: steps.changed-files.outputs.common == 'true' || steps.changed-files.outputs.entra == 'true' + env: + ENTRA_TENANT_ID: ${{ steps.get-kv-secrets.outputs.ENTRA-TENANT-ID }} + ENTRA_APPLICATION_ID: ${{ steps.get-kv-secrets.outputs.ENTRA-APPLICATION-ID }} + ENTRA_KEY: ${{ steps.get-kv-secrets.outputs.ENTRA-KEY }} + JEST_JUNIT_UNIQUE_OUTPUT_NAME: "true" # avoids junit outputs from clashing + run: | + node --experimental-vm-modules node_modules/.bin/jest entra-id-directory.service.integration.spec.ts --coverage --coverageDirectory=coverage-entra + + # Okta + - name: Run Okta integration tests + if: steps.changed-files.outputs.common == 'true' || steps.changed-files.outputs.okta == 'true' + env: + OKTA_ORG_URL: ${{ steps.get-kv-secrets.outputs.OKTA-ORG-URL }} + OKTA_TOKEN: ${{ steps.get-kv-secrets.outputs.OKTA-TOKEN }} + JEST_JUNIT_UNIQUE_OUTPUT_NAME: "true" # avoids junit outputs from clashing + run: | + node --experimental-vm-modules node_modules/.bin/jest okta-directory.service.integration.spec.ts --coverage --coverageDirectory=coverage-okta + - name: Report test results id: report uses: dorny/test-reporter@a43b3a5f7366b97d083190328d2c652e1a8b6aa2 # v3.0.0 From 3643bda60e2706e763db08bdefbae10cdf70b1a5 Mon Sep 17 00:00:00 2001 From: Brandon Date: Wed, 10 Jun 2026 15:59:26 -0400 Subject: [PATCH 5/5] fix tests --- ...okta-directory.service.integration.spec.ts | 5 +-- utils/okta/user-fixtures.ts | 35 +++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/libs/services/directory-services/okta-directory.service.integration.spec.ts b/libs/services/directory-services/okta-directory.service.integration.spec.ts index 43ad33d76..e809d4264 100644 --- a/libs/services/directory-services/okta-directory.service.integration.spec.ts +++ b/libs/services/directory-services/okta-directory.service.integration.spec.ts @@ -10,7 +10,7 @@ import { StateService } from "@/libs/abstractions/state.service"; import { getOktaConfiguration, getSyncConfiguration } from "../../../utils/okta/config-fixtures"; import { allGroupFixtures, integrationGroupFixtures } from "../../../utils/okta/group-fixtures"; -import { allUserFixtures, groupAUserFixtures } from "../../../utils/okta/user-fixtures"; +import { allUserFixtures, integrationGroupUserFixtures } from "../../../utils/okta/user-fixtures"; import { DirectoryType } from "../../enums/directoryType"; import { OktaDirectoryService } from "./okta-directory.service"; @@ -88,7 +88,8 @@ describe("oktaDirectoryService", () => { expect(groups).toEqual(expect.arrayContaining(integrationGroupFixtures)); expect(groups).toHaveLength(integrationGroupFixtures.length); - expect(users).toEqual(expect.arrayContaining(groupAUserFixtures)); + expect(users).toEqual(expect.arrayContaining(integrationGroupUserFixtures)); + expect(users).toHaveLength(integrationGroupUserFixtures.length); }); it("throws when credentials are invalid", async () => { diff --git a/utils/okta/user-fixtures.ts b/utils/okta/user-fixtures.ts index c3c37760d..450cea7cf 100644 --- a/utils/okta/user-fixtures.ts +++ b/utils/okta/user-fixtures.ts @@ -60,5 +60,40 @@ const groupAUsersData: Jsonify[] = [ }, ]; +// Union of Integration Test Group A and B members: Timo, Jimmy, Max, Oscar +const integrationGroupUsersData: Jsonify[] = [ + { + deleted: false, + disabled: false, + email: "timo@test.com", + externalId: "00u13t7bh9uQY8CwD698", + referenceId: "00u13t7bh9uQY8CwD698", + }, + { + deleted: false, + disabled: false, + email: "max@test.com", + externalId: "00u13t7bhpibaNW40698", + referenceId: "00u13t7bhpibaNW40698", + }, + { + deleted: false, + disabled: false, + email: "oscar@test.com", + externalId: "00u13t7c00brmAXIO698", + referenceId: "00u13t7c00brmAXIO698", + }, + { + deleted: false, + disabled: false, + email: "jimmy@test.com", + externalId: "00u13t7eifbXFPQKW698", + referenceId: "00u13t7eifbXFPQKW698", + }, +]; + export const allUserFixtures = allUsersData.map((u) => UserEntry.fromJSON(u)); export const groupAUserFixtures = groupAUsersData.map((u) => UserEntry.fromJSON(u)); +export const integrationGroupUserFixtures = integrationGroupUsersData.map((u) => + UserEntry.fromJSON(u), +);