1
0
mirror of https://github.com/bitwarden/directory-connector synced 2025-12-05 23:53:21 +00:00

[PM-26671] Google workspace integration tests (#894)

Add tests for Google Workspace - not enabled in CI yet
This commit is contained in:
Thomas Rittson
2025-10-28 11:31:02 +10:00
committed by GitHub
parent daeb96713f
commit fe01b49df1
26 changed files with 259 additions and 37 deletions

View File

@@ -8,14 +8,14 @@ on:
paths: paths:
- ".github/workflows/integration-test.yml" # this file - ".github/workflows/integration-test.yml" # this file
- "src/services/ldap-directory.service*" # we only have integration for LDAP testing at the moment - "src/services/ldap-directory.service*" # we only have integration for LDAP testing at the moment
- "./openldap/**/*" # any change to test fixtures - "./utils/**/*" # any change to test fixtures
- "./docker-compose.yml" # any change to Docker configuration - "./docker-compose.yml" # any change to Docker configuration
- "./package.json" # dependencies - "./package.json" # dependencies
pull_request: pull_request:
paths: paths:
- ".github/workflows/integration-test.yml" # this file - ".github/workflows/integration-test.yml" # this file
- "src/services/ldap-directory.service*" # we only have integration for LDAP testing at the moment - "src/services/ldap-directory.service*" # we only have integration for LDAP testing at the moment
- "./openldap/**/*" # any change to test fixtures - "./utils/**/*" # any change to test fixtures
- "./docker-compose.yml" # any change to Docker configuration - "./docker-compose.yml" # any change to Docker configuration
- "./package.json" # dependencies - "./package.json" # dependencies
permissions: permissions:
@@ -55,11 +55,11 @@ jobs:
sudo apt-get update sudo apt-get update
sudo apt-get -y install mkcert sudo apt-get -y install mkcert
- name: Setup integration tests - name: Setup LDAP integration tests
run: npm run test:integration:setup run: npm run test:integration:setup
- name: Run integration tests - name: Run LDAP integration tests
run: npm run test:integration --coverage run: npx jest ldap-directory.service.integration.spec.ts --coverage
- name: Report test results - name: Report test results
uses: dorny/test-reporter@dc3a92680fcc15842eef52e8c4606ea7ce6bd3f3 # v2.1.1 uses: dorny/test-reporter@dc3a92680fcc15842eef52e8c4606ea7ce6bd3f3 # v2.1.1

3
.gitignore vendored
View File

@@ -2,6 +2,9 @@
.DS_Store .DS_Store
Thumbs.db Thumbs.db
# Environment variables used for tests
.env
# IDEs and editors # IDEs and editors
.idea/ .idea/
.project .project

View File

@@ -11,8 +11,8 @@ services:
- LDAP_TLS_KEY_FILE=/certs/openldap-key.pem - LDAP_TLS_KEY_FILE=/certs/openldap-key.pem
- LDAP_TLS_CA_FILE=/certs/rootCA.pem - LDAP_TLS_CA_FILE=/certs/rootCA.pem
volumes: volumes:
- "./openldap/ldifs:/ldifs" - "./utils/openldap/ldifs:/ldifs"
- "./openldap/certs:/certs" - "./utils/openldap/certs:/certs"
ports: ports:
- "1389:1389" - "1389:1389"
- "1636:1636" - "1636:1636"

View File

@@ -1,10 +0,0 @@
if ! [ -x "$(command -v mkcert)" ]; then
echo 'Error: mkcert is not installed. Install mkcert first and then re-run this script.'
echo 'e.g. brew install mkcert'
exit 1
fi
mkcert -install
mkdir -p ./openldap/certs
cp "$(mkcert -CAROOT)/rootCA.pem" ./openldap/certs/rootCA.pem
mkcert -key-file ./openldap/certs/openldap-key.pem -cert-file ./openldap/certs/openldap.pem localhost openldap

View File

@@ -69,7 +69,7 @@
"test:watch:all": "jest --watchAll --testPathIgnorePatterns=.integration.spec.ts", "test:watch:all": "jest --watchAll --testPathIgnorePatterns=.integration.spec.ts",
"test:integration": "jest .integration.spec.ts", "test:integration": "jest .integration.spec.ts",
"test:integration:watch": "jest .integration.spec.ts --watch", "test:integration:watch": "jest .integration.spec.ts --watch",
"test:integration:setup": "sh ./openldap/mkcert.sh && docker compose up -d", "test:integration:setup": "sh ./utils/openldap/mkcert.sh && docker compose up -d",
"test:types": "npx tsc --noEmit" "test:types": "npx tsc --noEmit"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -2,8 +2,8 @@ import { GetUniqueString } from "@/jslib/common/spec/utils";
import { UserEntry } from "@/src/models/userEntry"; import { UserEntry } from "@/src/models/userEntry";
import { groupSimulator, userSimulator } from "../../utils/request-builder-helper";
import { RequestBuilderOptions } from "../abstractions/request-builder.service"; import { RequestBuilderOptions } from "../abstractions/request-builder.service";
import { groupSimulator, userSimulator } from "../utils/request-builder-helper";
import { BatchRequestBuilder } from "./batch-request-builder"; import { BatchRequestBuilder } from "./batch-request-builder";

View File

@@ -0,0 +1,82 @@
import { config as dotenvConfig } from "dotenv";
import { mock, MockProxy } from "jest-mock-extended";
import { I18nService } from "../../jslib/common/src/abstractions/i18n.service";
import { LogService } from "../../jslib/common/src/abstractions/log.service";
import {
getGSuiteConfiguration,
getSyncConfiguration,
} from "../../utils/google-workspace/config-fixtures";
import { groupFixtures } from "../../utils/google-workspace/group-fixtures";
import { userFixtures } from "../../utils/google-workspace/user-fixtures";
import { DirectoryType } from "../enums/directoryType";
import { GSuiteDirectoryService } from "./gsuite-directory.service";
import { StateService } from "./state.service";
// These tests integrate with a test Google Workspace instance.
// 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" });
// These filters target integration test data.
// These should return data that matches the user and group fixtures exactly.
// There may be additional data present if not used.
const INTEGRATION_USER_FILTER =
"exclude:integration-user-a@bwrox.dev|orgUnitPath='/Integration testing'";
const INTEGRATION_GROUP_FILTER = "|name:Integration*";
describe("gsuiteDirectoryService", () => {
let logService: MockProxy<LogService>;
let i18nService: MockProxy<I18nService>;
let stateService: MockProxy<StateService>;
let directoryService: GSuiteDirectoryService;
beforeEach(() => {
logService = mock();
i18nService = mock();
stateService = mock();
stateService.getDirectoryType.mockResolvedValue(DirectoryType.GSuite);
stateService.getLastUserSync.mockResolvedValue(null); // do not filter results by last modified date
i18nService.t.mockImplementation((id) => id); // passthrough implementation for any error messages
directoryService = new GSuiteDirectoryService(logService, i18nService, stateService);
});
it("syncs without using filters (includes test data)", async () => {
const directoryConfig = getGSuiteConfiguration();
stateService.getDirectory.calledWith(DirectoryType.GSuite).mockResolvedValue(directoryConfig);
const syncConfig = getSyncConfiguration({
groups: true,
users: true,
});
stateService.getSync.mockResolvedValue(syncConfig);
const result = await directoryService.getEntries(true, true);
expect(result[0]).toEqual(expect.arrayContaining(groupFixtures));
expect(result[1]).toEqual(expect.arrayContaining(userFixtures));
});
it("syncs using user and group filters (exact match for test data)", async () => {
const directoryConfig = getGSuiteConfiguration();
stateService.getDirectory.calledWith(DirectoryType.GSuite).mockResolvedValue(directoryConfig);
const syncConfig = getSyncConfiguration({
groups: true,
users: true,
userFilter: INTEGRATION_USER_FILTER,
groupFilter: INTEGRATION_GROUP_FILTER,
});
stateService.getSync.mockResolvedValue(syncConfig);
const result = await directoryService.getEntries(true, true);
expect(result).toEqual([groupFixtures, userFixtures]);
});
});

View File

@@ -2,10 +2,10 @@ import { mock, MockProxy } from "jest-mock-extended";
import { I18nService } from "../../jslib/common/src/abstractions/i18n.service"; import { I18nService } from "../../jslib/common/src/abstractions/i18n.service";
import { LogService } from "../../jslib/common/src/abstractions/log.service"; import { LogService } from "../../jslib/common/src/abstractions/log.service";
import { groupFixtures } from "../../openldap/group-fixtures"; import { getLdapConfiguration, getSyncConfiguration } from "../../utils/openldap/config-fixtures";
import { userFixtures } from "../../openldap/user-fixtures"; import { groupFixtures } from "../../utils/openldap/group-fixtures";
import { userFixtures } from "../../utils/openldap/user-fixtures";
import { DirectoryType } from "../enums/directoryType"; import { DirectoryType } from "../enums/directoryType";
import { getLdapConfiguration, getSyncConfiguration } from "../utils/test-fixtures";
import { LdapDirectoryService } from "./ldap-directory.service"; import { LdapDirectoryService } from "./ldap-directory.service";
import { StateService } from "./state.service"; import { StateService } from "./state.service";
@@ -52,7 +52,7 @@ describe("ldapDirectoryService", () => {
getLdapConfiguration({ getLdapConfiguration({
ssl: true, ssl: true,
startTls: true, startTls: true,
tlsCaPath: "./openldap/certs/rootCA.pem", tlsCaPath: "./utils/openldap/certs/rootCA.pem",
}), }),
); );
stateService.getSync.mockResolvedValue(getSyncConfiguration({ groups: true, users: true })); stateService.getSync.mockResolvedValue(getSyncConfiguration({ groups: true, users: true }));
@@ -67,7 +67,7 @@ describe("ldapDirectoryService", () => {
getLdapConfiguration({ getLdapConfiguration({
port: 1636, port: 1636,
ssl: true, ssl: true,
sslCaPath: "./openldap/certs/rootCA.pem", sslCaPath: "./utils/openldap/certs/rootCA.pem",
}), }),
); );
stateService.getSync.mockResolvedValue(getSyncConfiguration({ groups: true, users: true })); stateService.getSync.mockResolvedValue(getSyncConfiguration({ groups: true, users: true }));

View File

@@ -2,8 +2,8 @@ import { GetUniqueString } from "@/jslib/common/spec/utils";
import { UserEntry } from "@/src/models/userEntry"; import { UserEntry } from "@/src/models/userEntry";
import { groupSimulator, userSimulator } from "../../utils/request-builder-helper";
import { RequestBuilderOptions } from "../abstractions/request-builder.service"; import { RequestBuilderOptions } from "../abstractions/request-builder.service";
import { groupSimulator, userSimulator } from "../utils/request-builder-helper";
import { SingleRequestBuilder } from "./single-request-builder"; import { SingleRequestBuilder } from "./single-request-builder";

View File

@@ -7,11 +7,9 @@ import { EnvironmentService } from "@/jslib/common/src/services/environment.serv
import { I18nService } from "../../jslib/common/src/abstractions/i18n.service"; import { I18nService } from "../../jslib/common/src/abstractions/i18n.service";
import { LogService } from "../../jslib/common/src/abstractions/log.service"; import { LogService } from "../../jslib/common/src/abstractions/log.service";
import { groupFixtures } from "../../openldap/group-fixtures"; import { getLdapConfiguration, getSyncConfiguration } from "../../utils/openldap/config-fixtures";
import { userFixtures } from "../../openldap/user-fixtures";
import { DirectoryFactoryService } from "../abstractions/directory-factory.service"; import { DirectoryFactoryService } from "../abstractions/directory-factory.service";
import { DirectoryType } from "../enums/directoryType"; import { DirectoryType } from "../enums/directoryType";
import { getLdapConfiguration, getSyncConfiguration } from "../utils/test-fixtures";
import { BatchRequestBuilder } from "./batch-request-builder"; import { BatchRequestBuilder } from "./batch-request-builder";
import { LdapDirectoryService } from "./ldap-directory.service"; import { LdapDirectoryService } from "./ldap-directory.service";
@@ -20,6 +18,9 @@ import { StateService } from "./state.service";
import { SyncService } from "./sync.service"; import { SyncService } from "./sync.service";
import * as constants from "./sync.service"; import * as constants from "./sync.service";
import { groupFixtures } from "@/utils/openldap/group-fixtures";
import { userFixtures } from "@/utils/openldap/user-fixtures";
describe("SyncService", () => { describe("SyncService", () => {
let logService: MockProxy<LogService>; let logService: MockProxy<LogService>;
let i18nService: MockProxy<I18nService>; let i18nService: MockProxy<I18nService>;

View File

@@ -6,9 +6,9 @@ import { MessagingService } from "@/jslib/common/src/abstractions/messaging.serv
import { OrganizationImportRequest } from "@/jslib/common/src/models/request/organizationImportRequest"; import { OrganizationImportRequest } from "@/jslib/common/src/models/request/organizationImportRequest";
import { ApiService } from "@/jslib/common/src/services/api.service"; import { ApiService } from "@/jslib/common/src/services/api.service";
import { getSyncConfiguration } from "../../utils/openldap/config-fixtures";
import { DirectoryFactoryService } from "../abstractions/directory-factory.service"; import { DirectoryFactoryService } from "../abstractions/directory-factory.service";
import { DirectoryType } from "../enums/directoryType"; import { DirectoryType } from "../enums/directoryType";
import { getSyncConfiguration } from "../utils/test-fixtures";
import { BatchRequestBuilder } from "./batch-request-builder"; import { BatchRequestBuilder } from "./batch-request-builder";
import { I18nService } from "./i18n.service"; import { I18nService } from "./i18n.service";
@@ -18,8 +18,8 @@ import { StateService } from "./state.service";
import { SyncService } from "./sync.service"; import { SyncService } from "./sync.service";
import * as constants from "./sync.service"; import * as constants from "./sync.service";
import { groupFixtures } from "@/openldap/group-fixtures"; import { groupFixtures } from "@/utils/openldap/group-fixtures";
import { userFixtures } from "@/openldap/user-fixtures"; import { userFixtures } from "@/utils/openldap/user-fixtures";
describe("SyncService", () => { describe("SyncService", () => {
let cryptoFunctionService: MockProxy<CryptoFunctionService>; let cryptoFunctionService: MockProxy<CryptoFunctionService>;

4
utils/.env.example Normal file
View File

@@ -0,0 +1,4 @@
GOOGLE_DOMAIN=
GOOGLE_ADMIN_USER=
GOOGLE_CLIENT_EMAIL=
GOOGLE_PRIVATE_KEY=

View File

@@ -0,0 +1,56 @@
import { GSuiteConfiguration } from "../../src/models/gsuiteConfiguration";
import { SyncConfiguration } from "../../src/models/syncConfiguration";
/**
* @returns a basic GSuite configuration. Can be overridden by passing in a partial configuration.
*/
export const getGSuiteConfiguration = (
config?: Partial<GSuiteConfiguration>,
): GSuiteConfiguration => {
const adminUser = process.env.GOOGLE_ADMIN_USER;
const clientEmail = process.env.GOOGLE_CLIENT_EMAIL;
const privateKey = process.env.GOOGLE_PRIVATE_KEY;
const domain = process.env.GOOGLE_DOMAIN;
if (!adminUser || !clientEmail || !privateKey || !domain) {
throw new Error("Google Workspace integration test credentials not configured.");
}
return {
// TODO
adminUser,
clientEmail,
privateKey,
domain: domain,
customer: "",
...(config ?? {}),
};
};
/**
* @returns a basic Google Workspace 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 ?? {}),
});

View File

@@ -0,0 +1,26 @@
import { Jsonify } from "type-fest";
import { GroupEntry } from "../../src/models/groupEntry";
// These must match the Google Workspace seed data
const data: Jsonify<GroupEntry>[] = [
{
externalId: "0319y80a3anpxhj",
groupMemberReferenceIds: [],
name: "Integration Test Group A",
referenceId: "0319y80a3anpxhj",
userMemberExternalIds: ["111605910541641314041", "111147009830456099026"],
users: [],
},
{
externalId: "02afmg28317uyub",
groupMemberReferenceIds: [],
name: "Integration Test Group B",
referenceId: "02afmg28317uyub",
userMemberExternalIds: ["111147009830456099026", "100150970267699397306"],
users: [],
},
];
export const groupFixtures = data.map((g) => GroupEntry.fromJSON(g));

View File

@@ -0,0 +1,50 @@
import { Jsonify } from "type-fest";
import { UserEntry } from "../../src/models/userEntry";
// These must match the Google Workspace seed data
const data: Jsonify<UserEntry>[] = [
// In Group A
{
deleted: false,
disabled: false,
email: "testuser1@bwrox.dev",
externalId: "111605910541641314041",
referenceId: "111605910541641314041",
},
// In Groups A + B
{
deleted: false,
disabled: false,
email: "testuser2@bwrox.dev",
externalId: "111147009830456099026",
referenceId: "111147009830456099026",
},
// In Group B
{
deleted: false,
disabled: false,
email: "testuser3@bwrox.dev",
externalId: "100150970267699397306",
referenceId: "100150970267699397306",
},
// Not in a group
{
deleted: false,
disabled: false,
email: "testuser4@bwrox.dev",
externalId: "113764752650306721470",
referenceId: "113764752650306721470",
},
// Disabled user
{
deleted: false,
disabled: true,
email: "testuser5@bwrox.dev",
externalId: "110381976819725658200",
referenceId: "110381976819725658200",
},
];
export const userFixtures = data.map((g) => UserEntry.fromJSON(g));

View File

@@ -1,5 +1,5 @@
import { LdapConfiguration } from "../models/ldapConfiguration"; import { LdapConfiguration } from "../../src/models/ldapConfiguration";
import { SyncConfiguration } from "../models/syncConfiguration"; import { SyncConfiguration } from "../../src/models/syncConfiguration";
/** /**
* @returns a basic ldap configuration without TLS/SSL enabled. Can be overridden by passing in a partial configuration. * @returns a basic ldap configuration without TLS/SSL enabled. Can be overridden by passing in a partial configuration.

View File

@@ -1,6 +1,6 @@
import { Jsonify } from "type-fest"; import { Jsonify } from "type-fest";
import { GroupEntry } from "../src/models/groupEntry"; import { GroupEntry } from "@/src/models/groupEntry";
// These must match the ldap server seed data in directory.ldif // These must match the ldap server seed data in directory.ldif
const data: Jsonify<GroupEntry>[] = [ const data: Jsonify<GroupEntry>[] = [

10
utils/openldap/mkcert.sh Executable file
View File

@@ -0,0 +1,10 @@
if ! [ -x "$(command -v mkcert)" ]; then
echo 'Error: mkcert is not installed. Install mkcert first and then re-run this script.'
echo 'e.g. brew install mkcert'
exit 1
fi
mkcert -install
mkdir -p ./utils/openldap/certs
cp "$(mkcert -CAROOT)/rootCA.pem" ./utils/openldap/certs/rootCA.pem
mkcert -key-file ./utils/openldap/certs/openldap-key.pem -cert-file ./utils/openldap/certs/openldap.pem localhost openldap

View File

@@ -1,6 +1,6 @@
import { Jsonify } from "type-fest"; import { Jsonify } from "type-fest";
import { UserEntry } from "../src/models/userEntry"; import { UserEntry } from "@/src/models/userEntry";
// These must match the ldap server seed data in directory.ldif // These must match the ldap server seed data in directory.ldif
const data: Jsonify<UserEntry>[] = [ const data: Jsonify<UserEntry>[] = [

View File

@@ -1,7 +1,7 @@
import { GetUniqueString } from "@/jslib/common/spec/utils"; import { GetUniqueString } from "@/jslib/common/spec/utils";
import { GroupEntry } from "../models/groupEntry"; import { GroupEntry } from "../src/models/groupEntry";
import { UserEntry } from "../models/userEntry"; import { UserEntry } from "../src/models/userEntry";
export function userSimulator(userCount: number): UserEntry[] { export function userSimulator(userCount: number): UserEntry[] {
const users: UserEntry[] = []; const users: UserEntry[] = [];