From eeb37c5fd7055471d2273f97df865711d00e74ee Mon Sep 17 00:00:00 2001 From: Thomas Rittson Date: Sat, 1 Nov 2025 13:57:10 +1000 Subject: [PATCH 01/14] Move directory services into subdir --- src/abstractions/directory-factory.service.ts | 2 +- src/services/directory-factory.service.ts | 10 +++++----- .../directory.service.ts | 4 ++-- .../entra-id-directory.service.ts | 16 ++++++++-------- ...gsuite-directory.service.integration.spec.ts | 14 +++++++------- .../gsuite-directory.service.ts | 16 ++++++++-------- .../ldap-directory.service.integration.spec.ts | 17 ++++++++++------- .../ldap-directory.service.ts | 12 ++++++------ .../okta-directory.service.ts | 16 ++++++++-------- .../onelogin-directory.service.ts | 16 ++++++++-------- src/services/sync.service.integration.spec.ts | 2 +- src/services/sync.service.spec.ts | 2 +- 12 files changed, 65 insertions(+), 62 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 (84%) rename src/services/{ => directory-services}/gsuite-directory.service.ts (94%) 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/src/abstractions/directory-factory.service.ts b/src/abstractions/directory-factory.service.ts index 90d2f6743..2f775afce 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/services/directory-factory.service.ts b/src/services/directory-factory.service.ts index e3da3c043..eccea5c22 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 e13f87322..d7937e413 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 449e11b5c..d76f57a54 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 { BaseDirectoryService } from "./baseDirectory.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 { 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 84% rename from src/services/gsuite-directory.service.integration.spec.ts rename to src/services/directory-services/gsuite-directory.service.integration.spec.ts index c58de9575..7499490d7 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. diff --git a/src/services/gsuite-directory.service.ts b/src/services/directory-services/gsuite-directory.service.ts similarity index 94% rename from src/services/gsuite-directory.service.ts rename to src/services/directory-services/gsuite-directory.service.ts index 714818ebc..b5bd72125 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 { BaseDirectoryService } from "./baseDirectory.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 { BaseDirectoryService } from "../baseDirectory.service"; + import { IDirectoryService } from "./directory.service"; export class GSuiteDirectoryService extends BaseDirectoryService implements IDirectoryService { 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 c780f4c65..67d1be55d 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 b08f0a085..b9893885c 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 2c2d99130..c73fd49a0 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 { BaseDirectoryService } from "./baseDirectory.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 { 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 d46398c97..9b8eb2a2d 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 { BaseDirectoryService } from "./baseDirectory.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 { 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 e6ff27806..241d5f0b0 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 726e6240f..46a111db5 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"; From ebfe10110c97ae808f7ef66f841c8f02d2c8c303 Mon Sep 17 00:00:00 2001 From: Thomas Rittson Date: Sat, 1 Nov 2025 14:49:00 +1000 Subject: [PATCH 02/14] Update workflow to run Google tests --- .github/workflows/integration-test.yml | 64 ++++++++++++++++++++------ 1 file changed, 50 insertions(+), 14 deletions(-) diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index d08f6729d..e4d5c7ef0 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -2,22 +2,23 @@ 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 this stays consistent with the changed-files job. push: - branches: - - "main" 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 + - "src/services/directory-services/**" # any directory service + - "utils/**" # any change to test fixtures 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 + - "src/services/directory-services/**" # any directory service + - "utils/**" # any change to test fixtures permissions: contents: read checks: write # required by dorny/test-reporter to upload its results @@ -50,17 +51,52 @@ jobs: - name: Install Node dependencies run: npm ci - - name: Install mkcert + # Only run relevant tests depending on what files have changed. + # This should be kept consistent with the workflow level triggers. + - name: Get changed files + id: changed-files + uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 + with: + list-files: shell + token: ${{ secrets.GITHUB_TOKEN }} + filters: | + common: + - '.github/workflows/integration-test.yml' + - 'utils/**/*' + - 'package.json' + ldap: + - 'docker-compose.yml' + - 'src/services/**/ldap-directory.service*' + google: + - 'src/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 + if: steps.changed-files.outputs.common == 'true' || steps.changed-files.outputs.ldap == 'true' run: npx jest ldap-directory.service.integration.spec.ts --coverage + # Google Workspace + - name: Run Google Workspace integration tests + if: steps.changed-files.outputs.common == 'true' || steps.changed-files.outputs.google == 'true' + env: + GOOGLE_DOMAIN: ${{ vars.GOOGLE_DOMAIN }} + GOOGLE_ADMIN_USER: ${{ vars.GOOGLE_ADMIN_USER }} + GOOGLE_CLIENT_EMAIL: ${{ vars.GOOGLE_CLIENT_EMAIL }} + GOOGLE_PRIVATE_KEY: ${{ secrets.GOOGLE_PRIVATE_KEY }} + run: | + GOOGLE_DOMAIN="$GOOGLE_DOMAIN" \ + GOOGLE_ADMIN_USER="$GOOGLE_ADMIN_USER" \ + GOOGLE_CLIENT_EMAIL="$GOOGLE_CLIENT_EMAIL" \ + GOOGLE_PRIVATE_KEY="$GOOGLE_PRIVATE_KEY" \ + npx jest gsuite-directory.service.integration.spec.ts --coverage + - name: Report test results uses: dorny/test-reporter@dc3a92680fcc15842eef52e8c4606ea7ce6bd3f3 # v2.1.1 if: ${{ github.event.pull_request.head.repo.full_name == github.repository && !cancelled() }} From f79c7c9a2d1ef14d8c0e055f9af1f8c31305cc8b Mon Sep 17 00:00:00 2001 From: Thomas Rittson Date: Sat, 1 Nov 2025 14:50:55 +1000 Subject: [PATCH 03/14] Increase test timeouts --- .../gsuite-directory.service.integration.spec.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/services/directory-services/gsuite-directory.service.integration.spec.ts b/src/services/directory-services/gsuite-directory.service.integration.spec.ts index 7499490d7..3a2609de5 100644 --- a/src/services/directory-services/gsuite-directory.service.integration.spec.ts +++ b/src/services/directory-services/gsuite-directory.service.integration.spec.ts @@ -28,6 +28,10 @@ const INTEGRATION_USER_FILTER = "exclude:integration-user-a@bwrox.dev|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; From fc910b56498201a5bf245d891909cb9df851b616 Mon Sep 17 00:00:00 2001 From: Thomas Rittson Date: Sat, 1 Nov 2025 15:01:33 +1000 Subject: [PATCH 04/14] Remove now-deleted user from filter --- .../gsuite-directory.service.integration.spec.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/services/directory-services/gsuite-directory.service.integration.spec.ts b/src/services/directory-services/gsuite-directory.service.integration.spec.ts index 3a2609de5..397e594c9 100644 --- a/src/services/directory-services/gsuite-directory.service.integration.spec.ts +++ b/src/services/directory-services/gsuite-directory.service.integration.spec.ts @@ -24,8 +24,7 @@ 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! From 2abcc2bbf5950a97b252aefe07d8998f6b740958 Mon Sep 17 00:00:00 2001 From: Thomas Rittson Date: Tue, 4 Nov 2025 13:43:59 +1000 Subject: [PATCH 05/14] Address review feedback - more precise path filters - additional comments - only run coverage if tests actually ran - prevent coverage output from clashing --- .github/workflows/integration-test.yml | 31 +++++++++++++++----------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index e4d5c7ef0..3c65f196e 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -3,22 +3,26 @@ 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 this stays consistent with the changed-files job. + # This is done at the workflow level and at the job level. + # Make sure these triggers stay consistent with the 'changed-files' job. push: paths: - ".github/workflows/integration-test.yml" # this file - "docker-compose.yml" # any change to Docker configuration - "package.json" # dependencies - - "src/services/directory-services/**" # any directory service - "utils/**" # any change to test fixtures + - "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 - "docker-compose.yml" # any change to Docker configuration - "package.json" # dependencies - - "src/services/directory-services/**" # any directory service - "utils/**" # any change to test fixtures + - "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 @@ -53,12 +57,14 @@ jobs: # 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' @@ -66,9 +72,9 @@ jobs: - 'package.json' ldap: - 'docker-compose.yml' - - 'src/services/**/ldap-directory.service*' + - 'src/services/directory-services/ldap-directory.service*' google: - - 'src/services/**/gsuite-directory.service*' + - 'src/services/directory-services/gsuite-directory.service*' # LDAP - name: Setup LDAP integration tests @@ -80,7 +86,7 @@ jobs: - name: Run LDAP integration tests if: steps.changed-files.outputs.common == 'true' || steps.changed-files.outputs.ldap == 'true' - run: npx jest ldap-directory.service.integration.spec.ts --coverage + run: npx jest ldap-directory.service.integration.spec.ts --coverage --coverageDirectory=coverage-ldap # Google Workspace - name: Run Google Workspace integration tests @@ -91,15 +97,12 @@ jobs: GOOGLE_CLIENT_EMAIL: ${{ vars.GOOGLE_CLIENT_EMAIL }} GOOGLE_PRIVATE_KEY: ${{ secrets.GOOGLE_PRIVATE_KEY }} run: | - GOOGLE_DOMAIN="$GOOGLE_DOMAIN" \ - GOOGLE_ADMIN_USER="$GOOGLE_ADMIN_USER" \ - GOOGLE_CLIENT_EMAIL="$GOOGLE_CLIENT_EMAIL" \ - GOOGLE_PRIVATE_KEY="$GOOGLE_PRIVATE_KEY" \ - npx jest gsuite-directory.service.integration.spec.ts --coverage + 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() && steps.changed-files.outputs.changes != '[]' with: name: Test Results path: "junit.xml" @@ -107,7 +110,9 @@ jobs: fail-on-error: true - name: Upload coverage to codecov.io + if: steps.report.conclusion == 'success' uses: codecov/codecov-action@5a605bd92782ce0810fa3b8acc235c921b497052 # v5.2.0 - name: Upload results to codecov.io + if: steps.report.conclusion == 'success' uses: codecov/test-results-action@4e79e65778be1cecd5df25e14af1eafb6df80ea9 # v1.0.2 From 3f95408f8f8078e6cc2c04c03bf6c5415066c894 Mon Sep 17 00:00:00 2001 From: Thomas Rittson Date: Tue, 4 Nov 2025 13:52:35 +1000 Subject: [PATCH 06/14] Added SyncService as a trigger --- .github/workflows/integration-test.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index 3c65f196e..25bebffd1 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -11,6 +11,7 @@ on: - "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 @@ -20,6 +21,7 @@ on: - "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 @@ -70,6 +72,7 @@ jobs: - '.github/workflows/integration-test.yml' - 'utils/**/*' - 'package.json' + - 'src/services/sync.service.ts' ldap: - 'docker-compose.yml' - 'src/services/directory-services/ldap-directory.service*' From 3d1f592869871b280ddedded80d358deccba7ccb Mon Sep 17 00:00:00 2001 From: Thomas Rittson Date: Tue, 4 Nov 2025 14:08:58 +1000 Subject: [PATCH 07/14] Fix junit output overwriting itself --- .github/workflows/integration-test.yml | 5 ++++- .gitignore | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index 25bebffd1..62e36356f 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -89,6 +89,8 @@ jobs: - name: Run LDAP integration tests 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 @@ -99,6 +101,7 @@ jobs: GOOGLE_ADMIN_USER: ${{ vars.GOOGLE_ADMIN_USER }} GOOGLE_CLIENT_EMAIL: ${{ vars.GOOGLE_CLIENT_EMAIL }} GOOGLE_PRIVATE_KEY: ${{ secrets.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 @@ -108,7 +111,7 @@ jobs: if: github.event.pull_request.head.repo.full_name == github.repository && !cancelled() && steps.changed-files.outputs.changes != '[]' with: name: Test Results - path: "junit.xml" + path: "junit.xml*" reporter: jest-junit fail-on-error: true diff --git a/.gitignore b/.gitignore index 69cc709de..295b79280 100644 --- a/.gitignore +++ b/.gitignore @@ -33,8 +33,8 @@ build-cli .angular/cache # Testing -coverage -junit.xml +coverage* +junit.xml* # Misc *.crx From 944cddc22f00bb469975b9fdb37e48249c2ebf35 Mon Sep 17 00:00:00 2001 From: Thomas Rittson Date: Tue, 4 Nov 2025 14:20:36 +1000 Subject: [PATCH 08/14] Make glob pattern consistent --- .github/workflows/integration-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index 62e36356f..bd4f97c29 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -70,7 +70,7 @@ jobs: filters: | common: - '.github/workflows/integration-test.yml' - - 'utils/**/*' + - 'utils/**' - 'package.json' - 'src/services/sync.service.ts' ldap: From e3d7c776be8b0a6548d86a398006507a03a8aa20 Mon Sep 17 00:00:00 2001 From: Thomas Rittson Date: Tue, 4 Nov 2025 14:20:55 +1000 Subject: [PATCH 09/14] simplify conditionals --- .github/workflows/integration-test.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index bd4f97c29..160ef7a37 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -108,7 +108,7 @@ jobs: - 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() && steps.changed-files.outputs.changes != '[]' + if: github.event.pull_request.head.repo.full_name == github.repository && !cancelled() with: name: Test Results path: "junit.xml*" @@ -116,9 +116,7 @@ jobs: fail-on-error: true - name: Upload coverage to codecov.io - if: steps.report.conclusion == 'success' uses: codecov/codecov-action@5a605bd92782ce0810fa3b8acc235c921b497052 # v5.2.0 - name: Upload results to codecov.io - if: steps.report.conclusion == 'success' uses: codecov/test-results-action@4e79e65778be1cecd5df25e14af1eafb6df80ea9 # v1.0.2 From c5d83b424de5ca4cd333b21029a963c2104f4c1a Mon Sep 17 00:00:00 2001 From: Thomas Rittson Date: Tue, 4 Nov 2025 14:21:08 +1000 Subject: [PATCH 10/14] sanitize googleapis errors --- src/locales/en/messages.json | 3 +++ .../directory-services/gsuite-directory.service.ts | 8 +++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/locales/en/messages.json b/src/locales/en/messages.json index b8859b1b6..272991f37 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-services/gsuite-directory.service.ts b/src/services/directory-services/gsuite-directory.service.ts index b5bd72125..0988279e3 100644 --- a/src/services/directory-services/gsuite-directory.service.ts +++ b/src/services/directory-services/gsuite-directory.service.ts @@ -253,7 +253,13 @@ export class GSuiteDirectoryService extends BaseDirectoryService implements IDir ], }); - await this.client.authorize(); + try { + await this.client.authorize(); + } catch { + // Catch and rethrow this to sanitize any sensitive info (e.g. private key) in the error message + this.logService.error("Google Workspace authentication failed"); + throw new Error(this.i18nService.t("authenticationFailed")); + } this.authParams = { auth: this.client, From 37ce3d15516f886bd72125f49eda5c7a6250ba73 Mon Sep 17 00:00:00 2001 From: Thomas Rittson Date: Tue, 4 Nov 2025 14:27:35 +1000 Subject: [PATCH 11/14] prevent tests running twice in CI --- .github/workflows/integration-test.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index 160ef7a37..9371f18e3 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -6,6 +6,9 @@ on: # 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' + - 'rc' paths: - ".github/workflows/integration-test.yml" # this file - "docker-compose.yml" # any change to Docker configuration From 5a17f3a6bd9896ccbbe07a24fb602c65b4db0374 Mon Sep 17 00:00:00 2001 From: Thomas Rittson Date: Sun, 9 Nov 2025 08:47:32 +1000 Subject: [PATCH 12/14] Use Azure Key Vault --- .github/workflows/integration-test.yml | 27 ++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index 9371f18e3..b456cb76c 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -31,6 +31,7 @@ on: 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 @@ -60,6 +61,24 @@ jobs: - name: Install Node dependencies run: npm ci + # 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 @@ -100,10 +119,10 @@ jobs: - name: Run Google Workspace integration tests if: steps.changed-files.outputs.common == 'true' || steps.changed-files.outputs.google == 'true' env: - GOOGLE_DOMAIN: ${{ vars.GOOGLE_DOMAIN }} - GOOGLE_ADMIN_USER: ${{ vars.GOOGLE_ADMIN_USER }} - GOOGLE_CLIENT_EMAIL: ${{ vars.GOOGLE_CLIENT_EMAIL }} - GOOGLE_PRIVATE_KEY: ${{ secrets.GOOGLE_PRIVATE_KEY }} + GOOGLE_DOMAIN: ${{ steps.get-kv-secrets.output.GOOGLE-DOMAIN }} + GOOGLE_ADMIN_USER: ${{ steps.get-kv-secrets.output.GOOGLE-ADMIN-USER }} + GOOGLE_CLIENT_EMAIL: ${{ steps.get-kv-secrets.output.GOOGLE-CLIENT-EMAIL }} + GOOGLE_PRIVATE_KEY: ${{ steps.get-kv-secrets.output.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 From 3003a7c9a82b398b40e83d8a4fff9fc4f4f4c5ab Mon Sep 17 00:00:00 2001 From: Thomas Rittson Date: Sun, 9 Nov 2025 09:05:23 +1000 Subject: [PATCH 13/14] Fix typo --- .github/workflows/integration-test.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index b456cb76c..05dc69ab3 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -119,10 +119,10 @@ jobs: - 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.output.GOOGLE-DOMAIN }} - GOOGLE_ADMIN_USER: ${{ steps.get-kv-secrets.output.GOOGLE-ADMIN-USER }} - GOOGLE_CLIENT_EMAIL: ${{ steps.get-kv-secrets.output.GOOGLE-CLIENT-EMAIL }} - GOOGLE_PRIVATE_KEY: ${{ steps.get-kv-secrets.output.GOOGLE-PRIVATE-KEY }} + 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 From 7501ae7ee713193bdd22d67fd09b2447d666cb1f Mon Sep 17 00:00:00 2001 From: Thomas Rittson Date: Sun, 9 Nov 2025 09:17:36 +1000 Subject: [PATCH 14/14] Preserve error context in logService --- src/services/directory-services/gsuite-directory.service.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/services/directory-services/gsuite-directory.service.ts b/src/services/directory-services/gsuite-directory.service.ts index 0988279e3..95714fb4f 100644 --- a/src/services/directory-services/gsuite-directory.service.ts +++ b/src/services/directory-services/gsuite-directory.service.ts @@ -255,9 +255,11 @@ export class GSuiteDirectoryService extends BaseDirectoryService implements IDir try { await this.client.authorize(); - } catch { + } 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"); + this.logService.error( + `Google Workspace authentication failed: ${error?.name || "Unknown error"}`, + ); throw new Error(this.i18nService.t("authenticationFailed")); }