From a44eb28be84f6bb3f296de1374ca9c1babfdcdf3 Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Wed, 12 Nov 2025 06:03:37 +1000 Subject: [PATCH] [PM-26672] Add Google Workspace integration tests to CI pipeline (#909) - reorganize integration test files to allow for future additions - add Google Workspace integration tests to the Github workflow - refactor to run tests selective based on changed files and use Azure Key Vault --- .github/workflows/integration-test.yml | 99 ++++++++++++++++--- .gitignore | 4 +- src/abstractions/directory-factory.service.ts | 2 +- src/locales/en/messages.json | 3 + src/services/directory-factory.service.ts | 10 +- .../directory.service.ts | 4 +- .../entra-id-directory.service.ts | 14 +-- ...uite-directory.service.integration.spec.ts | 21 ++-- .../gsuite-directory.service.ts | 24 +++-- ...ldap-directory.service.integration.spec.ts | 17 ++-- .../ldap-directory.service.ts | 12 +-- .../okta-directory.service.ts | 14 +-- .../onelogin-directory.service.ts | 14 +-- src/services/sync.service.integration.spec.ts | 2 +- src/services/sync.service.spec.ts | 2 +- 15 files changed, 163 insertions(+), 79 deletions(-) rename src/services/{ => directory-services}/directory.service.ts (53%) rename src/services/{ => directory-services}/entra-id-directory.service.ts (97%) rename src/services/{ => directory-services}/gsuite-directory.service.integration.spec.ts (80%) rename src/services/{ => directory-services}/gsuite-directory.service.ts (90%) rename src/services/{ => directory-services}/ldap-directory.service.integration.spec.ts (91%) rename src/services/{ => directory-services}/ldap-directory.service.ts (97%) rename src/services/{ => directory-services}/okta-directory.service.ts (95%) rename src/services/{ => directory-services}/onelogin-directory.service.ts (93%) diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index d08f6729..05dc69ab 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -2,25 +2,36 @@ name: Integration Testing on: workflow_dispatch: + # Integration tests are slow, so only run them if relevant files have changed. + # This is done at the workflow level and at the job level. + # Make sure these triggers stay consistent with the 'changed-files' job. push: branches: - - "main" + - 'main' + - 'rc' paths: - ".github/workflows/integration-test.yml" # this file - - "src/services/ldap-directory.service*" # we only have integration for LDAP testing at the moment - - "./utils/**/*" # any change to test fixtures - - "./docker-compose.yml" # any change to Docker configuration - - "./package.json" # dependencies + - "docker-compose.yml" # any change to Docker configuration + - "package.json" # dependencies + - "utils/**" # any change to test fixtures + - "src/services/sync.service.ts" # core sync service used by all directory services + - "src/services/directory-services/ldap-directory.service*" # LDAP directory service + - "src/services/directory-services/gsuite-directory.service*" # Google Workspace directory service + # Add directory services here as we add test coverage pull_request: paths: - ".github/workflows/integration-test.yml" # this file - - "src/services/ldap-directory.service*" # we only have integration for LDAP testing at the moment - - "./utils/**/*" # any change to test fixtures - - "./docker-compose.yml" # any change to Docker configuration - - "./package.json" # dependencies + - "docker-compose.yml" # any change to Docker configuration + - "package.json" # dependencies + - "utils/**" # any change to test fixtures + - "src/services/sync.service.ts" # core sync service used by all directory services + - "src/services/directory-services/ldap-directory.service*" # LDAP directory service + - "src/services/directory-services/gsuite-directory.service*" # Google Workspace directory service + # Add directory services here as we add test coverage permissions: contents: read checks: write # required by dorny/test-reporter to upload its results + id-token: write # required to use OIDC to login to Azure Key Vault jobs: testing: name: Run tests @@ -50,23 +61,79 @@ jobs: - name: Install Node dependencies run: npm ci - - name: Install mkcert + # Get secrets from Azure Key Vault + - name: Azure Login + uses: bitwarden/gh-actions/azure-login@main + with: + subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + tenant_id: ${{ secrets.AZURE_TENANT_ID }} + client_id: ${{ secrets.AZURE_CLIENT_ID }} + + - name: Get KV Secrets + id: get-kv-secrets + 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" + + - name: Azure Logout + uses: bitwarden/gh-actions/azure-logout@main + + # Only run relevant tests depending on what files have changed. + # This should be kept consistent with the workflow level triggers. + # Note: docker-compose.yml is only used for ldap for now + - name: Get changed files + id: changed-files + uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 + with: + list-files: shell + token: ${{ secrets.GITHUB_TOKEN }} + # Add directory services here as we add test coverage + filters: | + common: + - '.github/workflows/integration-test.yml' + - 'utils/**' + - 'package.json' + - 'src/services/sync.service.ts' + ldap: + - 'docker-compose.yml' + - 'src/services/directory-services/ldap-directory.service*' + google: + - 'src/services/directory-services/gsuite-directory.service*' + + # LDAP + - name: Setup LDAP integration tests + if: steps.changed-files.outputs.common == 'true' || steps.changed-files.outputs.ldap == 'true' run: | sudo apt-get update sudo apt-get -y install mkcert - - - name: Setup LDAP integration tests - run: npm run test:integration:setup + npm run test:integration:setup - name: Run LDAP integration tests - run: npx jest ldap-directory.service.integration.spec.ts --coverage + if: steps.changed-files.outputs.common == 'true' || steps.changed-files.outputs.ldap == 'true' + env: + JEST_JUNIT_UNIQUE_OUTPUT_NAME: "true" # avoids junit outputs from clashing + run: npx jest ldap-directory.service.integration.spec.ts --coverage --coverageDirectory=coverage-ldap + + # Google Workspace + - name: Run Google Workspace integration tests + if: steps.changed-files.outputs.common == 'true' || steps.changed-files.outputs.google == 'true' + env: + GOOGLE_DOMAIN: ${{ steps.get-kv-secrets.outputs.GOOGLE-DOMAIN }} + GOOGLE_ADMIN_USER: ${{ steps.get-kv-secrets.outputs.GOOGLE-ADMIN-USER }} + GOOGLE_CLIENT_EMAIL: ${{ steps.get-kv-secrets.outputs.GOOGLE-CLIENT-EMAIL }} + GOOGLE_PRIVATE_KEY: ${{ steps.get-kv-secrets.outputs.GOOGLE-PRIVATE-KEY }} + JEST_JUNIT_UNIQUE_OUTPUT_NAME: "true" # avoids junit outputs from clashing + run: | + npx jest gsuite-directory.service.integration.spec.ts --coverage --coverageDirectory=coverage-google - name: Report test results + id: report uses: dorny/test-reporter@dc3a92680fcc15842eef52e8c4606ea7ce6bd3f3 # v2.1.1 - if: ${{ github.event.pull_request.head.repo.full_name == github.repository && !cancelled() }} + if: github.event.pull_request.head.repo.full_name == github.repository && !cancelled() with: name: Test Results - path: "junit.xml" + path: "junit.xml*" reporter: jest-junit fail-on-error: true diff --git a/.gitignore b/.gitignore index 69cc709d..295b7928 100644 --- a/.gitignore +++ b/.gitignore @@ -33,8 +33,8 @@ build-cli .angular/cache # Testing -coverage -junit.xml +coverage* +junit.xml* # Misc *.crx diff --git a/src/abstractions/directory-factory.service.ts b/src/abstractions/directory-factory.service.ts index 90d2f674..2f775afc 100644 --- a/src/abstractions/directory-factory.service.ts +++ b/src/abstractions/directory-factory.service.ts @@ -1,5 +1,5 @@ import { DirectoryType } from "@/src/enums/directoryType"; -import { IDirectoryService } from "@/src/services/directory.service"; +import { IDirectoryService } from "@/src/services/directory-services/directory.service"; export abstract class DirectoryFactoryService { abstract createService(type: DirectoryType): IDirectoryService; diff --git a/src/locales/en/messages.json b/src/locales/en/messages.json index b8859b1b..272991f3 100644 --- a/src/locales/en/messages.json +++ b/src/locales/en/messages.json @@ -768,5 +768,8 @@ }, "launchWebVault": { "message": "Launch Web Vault" + }, + "authenticationFailed": { + "message": "Authentication failed" } } diff --git a/src/services/directory-factory.service.ts b/src/services/directory-factory.service.ts index e3da3c04..eccea5c2 100644 --- a/src/services/directory-factory.service.ts +++ b/src/services/directory-factory.service.ts @@ -5,11 +5,11 @@ import { DirectoryFactoryService } from "../abstractions/directory-factory.servi import { StateService } from "../abstractions/state.service"; import { DirectoryType } from "../enums/directoryType"; -import { EntraIdDirectoryService } from "./entra-id-directory.service"; -import { GSuiteDirectoryService } from "./gsuite-directory.service"; -import { LdapDirectoryService } from "./ldap-directory.service"; -import { OktaDirectoryService } from "./okta-directory.service"; -import { OneLoginDirectoryService } from "./onelogin-directory.service"; +import { EntraIdDirectoryService } from "./directory-services/entra-id-directory.service"; +import { GSuiteDirectoryService } from "./directory-services/gsuite-directory.service"; +import { LdapDirectoryService } from "./directory-services/ldap-directory.service"; +import { OktaDirectoryService } from "./directory-services/okta-directory.service"; +import { OneLoginDirectoryService } from "./directory-services/onelogin-directory.service"; export class DefaultDirectoryFactoryService implements DirectoryFactoryService { constructor( diff --git a/src/services/directory.service.ts b/src/services/directory-services/directory.service.ts similarity index 53% rename from src/services/directory.service.ts rename to src/services/directory-services/directory.service.ts index e13f8732..d7937e41 100644 --- a/src/services/directory.service.ts +++ b/src/services/directory-services/directory.service.ts @@ -1,5 +1,5 @@ -import { GroupEntry } from "../models/groupEntry"; -import { UserEntry } from "../models/userEntry"; +import { GroupEntry } from "../../models/groupEntry"; +import { UserEntry } from "../../models/userEntry"; export interface IDirectoryService { getEntries(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]>; diff --git a/src/services/entra-id-directory.service.ts b/src/services/directory-services/entra-id-directory.service.ts similarity index 97% rename from src/services/entra-id-directory.service.ts rename to src/services/directory-services/entra-id-directory.service.ts index 449e11b5..d76f57a5 100644 --- a/src/services/entra-id-directory.service.ts +++ b/src/services/directory-services/entra-id-directory.service.ts @@ -7,14 +7,14 @@ import * as graphType from "@microsoft/microsoft-graph-types"; import { I18nService } from "@/jslib/common/src/abstractions/i18n.service"; import { LogService } from "@/jslib/common/src/abstractions/log.service"; -import { StateService } from "../abstractions/state.service"; -import { DirectoryType } from "../enums/directoryType"; -import { EntraIdConfiguration } from "../models/entraIdConfiguration"; -import { GroupEntry } from "../models/groupEntry"; -import { SyncConfiguration } from "../models/syncConfiguration"; -import { UserEntry } from "../models/userEntry"; +import { StateService } from "../../abstractions/state.service"; +import { DirectoryType } from "../../enums/directoryType"; +import { EntraIdConfiguration } from "../../models/entraIdConfiguration"; +import { GroupEntry } from "../../models/groupEntry"; +import { SyncConfiguration } from "../../models/syncConfiguration"; +import { UserEntry } from "../../models/userEntry"; +import { BaseDirectoryService } from "../baseDirectory.service"; -import { BaseDirectoryService } from "./baseDirectory.service"; import { IDirectoryService } from "./directory.service"; const EntraIdPublicIdentityAuthority = "login.microsoftonline.com"; diff --git a/src/services/gsuite-directory.service.integration.spec.ts b/src/services/directory-services/gsuite-directory.service.integration.spec.ts similarity index 80% rename from src/services/gsuite-directory.service.integration.spec.ts rename to src/services/directory-services/gsuite-directory.service.integration.spec.ts index c58de957..397e594c 100644 --- a/src/services/gsuite-directory.service.integration.spec.ts +++ b/src/services/directory-services/gsuite-directory.service.integration.spec.ts @@ -1,18 +1,18 @@ 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 { 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"; +} 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 { StateService } from "../state.service"; 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. @@ -24,10 +24,13 @@ 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_USER_FILTER = "|orgUnitPath='/Integration testing'"; const INTEGRATION_GROUP_FILTER = "|name:Integration*"; +// These tests are slow! +// Increase the default timeout from 5s to 15s +jest.setTimeout(15000); + describe("gsuiteDirectoryService", () => { let logService: MockProxy; let i18nService: MockProxy; diff --git a/src/services/gsuite-directory.service.ts b/src/services/directory-services/gsuite-directory.service.ts similarity index 90% rename from src/services/gsuite-directory.service.ts rename to src/services/directory-services/gsuite-directory.service.ts index 714818eb..95714fb4 100644 --- a/src/services/gsuite-directory.service.ts +++ b/src/services/directory-services/gsuite-directory.service.ts @@ -4,14 +4,14 @@ import { admin_directory_v1, google } from "googleapis"; import { I18nService } from "@/jslib/common/src/abstractions/i18n.service"; import { LogService } from "@/jslib/common/src/abstractions/log.service"; -import { StateService } from "../abstractions/state.service"; -import { DirectoryType } from "../enums/directoryType"; -import { GroupEntry } from "../models/groupEntry"; -import { GSuiteConfiguration } from "../models/gsuiteConfiguration"; -import { SyncConfiguration } from "../models/syncConfiguration"; -import { UserEntry } from "../models/userEntry"; +import { StateService } from "../../abstractions/state.service"; +import { DirectoryType } from "../../enums/directoryType"; +import { GroupEntry } from "../../models/groupEntry"; +import { GSuiteConfiguration } from "../../models/gsuiteConfiguration"; +import { SyncConfiguration } from "../../models/syncConfiguration"; +import { UserEntry } from "../../models/userEntry"; +import { BaseDirectoryService } from "../baseDirectory.service"; -import { BaseDirectoryService } from "./baseDirectory.service"; import { IDirectoryService } from "./directory.service"; export class GSuiteDirectoryService extends BaseDirectoryService implements IDirectoryService { @@ -253,7 +253,15 @@ export class GSuiteDirectoryService extends BaseDirectoryService implements IDir ], }); - await this.client.authorize(); + try { + await this.client.authorize(); + } catch (error) { + // Catch and rethrow this to sanitize any sensitive info (e.g. private key) in the error message + this.logService.error( + `Google Workspace authentication failed: ${error?.name || "Unknown error"}`, + ); + throw new Error(this.i18nService.t("authenticationFailed")); + } this.authParams = { auth: this.client, diff --git a/src/services/ldap-directory.service.integration.spec.ts b/src/services/directory-services/ldap-directory.service.integration.spec.ts similarity index 91% rename from src/services/ldap-directory.service.integration.spec.ts rename to src/services/directory-services/ldap-directory.service.integration.spec.ts index c780f4c6..67d1be55 100644 --- a/src/services/ldap-directory.service.integration.spec.ts +++ b/src/services/directory-services/ldap-directory.service.integration.spec.ts @@ -1,14 +1,17 @@ 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 { getLdapConfiguration, getSyncConfiguration } from "../../utils/openldap/config-fixtures"; -import { groupFixtures } from "../../utils/openldap/group-fixtures"; -import { userFixtures } from "../../utils/openldap/user-fixtures"; -import { DirectoryType } from "../enums/directoryType"; +import { I18nService } from "../../../jslib/common/src/abstractions/i18n.service"; +import { LogService } from "../../../jslib/common/src/abstractions/log.service"; +import { + getLdapConfiguration, + getSyncConfiguration, +} from "../../../utils/openldap/config-fixtures"; +import { groupFixtures } from "../../../utils/openldap/group-fixtures"; +import { userFixtures } from "../../../utils/openldap/user-fixtures"; +import { DirectoryType } from "../../enums/directoryType"; +import { StateService } from "../state.service"; import { LdapDirectoryService } from "./ldap-directory.service"; -import { StateService } from "./state.service"; // These tests integrate with the OpenLDAP docker image and seed data located in the openldap folder. // To run theses tests: diff --git a/src/services/ldap-directory.service.ts b/src/services/directory-services/ldap-directory.service.ts similarity index 97% rename from src/services/ldap-directory.service.ts rename to src/services/directory-services/ldap-directory.service.ts index b08f0a08..b9893885 100644 --- a/src/services/ldap-directory.service.ts +++ b/src/services/directory-services/ldap-directory.service.ts @@ -7,12 +7,12 @@ import { I18nService } from "@/jslib/common/src/abstractions/i18n.service"; import { LogService } from "@/jslib/common/src/abstractions/log.service"; import { Utils } from "@/jslib/common/src/misc/utils"; -import { StateService } from "../abstractions/state.service"; -import { DirectoryType } from "../enums/directoryType"; -import { GroupEntry } from "../models/groupEntry"; -import { LdapConfiguration } from "../models/ldapConfiguration"; -import { SyncConfiguration } from "../models/syncConfiguration"; -import { UserEntry } from "../models/userEntry"; +import { StateService } from "../../abstractions/state.service"; +import { DirectoryType } from "../../enums/directoryType"; +import { GroupEntry } from "../../models/groupEntry"; +import { LdapConfiguration } from "../../models/ldapConfiguration"; +import { SyncConfiguration } from "../../models/syncConfiguration"; +import { UserEntry } from "../../models/userEntry"; import { IDirectoryService } from "./directory.service"; diff --git a/src/services/okta-directory.service.ts b/src/services/directory-services/okta-directory.service.ts similarity index 95% rename from src/services/okta-directory.service.ts rename to src/services/directory-services/okta-directory.service.ts index 2c2d9913..c73fd49a 100644 --- a/src/services/okta-directory.service.ts +++ b/src/services/directory-services/okta-directory.service.ts @@ -3,14 +3,14 @@ import * as https from "https"; import { I18nService } from "@/jslib/common/src/abstractions/i18n.service"; import { LogService } from "@/jslib/common/src/abstractions/log.service"; -import { StateService } from "../abstractions/state.service"; -import { DirectoryType } from "../enums/directoryType"; -import { GroupEntry } from "../models/groupEntry"; -import { OktaConfiguration } from "../models/oktaConfiguration"; -import { SyncConfiguration } from "../models/syncConfiguration"; -import { UserEntry } from "../models/userEntry"; +import { StateService } from "../../abstractions/state.service"; +import { DirectoryType } from "../../enums/directoryType"; +import { GroupEntry } from "../../models/groupEntry"; +import { OktaConfiguration } from "../../models/oktaConfiguration"; +import { SyncConfiguration } from "../../models/syncConfiguration"; +import { UserEntry } from "../../models/userEntry"; +import { BaseDirectoryService } from "../baseDirectory.service"; -import { BaseDirectoryService } from "./baseDirectory.service"; import { IDirectoryService } from "./directory.service"; const DelayBetweenBuildGroupCallsInMilliseconds = 500; diff --git a/src/services/onelogin-directory.service.ts b/src/services/directory-services/onelogin-directory.service.ts similarity index 93% rename from src/services/onelogin-directory.service.ts rename to src/services/directory-services/onelogin-directory.service.ts index d46398c9..9b8eb2a2 100644 --- a/src/services/onelogin-directory.service.ts +++ b/src/services/directory-services/onelogin-directory.service.ts @@ -1,14 +1,14 @@ import { I18nService } from "@/jslib/common/src/abstractions/i18n.service"; import { LogService } from "@/jslib/common/src/abstractions/log.service"; -import { StateService } from "../abstractions/state.service"; -import { DirectoryType } from "../enums/directoryType"; -import { GroupEntry } from "../models/groupEntry"; -import { OneLoginConfiguration } from "../models/oneLoginConfiguration"; -import { SyncConfiguration } from "../models/syncConfiguration"; -import { UserEntry } from "../models/userEntry"; +import { StateService } from "../../abstractions/state.service"; +import { DirectoryType } from "../../enums/directoryType"; +import { GroupEntry } from "../../models/groupEntry"; +import { OneLoginConfiguration } from "../../models/oneLoginConfiguration"; +import { SyncConfiguration } from "../../models/syncConfiguration"; +import { UserEntry } from "../../models/userEntry"; +import { BaseDirectoryService } from "../baseDirectory.service"; -import { BaseDirectoryService } from "./baseDirectory.service"; import { IDirectoryService } from "./directory.service"; // Basic email validation: something@something.something diff --git a/src/services/sync.service.integration.spec.ts b/src/services/sync.service.integration.spec.ts index e6ff2780..241d5f0b 100644 --- a/src/services/sync.service.integration.spec.ts +++ b/src/services/sync.service.integration.spec.ts @@ -12,7 +12,7 @@ import { DirectoryFactoryService } from "../abstractions/directory-factory.servi import { DirectoryType } from "../enums/directoryType"; import { BatchRequestBuilder } from "./batch-request-builder"; -import { LdapDirectoryService } from "./ldap-directory.service"; +import { LdapDirectoryService } from "./directory-services/ldap-directory.service"; import { SingleRequestBuilder } from "./single-request-builder"; import { StateService } from "./state.service"; import { SyncService } from "./sync.service"; diff --git a/src/services/sync.service.spec.ts b/src/services/sync.service.spec.ts index 726e6240..46a111db 100644 --- a/src/services/sync.service.spec.ts +++ b/src/services/sync.service.spec.ts @@ -11,8 +11,8 @@ import { DirectoryFactoryService } from "../abstractions/directory-factory.servi import { DirectoryType } from "../enums/directoryType"; import { BatchRequestBuilder } from "./batch-request-builder"; +import { LdapDirectoryService } from "./directory-services/ldap-directory.service"; import { I18nService } from "./i18n.service"; -import { LdapDirectoryService } from "./ldap-directory.service"; import { SingleRequestBuilder } from "./single-request-builder"; import { StateService } from "./state.service"; import { SyncService } from "./sync.service";