Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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<LogService>;
let i18nService: MockProxy<I18nService>;
let stateService: MockProxy<StateService>;

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();
});
});
Original file line number Diff line number Diff line change
@@ -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 { 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<LogService>;
let i18nService: MockProxy<I18nService>;
let stateService: MockProxy<StateService>;

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);
Comment thread
BTreston marked this conversation as resolved.

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();
});
});
53 changes: 53 additions & 0 deletions utils/entra/config-fixtures.ts
Original file line number Diff line number Diff line change
@@ -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>,
): 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>): 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 ?? {}),
});
46 changes: 46 additions & 0 deletions utils/entra/group-fixtures.ts
Original file line number Diff line number Diff line change
@@ -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<GroupEntry>[] = [
{
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<GroupEntry>[] = [
{
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));
78 changes: 78 additions & 0 deletions utils/entra/user-fixtures.ts
Original file line number Diff line number Diff line change
@@ -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<UserEntry>[] = [
{
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<UserEntry>[] = [
{
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));
Loading
Loading