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

[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
This commit is contained in:
Thomas Rittson
2025-11-12 06:03:37 +10:00
committed by GitHub
parent ab436551de
commit a44eb28be8
15 changed files with 163 additions and 79 deletions

View File

@@ -2,25 +2,36 @@ name: Integration Testing
on: on:
workflow_dispatch: 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: push:
branches: branches:
- "main" - 'main'
- 'rc'
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 - "docker-compose.yml" # any change to Docker configuration
- "./utils/**/*" # any change to test fixtures - "package.json" # dependencies
- "./docker-compose.yml" # any change to Docker configuration - "utils/**" # any change to test fixtures
- "./package.json" # dependencies - "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: 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 - "docker-compose.yml" # any change to Docker configuration
- "./utils/**/*" # any change to test fixtures - "package.json" # dependencies
- "./docker-compose.yml" # any change to Docker configuration - "utils/**" # any change to test fixtures
- "./package.json" # dependencies - "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: permissions:
contents: read contents: read
checks: write # required by dorny/test-reporter to upload its results 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: jobs:
testing: testing:
name: Run tests name: Run tests
@@ -50,23 +61,79 @@ jobs:
- name: Install Node dependencies - name: Install Node dependencies
run: npm ci 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: | run: |
sudo apt-get update sudo apt-get update
sudo apt-get -y install mkcert sudo apt-get -y install mkcert
npm run test:integration:setup
- name: Setup LDAP integration tests
run: npm run test:integration:setup
- name: Run LDAP integration tests - 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 - name: Report test results
id: report
uses: dorny/test-reporter@dc3a92680fcc15842eef52e8c4606ea7ce6bd3f3 # v2.1.1 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: with:
name: Test Results name: Test Results
path: "junit.xml" path: "junit.xml*"
reporter: jest-junit reporter: jest-junit
fail-on-error: true fail-on-error: true

4
.gitignore vendored
View File

@@ -33,8 +33,8 @@ build-cli
.angular/cache .angular/cache
# Testing # Testing
coverage coverage*
junit.xml junit.xml*
# Misc # Misc
*.crx *.crx

View File

@@ -1,5 +1,5 @@
import { DirectoryType } from "@/src/enums/directoryType"; 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 { export abstract class DirectoryFactoryService {
abstract createService(type: DirectoryType): IDirectoryService; abstract createService(type: DirectoryType): IDirectoryService;

View File

@@ -768,5 +768,8 @@
}, },
"launchWebVault": { "launchWebVault": {
"message": "Launch Web Vault" "message": "Launch Web Vault"
},
"authenticationFailed": {
"message": "Authentication failed"
} }
} }

View File

@@ -5,11 +5,11 @@ import { DirectoryFactoryService } from "../abstractions/directory-factory.servi
import { StateService } from "../abstractions/state.service"; import { StateService } from "../abstractions/state.service";
import { DirectoryType } from "../enums/directoryType"; import { DirectoryType } from "../enums/directoryType";
import { EntraIdDirectoryService } from "./entra-id-directory.service"; import { EntraIdDirectoryService } from "./directory-services/entra-id-directory.service";
import { GSuiteDirectoryService } from "./gsuite-directory.service"; import { GSuiteDirectoryService } from "./directory-services/gsuite-directory.service";
import { LdapDirectoryService } from "./ldap-directory.service"; import { LdapDirectoryService } from "./directory-services/ldap-directory.service";
import { OktaDirectoryService } from "./okta-directory.service"; import { OktaDirectoryService } from "./directory-services/okta-directory.service";
import { OneLoginDirectoryService } from "./onelogin-directory.service"; import { OneLoginDirectoryService } from "./directory-services/onelogin-directory.service";
export class DefaultDirectoryFactoryService implements DirectoryFactoryService { export class DefaultDirectoryFactoryService implements DirectoryFactoryService {
constructor( constructor(

View File

@@ -1,5 +1,5 @@
import { GroupEntry } from "../models/groupEntry"; import { GroupEntry } from "../../models/groupEntry";
import { UserEntry } from "../models/userEntry"; import { UserEntry } from "../../models/userEntry";
export interface IDirectoryService { export interface IDirectoryService {
getEntries(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]>; getEntries(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]>;

View File

@@ -7,14 +7,14 @@ import * as graphType from "@microsoft/microsoft-graph-types";
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 { StateService } from "../abstractions/state.service"; import { StateService } from "../../abstractions/state.service";
import { DirectoryType } from "../enums/directoryType"; import { DirectoryType } from "../../enums/directoryType";
import { EntraIdConfiguration } from "../models/entraIdConfiguration"; import { EntraIdConfiguration } from "../../models/entraIdConfiguration";
import { GroupEntry } from "../models/groupEntry"; import { GroupEntry } from "../../models/groupEntry";
import { SyncConfiguration } from "../models/syncConfiguration"; import { SyncConfiguration } from "../../models/syncConfiguration";
import { UserEntry } from "../models/userEntry"; import { UserEntry } from "../../models/userEntry";
import { BaseDirectoryService } from "../baseDirectory.service";
import { BaseDirectoryService } from "./baseDirectory.service";
import { IDirectoryService } from "./directory.service"; import { IDirectoryService } from "./directory.service";
const EntraIdPublicIdentityAuthority = "login.microsoftonline.com"; const EntraIdPublicIdentityAuthority = "login.microsoftonline.com";

View File

@@ -1,18 +1,18 @@
import { config as dotenvConfig } from "dotenv"; import { config as dotenvConfig } from "dotenv";
import { mock, MockProxy } from "jest-mock-extended"; 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 { import {
getGSuiteConfiguration, getGSuiteConfiguration,
getSyncConfiguration, getSyncConfiguration,
} from "../../utils/google-workspace/config-fixtures"; } from "../../../utils/google-workspace/config-fixtures";
import { groupFixtures } from "../../utils/google-workspace/group-fixtures"; import { groupFixtures } from "../../../utils/google-workspace/group-fixtures";
import { userFixtures } from "../../utils/google-workspace/user-fixtures"; import { userFixtures } from "../../../utils/google-workspace/user-fixtures";
import { DirectoryType } from "../enums/directoryType"; import { DirectoryType } from "../../enums/directoryType";
import { StateService } from "../state.service";
import { GSuiteDirectoryService } from "./gsuite-directory.service"; import { GSuiteDirectoryService } from "./gsuite-directory.service";
import { StateService } from "./state.service";
// These tests integrate with a test Google Workspace instance. // These tests integrate with a test Google Workspace instance.
// Credentials are located in the shared Bitwarden collection for Directory Connector testing. // 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 filters target integration test data.
// These should return data that matches the user and group fixtures exactly. // These should return data that matches the user and group fixtures exactly.
// There may be additional data present if not used. // There may be additional data present if not used.
const INTEGRATION_USER_FILTER = const INTEGRATION_USER_FILTER = "|orgUnitPath='/Integration testing'";
"exclude:integration-user-a@bwrox.dev|orgUnitPath='/Integration testing'";
const INTEGRATION_GROUP_FILTER = "|name:Integration*"; const INTEGRATION_GROUP_FILTER = "|name:Integration*";
// These tests are slow!
// Increase the default timeout from 5s to 15s
jest.setTimeout(15000);
describe("gsuiteDirectoryService", () => { describe("gsuiteDirectoryService", () => {
let logService: MockProxy<LogService>; let logService: MockProxy<LogService>;
let i18nService: MockProxy<I18nService>; let i18nService: MockProxy<I18nService>;

View File

@@ -4,14 +4,14 @@ import { admin_directory_v1, google } from "googleapis";
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 { StateService } from "../abstractions/state.service"; import { StateService } from "../../abstractions/state.service";
import { DirectoryType } from "../enums/directoryType"; import { DirectoryType } from "../../enums/directoryType";
import { GroupEntry } from "../models/groupEntry"; import { GroupEntry } from "../../models/groupEntry";
import { GSuiteConfiguration } from "../models/gsuiteConfiguration"; import { GSuiteConfiguration } from "../../models/gsuiteConfiguration";
import { SyncConfiguration } from "../models/syncConfiguration"; import { SyncConfiguration } from "../../models/syncConfiguration";
import { UserEntry } from "../models/userEntry"; import { UserEntry } from "../../models/userEntry";
import { BaseDirectoryService } from "../baseDirectory.service";
import { BaseDirectoryService } from "./baseDirectory.service";
import { IDirectoryService } from "./directory.service"; import { IDirectoryService } from "./directory.service";
export class GSuiteDirectoryService extends BaseDirectoryService implements IDirectoryService { 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 = { this.authParams = {
auth: this.client, auth: this.client,

View File

@@ -1,14 +1,17 @@
import { mock, MockProxy } from "jest-mock-extended"; 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 { getLdapConfiguration, getSyncConfiguration } from "../../utils/openldap/config-fixtures"; import {
import { groupFixtures } from "../../utils/openldap/group-fixtures"; getLdapConfiguration,
import { userFixtures } from "../../utils/openldap/user-fixtures"; getSyncConfiguration,
import { DirectoryType } from "../enums/directoryType"; } 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 { 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. // These tests integrate with the OpenLDAP docker image and seed data located in the openldap folder.
// To run theses tests: // To run theses tests:

View File

@@ -7,12 +7,12 @@ 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 { Utils } from "@/jslib/common/src/misc/utils"; import { Utils } from "@/jslib/common/src/misc/utils";
import { StateService } from "../abstractions/state.service"; import { StateService } from "../../abstractions/state.service";
import { DirectoryType } from "../enums/directoryType"; import { DirectoryType } from "../../enums/directoryType";
import { GroupEntry } from "../models/groupEntry"; import { GroupEntry } from "../../models/groupEntry";
import { LdapConfiguration } from "../models/ldapConfiguration"; import { LdapConfiguration } from "../../models/ldapConfiguration";
import { SyncConfiguration } from "../models/syncConfiguration"; import { SyncConfiguration } from "../../models/syncConfiguration";
import { UserEntry } from "../models/userEntry"; import { UserEntry } from "../../models/userEntry";
import { IDirectoryService } from "./directory.service"; import { IDirectoryService } from "./directory.service";

View File

@@ -3,14 +3,14 @@ import * as https from "https";
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 { StateService } from "../abstractions/state.service"; import { StateService } from "../../abstractions/state.service";
import { DirectoryType } from "../enums/directoryType"; import { DirectoryType } from "../../enums/directoryType";
import { GroupEntry } from "../models/groupEntry"; import { GroupEntry } from "../../models/groupEntry";
import { OktaConfiguration } from "../models/oktaConfiguration"; import { OktaConfiguration } from "../../models/oktaConfiguration";
import { SyncConfiguration } from "../models/syncConfiguration"; import { SyncConfiguration } from "../../models/syncConfiguration";
import { UserEntry } from "../models/userEntry"; import { UserEntry } from "../../models/userEntry";
import { BaseDirectoryService } from "../baseDirectory.service";
import { BaseDirectoryService } from "./baseDirectory.service";
import { IDirectoryService } from "./directory.service"; import { IDirectoryService } from "./directory.service";
const DelayBetweenBuildGroupCallsInMilliseconds = 500; const DelayBetweenBuildGroupCallsInMilliseconds = 500;

View File

@@ -1,14 +1,14 @@
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 { StateService } from "../abstractions/state.service"; import { StateService } from "../../abstractions/state.service";
import { DirectoryType } from "../enums/directoryType"; import { DirectoryType } from "../../enums/directoryType";
import { GroupEntry } from "../models/groupEntry"; import { GroupEntry } from "../../models/groupEntry";
import { OneLoginConfiguration } from "../models/oneLoginConfiguration"; import { OneLoginConfiguration } from "../../models/oneLoginConfiguration";
import { SyncConfiguration } from "../models/syncConfiguration"; import { SyncConfiguration } from "../../models/syncConfiguration";
import { UserEntry } from "../models/userEntry"; import { UserEntry } from "../../models/userEntry";
import { BaseDirectoryService } from "../baseDirectory.service";
import { BaseDirectoryService } from "./baseDirectory.service";
import { IDirectoryService } from "./directory.service"; import { IDirectoryService } from "./directory.service";
// Basic email validation: something@something.something // Basic email validation: something@something.something

View File

@@ -12,7 +12,7 @@ import { DirectoryFactoryService } from "../abstractions/directory-factory.servi
import { DirectoryType } from "../enums/directoryType"; import { DirectoryType } from "../enums/directoryType";
import { BatchRequestBuilder } from "./batch-request-builder"; 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 { SingleRequestBuilder } from "./single-request-builder";
import { StateService } from "./state.service"; import { StateService } from "./state.service";
import { SyncService } from "./sync.service"; import { SyncService } from "./sync.service";

View File

@@ -11,8 +11,8 @@ import { DirectoryFactoryService } from "../abstractions/directory-factory.servi
import { DirectoryType } from "../enums/directoryType"; import { DirectoryType } from "../enums/directoryType";
import { BatchRequestBuilder } from "./batch-request-builder"; import { BatchRequestBuilder } from "./batch-request-builder";
import { LdapDirectoryService } from "./directory-services/ldap-directory.service";
import { I18nService } from "./i18n.service"; import { I18nService } from "./i18n.service";
import { LdapDirectoryService } from "./ldap-directory.service";
import { SingleRequestBuilder } from "./single-request-builder"; import { SingleRequestBuilder } from "./single-request-builder";
import { StateService } from "./state.service"; import { StateService } from "./state.service";
import { SyncService } from "./sync.service"; import { SyncService } from "./sync.service";