From 13a8b46d3071e069238d1981629b42ebc2ba9d94 Mon Sep 17 00:00:00 2001 From: Vijay Oommen Date: Thu, 31 Jul 2025 11:45:35 -0500 Subject: [PATCH] [PM-23826] Crowdstrike integration dialog (#15757) --- .../integrations/integrations.component.ts | 381 ++++++++++-------- .../integration-card.component.html | 2 +- .../integration-card.component.spec.ts | 35 +- .../integration-card.component.ts | 78 +++- .../integration-card.stories.ts | 58 --- .../connect-dialog-hec.component.html | 38 ++ .../connect-dialog-hec.component.spec.ts | 176 ++++++++ .../connect-dialog-hec.component.ts | 81 ++++ .../integrations/integration-dialog/index.ts | 1 + .../integration-grid.component.html | 1 + .../integration-grid.component.spec.ts | 26 ++ .../integration-grid.stories.ts | 70 ---- .../shared/components/integrations/models.ts | 1 + apps/web/src/app/core/core.module.ts | 7 + apps/web/src/locales/en/messages.json | 12 + .../bit-common/src/dirt/integrations/index.ts | 5 + .../organization-integration-response.ts | 6 +- ...ganization-integration-api.service.spec.ts | 22 +- .../organization-integration-api.service.ts | 8 +- .../integrations.component.spec.ts | 30 +- 20 files changed, 678 insertions(+), 360 deletions(-) delete mode 100644 apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-card/integration-card.stories.ts create mode 100644 apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-dialog/connect-dialog/connect-dialog-hec.component.html create mode 100644 apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-dialog/connect-dialog/connect-dialog-hec.component.spec.ts create mode 100644 apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-dialog/connect-dialog/connect-dialog-hec.component.ts create mode 100644 apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-dialog/index.ts delete mode 100644 apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-grid/integration-grid.stories.ts diff --git a/apps/web/src/app/admin-console/organizations/integrations/integrations.component.ts b/apps/web/src/app/admin-console/organizations/integrations/integrations.component.ts index c0a57c82954..3ddf9c0a720 100644 --- a/apps/web/src/app/admin-console/organizations/integrations/integrations.component.ts +++ b/apps/web/src/app/admin-console/organizations/integrations/integrations.component.ts @@ -2,8 +2,10 @@ // @ts-strict-ignore import { Component, OnDestroy, OnInit } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; -import { Observable, Subject, switchMap, takeUntil } from "rxjs"; +import { Observable, Subject, switchMap, takeUntil, scheduled, asyncScheduler } from "rxjs"; +// eslint-disable-next-line no-restricted-imports +import { OrganizationIntegrationApiService } from "@bitwarden/bit-common/dirt/integrations"; import { getOrganizationById, OrganizationService, @@ -33,13 +35,192 @@ import { Integration } from "../shared/components/integrations/models"; ], }) export class AdminConsoleIntegrationsComponent implements OnInit, OnDestroy { - integrationsList: Integration[] = []; + // integrationsList: Integration[] = []; tabIndex: number; organization$: Observable; isEventBasedIntegrationsEnabled: boolean = false; private destroy$ = new Subject(); + // initialize the integrations list with default integrations + integrationsList: Integration[] = [ + { + name: "AD FS", + linkURL: "https://bitwarden.com/help/saml-adfs/", + image: "../../../../../../../images/integrations/azure-active-directory.svg", + type: IntegrationType.SSO, + }, + { + name: "Auth0", + linkURL: "https://bitwarden.com/help/saml-auth0/", + image: "../../../../../../../images/integrations/logo-auth0-badge-color.svg", + type: IntegrationType.SSO, + }, + { + name: "AWS", + linkURL: "https://bitwarden.com/help/saml-aws/", + image: "../../../../../../../images/integrations/aws-color.svg", + imageDarkMode: "../../../../../../../images/integrations/aws-darkmode.svg", + type: IntegrationType.SSO, + }, + { + name: "Microsoft Entra ID", + linkURL: "https://bitwarden.com/help/saml-azure/", + image: "../../../../../../../images/integrations/logo-microsoft-entra-id-color.svg", + type: IntegrationType.SSO, + }, + { + name: "Duo", + linkURL: "https://bitwarden.com/help/saml-duo/", + image: "../../../../../../../images/integrations/logo-duo-color.svg", + type: IntegrationType.SSO, + }, + { + name: "Google", + linkURL: "https://bitwarden.com/help/saml-google/", + image: "../../../../../../../images/integrations/logo-google-badge-color.svg", + type: IntegrationType.SSO, + }, + { + name: "JumpCloud", + linkURL: "https://bitwarden.com/help/saml-jumpcloud/", + image: "../../../../../../../images/integrations/logo-jumpcloud-badge-color.svg", + imageDarkMode: "../../../../../../../images/integrations/jumpcloud-darkmode.svg", + type: IntegrationType.SSO, + }, + { + name: "KeyCloak", + linkURL: "https://bitwarden.com/help/saml-keycloak/", + image: "../../../../../../../images/integrations/logo-keycloak-icon.svg", + type: IntegrationType.SSO, + }, + { + name: "Okta", + linkURL: "https://bitwarden.com/help/saml-okta/", + image: "../../../../../../../images/integrations/logo-okta-symbol-black.svg", + imageDarkMode: "../../../../../../../images/integrations/okta-darkmode.svg", + type: IntegrationType.SSO, + }, + { + name: "OneLogin", + linkURL: "https://bitwarden.com/help/saml-onelogin/", + image: "../../../../../../../images/integrations/logo-onelogin-badge-color.svg", + imageDarkMode: "../../../../../../../images/integrations/onelogin-darkmode.svg", + type: IntegrationType.SSO, + }, + { + name: "PingFederate", + linkURL: "https://bitwarden.com/help/saml-pingfederate/", + image: "../../../../../../../images/integrations/logo-ping-identity-badge-color.svg", + type: IntegrationType.SSO, + }, + { + name: "Microsoft Entra ID", + linkURL: "https://bitwarden.com/help/microsoft-entra-id-scim-integration/", + image: "../../../../../../../images/integrations/logo-microsoft-entra-id-color.svg", + type: IntegrationType.SCIM, + }, + { + name: "Okta", + linkURL: "https://bitwarden.com/help/okta-scim-integration/", + image: "../../../../../../../images/integrations/logo-okta-symbol-black.svg", + imageDarkMode: "../../../../../../../images/integrations/okta-darkmode.svg", + type: IntegrationType.SCIM, + }, + { + name: "OneLogin", + linkURL: "https://bitwarden.com/help/onelogin-scim-integration/", + image: "../../../../../../../images/integrations/logo-onelogin-badge-color.svg", + imageDarkMode: "../../../../../../../images/integrations/onelogin-darkmode.svg", + type: IntegrationType.SCIM, + }, + { + name: "JumpCloud", + linkURL: "https://bitwarden.com/help/jumpcloud-scim-integration/", + image: "../../../../../../../images/integrations/logo-jumpcloud-badge-color.svg", + imageDarkMode: "../../../../../../../images/integrations/jumpcloud-darkmode.svg", + type: IntegrationType.SCIM, + }, + { + name: "Ping Identity", + linkURL: "https://bitwarden.com/help/ping-identity-scim-integration/", + image: "../../../../../../../images/integrations/logo-ping-identity-badge-color.svg", + type: IntegrationType.SCIM, + }, + { + name: "Active Directory", + linkURL: "https://bitwarden.com/help/ldap-directory/", + image: "../../../../../../../images/integrations/azure-active-directory.svg", + type: IntegrationType.BWDC, + }, + { + name: "Microsoft Entra ID", + linkURL: "https://bitwarden.com/help/microsoft-entra-id/", + image: "../../../../../../../images/integrations/logo-microsoft-entra-id-color.svg", + type: IntegrationType.BWDC, + }, + { + name: "Google Workspace", + linkURL: "https://bitwarden.com/help/workspace-directory/", + image: "../../../../../../../images/integrations/logo-google-badge-color.svg", + type: IntegrationType.BWDC, + }, + { + name: "Okta", + linkURL: "https://bitwarden.com/help/okta-directory/", + image: "../../../../../../../images/integrations/logo-okta-symbol-black.svg", + imageDarkMode: "../../../../../../../images/integrations/okta-darkmode.svg", + type: IntegrationType.BWDC, + }, + { + name: "OneLogin", + linkURL: "https://bitwarden.com/help/onelogin-directory/", + image: "../../../../../../../images/integrations/logo-onelogin-badge-color.svg", + imageDarkMode: "../../../../../../../images/integrations/onelogin-darkmode.svg", + type: IntegrationType.BWDC, + }, + { + name: "Splunk", + linkURL: "https://bitwarden.com/help/splunk-siem/", + image: "../../../../../../../images/integrations/logo-splunk-black.svg", + imageDarkMode: "../../../../../../../images/integrations/splunk-darkmode.svg", + type: IntegrationType.EVENT, + }, + { + name: "Microsoft Sentinel", + linkURL: "https://bitwarden.com/help/microsoft-sentinel-siem/", + image: "../../../../../../../images/integrations/logo-microsoft-sentinel-color.svg", + type: IntegrationType.EVENT, + }, + { + name: "Rapid7", + linkURL: "https://bitwarden.com/help/rapid7-siem/", + image: "../../../../../../../images/integrations/logo-rapid7-black.svg", + imageDarkMode: "../../../../../../../images/integrations/rapid7-darkmode.svg", + type: IntegrationType.EVENT, + }, + { + name: "Elastic", + linkURL: "https://bitwarden.com/help/elastic-siem/", + image: "../../../../../../../images/integrations/logo-elastic-badge-color.svg", + type: IntegrationType.EVENT, + }, + { + name: "Panther", + linkURL: "https://bitwarden.com/help/panther-siem/", + image: "../../../../../../../images/integrations/logo-panther-round-color.svg", + type: IntegrationType.EVENT, + }, + { + name: "Microsoft Intune", + linkURL: "https://bitwarden.com/help/deploy-browser-extensions-with-intune/", + image: "../../../../../../../images/integrations/logo-microsoft-intune-color.svg", + type: IntegrationType.DEVICE, + }, + ]; + ngOnInit(): void { + const orgId = this.route.snapshot.params.organizationId; + this.organization$ = this.route.params.pipe( switchMap((params) => this.accountService.activeAccount$.pipe( @@ -51,6 +232,25 @@ export class AdminConsoleIntegrationsComponent implements OnInit, OnDestroy { ), ), ); + + scheduled(this.orgIntegrationApiService.getOrganizationIntegrations(orgId), asyncScheduler) + .pipe(takeUntil(this.destroy$)) + .subscribe((integrations) => { + // Update the integrations list with the fetched integrations + if (integrations && integrations.length > 0) { + integrations.forEach((integration) => { + const configJson = JSON.parse(integration.configuration || "{}"); + const serviceName = configJson.service ?? ""; + const existingIntegration = this.integrationsList.find((i) => i.name === serviceName); + + if (existingIntegration) { + // if a configuration exists, then it is connected + existingIntegration.isConnected = !!integration.configuration; + existingIntegration.configuration = integration.configuration || ""; + } + }); + } + }); } constructor( @@ -58,6 +258,7 @@ export class AdminConsoleIntegrationsComponent implements OnInit, OnDestroy { private organizationService: OrganizationService, private accountService: AccountService, private configService: ConfigService, + private orgIntegrationApiService: OrganizationIntegrationApiService, ) { this.configService .getFeatureFlag$(FeatureFlag.EventBasedOrganizationIntegrations) @@ -66,182 +267,6 @@ export class AdminConsoleIntegrationsComponent implements OnInit, OnDestroy { this.isEventBasedIntegrationsEnabled = isEnabled; }); - this.integrationsList = [ - { - name: "AD FS", - linkURL: "https://bitwarden.com/help/saml-adfs/", - image: "../../../../../../../images/integrations/azure-active-directory.svg", - type: IntegrationType.SSO, - }, - { - name: "Auth0", - linkURL: "https://bitwarden.com/help/saml-auth0/", - image: "../../../../../../../images/integrations/logo-auth0-badge-color.svg", - type: IntegrationType.SSO, - }, - { - name: "AWS", - linkURL: "https://bitwarden.com/help/saml-aws/", - image: "../../../../../../../images/integrations/aws-color.svg", - imageDarkMode: "../../../../../../../images/integrations/aws-darkmode.svg", - type: IntegrationType.SSO, - }, - { - name: "Microsoft Entra ID", - linkURL: "https://bitwarden.com/help/saml-azure/", - image: "../../../../../../../images/integrations/logo-microsoft-entra-id-color.svg", - type: IntegrationType.SSO, - }, - { - name: "Duo", - linkURL: "https://bitwarden.com/help/saml-duo/", - image: "../../../../../../../images/integrations/logo-duo-color.svg", - type: IntegrationType.SSO, - }, - { - name: "Google", - linkURL: "https://bitwarden.com/help/saml-google/", - image: "../../../../../../../images/integrations/logo-google-badge-color.svg", - type: IntegrationType.SSO, - }, - { - name: "JumpCloud", - linkURL: "https://bitwarden.com/help/saml-jumpcloud/", - image: "../../../../../../../images/integrations/logo-jumpcloud-badge-color.svg", - imageDarkMode: "../../../../../../../images/integrations/jumpcloud-darkmode.svg", - type: IntegrationType.SSO, - }, - { - name: "KeyCloak", - linkURL: "https://bitwarden.com/help/saml-keycloak/", - image: "../../../../../../../images/integrations/logo-keycloak-icon.svg", - type: IntegrationType.SSO, - }, - { - name: "Okta", - linkURL: "https://bitwarden.com/help/saml-okta/", - image: "../../../../../../../images/integrations/logo-okta-symbol-black.svg", - imageDarkMode: "../../../../../../../images/integrations/okta-darkmode.svg", - type: IntegrationType.SSO, - }, - { - name: "OneLogin", - linkURL: "https://bitwarden.com/help/saml-onelogin/", - image: "../../../../../../../images/integrations/logo-onelogin-badge-color.svg", - imageDarkMode: "../../../../../../../images/integrations/onelogin-darkmode.svg", - type: IntegrationType.SSO, - }, - { - name: "PingFederate", - linkURL: "https://bitwarden.com/help/saml-pingfederate/", - image: "../../../../../../../images/integrations/logo-ping-identity-badge-color.svg", - type: IntegrationType.SSO, - }, - { - name: "Microsoft Entra ID", - linkURL: "https://bitwarden.com/help/microsoft-entra-id-scim-integration/", - image: "../../../../../../../images/integrations/logo-microsoft-entra-id-color.svg", - type: IntegrationType.SCIM, - }, - { - name: "Okta", - linkURL: "https://bitwarden.com/help/okta-scim-integration/", - image: "../../../../../../../images/integrations/logo-okta-symbol-black.svg", - imageDarkMode: "../../../../../../../images/integrations/okta-darkmode.svg", - type: IntegrationType.SCIM, - }, - { - name: "OneLogin", - linkURL: "https://bitwarden.com/help/onelogin-scim-integration/", - image: "../../../../../../../images/integrations/logo-onelogin-badge-color.svg", - imageDarkMode: "../../../../../../../images/integrations/onelogin-darkmode.svg", - type: IntegrationType.SCIM, - }, - { - name: "JumpCloud", - linkURL: "https://bitwarden.com/help/jumpcloud-scim-integration/", - image: "../../../../../../../images/integrations/logo-jumpcloud-badge-color.svg", - imageDarkMode: "../../../../../../../images/integrations/jumpcloud-darkmode.svg", - type: IntegrationType.SCIM, - }, - { - name: "Ping Identity", - linkURL: "https://bitwarden.com/help/ping-identity-scim-integration/", - image: "../../../../../../../images/integrations/logo-ping-identity-badge-color.svg", - type: IntegrationType.SCIM, - }, - { - name: "Active Directory", - linkURL: "https://bitwarden.com/help/ldap-directory/", - image: "../../../../../../../images/integrations/azure-active-directory.svg", - type: IntegrationType.BWDC, - }, - { - name: "Microsoft Entra ID", - linkURL: "https://bitwarden.com/help/microsoft-entra-id/", - image: "../../../../../../../images/integrations/logo-microsoft-entra-id-color.svg", - type: IntegrationType.BWDC, - }, - { - name: "Google Workspace", - linkURL: "https://bitwarden.com/help/workspace-directory/", - image: "../../../../../../../images/integrations/logo-google-badge-color.svg", - type: IntegrationType.BWDC, - }, - { - name: "Okta", - linkURL: "https://bitwarden.com/help/okta-directory/", - image: "../../../../../../../images/integrations/logo-okta-symbol-black.svg", - imageDarkMode: "../../../../../../../images/integrations/okta-darkmode.svg", - type: IntegrationType.BWDC, - }, - { - name: "OneLogin", - linkURL: "https://bitwarden.com/help/onelogin-directory/", - image: "../../../../../../../images/integrations/logo-onelogin-badge-color.svg", - imageDarkMode: "../../../../../../../images/integrations/onelogin-darkmode.svg", - type: IntegrationType.BWDC, - }, - { - name: "Splunk", - linkURL: "https://bitwarden.com/help/splunk-siem/", - image: "../../../../../../../images/integrations/logo-splunk-black.svg", - imageDarkMode: "../../../../../../../images/integrations/splunk-darkmode.svg", - type: IntegrationType.EVENT, - }, - { - name: "Microsoft Sentinel", - linkURL: "https://bitwarden.com/help/microsoft-sentinel-siem/", - image: "../../../../../../../images/integrations/logo-microsoft-sentinel-color.svg", - type: IntegrationType.EVENT, - }, - { - name: "Rapid7", - linkURL: "https://bitwarden.com/help/rapid7-siem/", - image: "../../../../../../../images/integrations/logo-rapid7-black.svg", - imageDarkMode: "../../../../../../../images/integrations/rapid7-darkmode.svg", - type: IntegrationType.EVENT, - }, - { - name: "Elastic", - linkURL: "https://bitwarden.com/help/elastic-siem/", - image: "../../../../../../../images/integrations/logo-elastic-badge-color.svg", - type: IntegrationType.EVENT, - }, - { - name: "Panther", - linkURL: "https://bitwarden.com/help/panther-siem/", - image: "../../../../../../../images/integrations/logo-panther-round-color.svg", - type: IntegrationType.EVENT, - }, - { - name: "Microsoft Intune", - linkURL: "https://bitwarden.com/help/deploy-browser-extensions-with-intune/", - image: "../../../../../../../images/integrations/logo-microsoft-intune-color.svg", - type: IntegrationType.DEVICE, - }, - ]; - if (this.isEventBasedIntegrationsEnabled) { this.integrationsList.push({ name: "Crowdstrike", diff --git a/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-card/integration-card.component.html b/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-card/integration-card.component.html index 2c0db1cf933..e5687c71ed9 100644 --- a/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-card/integration-card.component.html +++ b/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-card/integration-card.component.html @@ -33,7 +33,7 @@

{{ description }}

@if (canSetupConnection) { - } diff --git a/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-card/integration-card.component.spec.ts b/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-card/integration-card.component.spec.ts index 16b7eb142e8..382d245b235 100644 --- a/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-card/integration-card.component.spec.ts +++ b/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-card/integration-card.component.spec.ts @@ -1,12 +1,15 @@ import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { ActivatedRoute } from "@angular/router"; import { mock } from "jest-mock-extended"; import { BehaviorSubject } from "rxjs"; import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-tokens"; +// eslint-disable-next-line no-restricted-imports +import { OrganizationIntegrationApiService } from "@bitwarden/bit-common/dirt/integrations/services"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { ThemeType } from "@bitwarden/common/platform/enums"; import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; -// FIXME: remove `src` and fix import +import { ToastService } from "@bitwarden/components"; // eslint-disable-next-line no-restricted-imports import { SharedModule } from "@bitwarden/components/src/shared"; import { I18nPipe } from "@bitwarden/ui-common"; @@ -17,6 +20,8 @@ describe("IntegrationCardComponent", () => { let component: IntegrationCardComponent; let fixture: ComponentFixture; const mockI18nService = mock(); + const activatedRoute = mock(); + const mockOrgIntegrationApiService = mock(); const systemTheme$ = new BehaviorSubject(ThemeType.Light); const usersPreferenceTheme$ = new BehaviorSubject(ThemeType.Light); @@ -24,26 +29,22 @@ describe("IntegrationCardComponent", () => { beforeEach(async () => { // reset system theme systemTheme$.next(ThemeType.Light); + activatedRoute.snapshot = { + paramMap: { + get: jest.fn().mockReturnValue("test-organization-id"), + }, + } as any; await TestBed.configureTestingModule({ imports: [IntegrationCardComponent, SharedModule], providers: [ - { - provide: ThemeStateService, - useValue: { selectedTheme$: usersPreferenceTheme$ }, - }, - { - provide: SYSTEM_THEME_OBSERVABLE, - useValue: systemTheme$, - }, - { - provide: I18nPipe, - useValue: mock(), - }, - { - provide: I18nService, - useValue: mockI18nService, - }, + { provide: ThemeStateService, useValue: { selectedTheme$: usersPreferenceTheme$ } }, + { provide: SYSTEM_THEME_OBSERVABLE, useValue: systemTheme$ }, + { provide: I18nPipe, useValue: mock() }, + { provide: I18nService, useValue: mockI18nService }, + { provide: ActivatedRoute, useValue: activatedRoute }, + { provide: OrganizationIntegrationApiService, useValue: mockOrgIntegrationApiService }, + { provide: ToastService, useValue: mock() }, ], }).compileComponents(); }); diff --git a/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-card/integration-card.component.ts b/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-card/integration-card.component.ts index 4188579bef9..1d95d3182b2 100644 --- a/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-card/integration-card.component.ts +++ b/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-card/integration-card.component.ts @@ -9,13 +9,26 @@ import { OnDestroy, ViewChild, } from "@angular/core"; -import { Observable, Subject, combineLatest, takeUntil } from "rxjs"; +import { ActivatedRoute } from "@angular/router"; +import { Observable, Subject, combineLatest, lastValueFrom, takeUntil } from "rxjs"; import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-tokens"; +// eslint-disable-next-line no-restricted-imports +import { + OrganizationIntegrationType, + OrganizationIntegrationRequest, + OrganizationIntegrationResponse, + OrganizationIntegrationApiService, +} from "@bitwarden/bit-common/dirt/integrations/index"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { ThemeType } from "@bitwarden/common/platform/enums"; import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; +import { OrganizationId } from "@bitwarden/common/types/guid"; +import { DialogService, ToastService } from "@bitwarden/components"; import { SharedModule } from "../../../../../../shared/shared.module"; +import { openHecConnectDialog } from "../integration-dialog/index"; +import { Integration } from "../models"; @Component({ selector: "app-integration-card", @@ -30,6 +43,7 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy { @Input() image: string; @Input() imageDarkMode?: string; @Input() linkURL: string; + @Input() integrationSettings: Integration; /** Adds relevant `rel` attribute to external links */ @Input() externalURL?: boolean; @@ -49,6 +63,11 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy { private themeStateService: ThemeStateService, @Inject(SYSTEM_THEME_OBSERVABLE) private systemTheme$: Observable, + private dialogService: DialogService, + private activatedRoute: ActivatedRoute, + private apiService: OrganizationIntegrationApiService, + private toastService: ToastService, + private i18nService: I18nService, ) {} ngAfterViewInit() { @@ -101,9 +120,58 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy { return this.isConnected !== undefined; } - setupConnection(app: string) { - // This method can be used to handle the connection logic for the integration - // For example, it could open a modal or redirect to a setup page - this.isConnected = !this.isConnected; // Toggle connection state for demonstration + async setupConnection() { + // invoke the dialog to connect the integration + const dialog = openHecConnectDialog(this.dialogService, { + data: { + settings: this.integrationSettings, + }, + }); + + const result = await lastValueFrom(dialog.closed); + + // the dialog was cancelled + if (!result || !result.success) { + return; + } + + // save the integration + try { + const dbResponse = await this.saveHecIntegration(result.configuration); + this.isConnected = !!dbResponse.id; + } catch { + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("failedToSaveIntegration"), + }); + return; + } + } + + async saveHecIntegration(configuration: string): Promise { + const organizationId = this.activatedRoute.snapshot.paramMap.get( + "organizationId", + ) as OrganizationId; + + const request = new OrganizationIntegrationRequest( + OrganizationIntegrationType.Hec, + configuration, + ); + + const integrations = await this.apiService.getOrganizationIntegrations(organizationId); + const existingIntegration = integrations.find( + (i) => i.type === OrganizationIntegrationType.Hec, + ); + + if (existingIntegration) { + return await this.apiService.updateOrganizationIntegration( + organizationId, + existingIntegration.id, + request, + ); + } else { + return await this.apiService.createOrganizationIntegration(organizationId, request); + } } } diff --git a/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-card/integration-card.stories.ts b/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-card/integration-card.stories.ts deleted file mode 100644 index 256bfd3d827..00000000000 --- a/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-card/integration-card.stories.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { importProvidersFrom } from "@angular/core"; -import { Meta, StoryObj, applicationConfig, moduleMetadata } from "@storybook/angular"; -import { of } from "rxjs"; - -import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-tokens"; -import { ThemeTypes } from "@bitwarden/common/platform/enums"; -import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; - -import { PreloadedEnglishI18nModule } from "../../../../../../core/tests"; - -import { IntegrationCardComponent } from "./integration-card.component"; - -class MockThemeService implements Partial {} - -export default { - title: "Web/Integration Layout/Integration Card", - component: IntegrationCardComponent, - decorators: [ - applicationConfig({ - providers: [importProvidersFrom(PreloadedEnglishI18nModule)], - }), - moduleMetadata({ - providers: [ - { - provide: ThemeStateService, - useClass: MockThemeService, - }, - { - provide: SYSTEM_THEME_OBSERVABLE, - useValue: of(ThemeTypes.Light), - }, - ], - }), - ], - args: { - integrations: [], - }, -} as Meta; - -type Story = StoryObj; - -export const Default: Story = { - render: (args) => ({ - props: args, - template: /*html*/ ` - - `, - }), - args: { - name: "Bitwarden", - image: "/integrations/bitwarden-vertical-blue.svg", - linkURL: "https://bitwarden.com", - }, -}; diff --git a/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-dialog/connect-dialog/connect-dialog-hec.component.html b/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-dialog/connect-dialog/connect-dialog-hec.component.html new file mode 100644 index 00000000000..7f28317dd67 --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-dialog/connect-dialog/connect-dialog-hec.component.html @@ -0,0 +1,38 @@ +
+ + + {{ "connectIntegrationButtonDesc" | i18n: connectInfo.settings.name }} + +
+ @if (loading) { + + + + } + @if (!loading) { + + + {{ "url" | i18n }} + + + + {{ "bearerToken" | i18n }} + + + + {{ "index" | i18n }} + + + + } +
+ + + + +
+
diff --git a/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-dialog/connect-dialog/connect-dialog-hec.component.spec.ts b/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-dialog/connect-dialog/connect-dialog-hec.component.spec.ts new file mode 100644 index 00000000000..9be854545aa --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-dialog/connect-dialog/connect-dialog-hec.component.spec.ts @@ -0,0 +1,176 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { FormBuilder, ReactiveFormsModule } from "@angular/forms"; +import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; +import { mock } from "jest-mock-extended"; + +import { IntegrationType } from "@bitwarden/common/enums"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { DIALOG_DATA, DialogConfig, DialogRef, DialogService } from "@bitwarden/components"; +import { I18nPipe } from "@bitwarden/ui-common"; +import { SharedModule } from "@bitwarden/web-vault/app/shared"; + +import { Integration } from "../../models"; + +import { + ConnectHecDialogComponent, + HecConnectDialogParams, + HecConnectDialogResult, + openHecConnectDialog, +} from "./connect-dialog-hec.component"; + +beforeAll(() => { + // Mock element.animate for jsdom + // the animate function is not available in jsdom, so we provide a mock implementation + // This is necessary for tests that rely on animations + // This mock does not perform any actual animations, it just provides a structure that allows tests + // to run without throwing errors related to missing animate function + if (!HTMLElement.prototype.animate) { + HTMLElement.prototype.animate = function () { + return { + play: () => {}, + pause: () => {}, + finish: () => {}, + cancel: () => {}, + reverse: () => {}, + addEventListener: () => {}, + removeEventListener: () => {}, + dispatchEvent: () => false, + onfinish: null, + oncancel: null, + startTime: 0, + currentTime: 0, + playbackRate: 1, + playState: "idle", + replaceState: "active", + effect: null, + finished: Promise.resolve(), + id: "", + remove: () => {}, + timeline: null, + ready: Promise.resolve(), + } as unknown as Animation; + }; + } +}); + +describe("ConnectDialogHecComponent", () => { + let component: ConnectHecDialogComponent; + let fixture: ComponentFixture; + let dialogRefMock = mock>(); + const mockI18nService = mock(); + + const integrationMock: Integration = { + name: "Test Integration", + image: "test-image.png", + linkURL: "https://example.com", + imageDarkMode: "test-image-dark.png", + newBadgeExpiration: "2024-12-31", + description: "Test Description", + isConnected: false, + canSetupConnection: true, + type: IntegrationType.EVENT, + } as Integration; + const connectInfo: HecConnectDialogParams = { settings: integrationMock }; + + beforeEach(async () => { + dialogRefMock = mock>(); + + await TestBed.configureTestingModule({ + imports: [ReactiveFormsModule, SharedModule, BrowserAnimationsModule], + providers: [ + FormBuilder, + { provide: DIALOG_DATA, useValue: connectInfo }, + { provide: DialogRef, useValue: dialogRefMock }, + { provide: I18nPipe, useValue: mock() }, + { provide: I18nService, useValue: mockI18nService }, + ], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ConnectHecDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + mockI18nService.t.mockImplementation((key) => key); + }); + + it("should create the component", () => { + expect(component).toBeTruthy(); + }); + + it("should initialize form with empty values", () => { + expect(component.formGroup.value).toEqual({ + url: "", + bearerToken: "", + index: "", + service: "Test Integration", + }); + }); + + it("should have required validators for all fields", () => { + component.formGroup.setValue({ url: "", bearerToken: "", index: "", service: "" }); + expect(component.formGroup.valid).toBeFalsy(); + + component.formGroup.setValue({ + url: "https://test.com", + bearerToken: "token", + index: "1", + service: "Test Service", + }); + expect(component.formGroup.valid).toBeTruthy(); + }); + + it("should invalidate url if not matching pattern", () => { + component.formGroup.setValue({ + url: "ftp://test.com", + bearerToken: "token", + index: "1", + service: "Test Service", + }); + expect(component.formGroup.valid).toBeFalsy(); + + component.formGroup.setValue({ + url: "https://test.com", + bearerToken: "token", + index: "1", + service: "Test Service", + }); + expect(component.formGroup.valid).toBeTruthy(); + }); + + it("should call dialogRef.close with correct result on submit", async () => { + component.formGroup.setValue({ + url: "https://test.com", + bearerToken: "token", + index: "1", + service: "Test Service", + }); + + await component.submit(); + + expect(dialogRefMock.close).toHaveBeenCalledWith({ + integrationSettings: integrationMock, + configuration: JSON.stringify({ + url: "https://test.com", + bearerToken: "token", + index: "1", + service: "Test Service", + }), + success: true, + error: null, + }); + }); +}); + +describe("openCrowdstrikeConnectDialog", () => { + it("should call dialogService.open with correct params", () => { + const dialogServiceMock = mock(); + const config: DialogConfig> = { + data: { settings: { name: "Test" } as Integration }, + } as any; + + openHecConnectDialog(dialogServiceMock, config); + + expect(dialogServiceMock.open).toHaveBeenCalledWith(ConnectHecDialogComponent, config); + }); +}); diff --git a/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-dialog/connect-dialog/connect-dialog-hec.component.ts b/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-dialog/connect-dialog/connect-dialog-hec.component.ts new file mode 100644 index 00000000000..c0af17db8d7 --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-dialog/connect-dialog/connect-dialog-hec.component.ts @@ -0,0 +1,81 @@ +import { Component, Inject, OnInit } from "@angular/core"; +import { FormBuilder, Validators } from "@angular/forms"; + +import { DIALOG_DATA, DialogConfig, DialogRef, DialogService } from "@bitwarden/components"; +import { SharedModule } from "@bitwarden/web-vault/app/shared"; + +import { Integration } from "../../models"; + +export type HecConnectDialogParams = { + settings: Integration; +}; + +export interface HecConnectDialogResult { + integrationSettings: Integration; + configuration: string; + success: boolean; + error: string | null; +} + +@Component({ + templateUrl: "./connect-dialog-hec.component.html", + imports: [SharedModule], +}) +export class ConnectHecDialogComponent implements OnInit { + loading = false; + formGroup = this.formBuilder.group({ + url: ["", [Validators.required, Validators.pattern("https?://.+")]], + bearerToken: ["", Validators.required], + index: ["", Validators.required], + service: ["", Validators.required], + }); + + constructor( + @Inject(DIALOG_DATA) protected connectInfo: HecConnectDialogParams, + protected formBuilder: FormBuilder, + private dialogRef: DialogRef, + ) {} + + ngOnInit(): void { + const settings = this.getSettingsAsJson(this.connectInfo.settings.configuration ?? ""); + + if (settings) { + this.formGroup.patchValue({ + url: settings?.url || "", + bearerToken: settings?.bearerToken || "", + index: settings?.index || "", + service: this.connectInfo.settings.name, + }); + } + } + + getSettingsAsJson(configuration: string) { + try { + return JSON.parse(configuration); + } catch { + return {}; + } + } + + submit = async (): Promise => { + const formJson = this.formGroup.getRawValue(); + + const result: HecConnectDialogResult = { + integrationSettings: this.connectInfo.settings, + configuration: JSON.stringify(formJson), + success: true, + error: null, + }; + + this.dialogRef.close(result); + + return; + }; +} + +export function openHecConnectDialog( + dialogService: DialogService, + config: DialogConfig>, +) { + return dialogService.open(ConnectHecDialogComponent, config); +} diff --git a/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-dialog/index.ts b/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-dialog/index.ts new file mode 100644 index 00000000000..8c4891b9aa8 --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-dialog/index.ts @@ -0,0 +1 @@ +export * from "./connect-dialog/connect-dialog-hec.component"; diff --git a/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-grid/integration-grid.component.html b/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-grid/integration-grid.component.html index b4eaff993f0..661c57b47fc 100644 --- a/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-grid/integration-grid.component.html +++ b/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-grid/integration-grid.component.html @@ -16,6 +16,7 @@ [description]="integration.description | i18n" [isConnected]="integration.isConnected" [canSetupConnection]="integration.canSetupConnection" + [integrationSettings]="integration" > diff --git a/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-grid/integration-grid.component.spec.ts b/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-grid/integration-grid.component.spec.ts index 04866f4627b..01a512ac38c 100644 --- a/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-grid/integration-grid.component.spec.ts +++ b/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-grid/integration-grid.component.spec.ts @@ -1,14 +1,20 @@ import { ComponentFixture, TestBed } from "@angular/core/testing"; import { By } from "@angular/platform-browser"; +import { ActivatedRoute } from "@angular/router"; import { mock } from "jest-mock-extended"; import { of } from "rxjs"; import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-tokens"; +// eslint-disable-next-line no-restricted-imports +import { OrganizationIntegrationApiService } from "@bitwarden/bit-common/dirt/integrations/services"; import { IntegrationType } from "@bitwarden/common/enums"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { ThemeTypes } from "@bitwarden/common/platform/enums"; +// eslint-disable-next-line import/order import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; // FIXME: remove `src` and fix import + +import { ToastService } from "@bitwarden/components"; // eslint-disable-next-line no-restricted-imports import { SharedModule } from "@bitwarden/components/src/shared"; import { I18nPipe } from "@bitwarden/ui-common"; @@ -21,6 +27,8 @@ import { IntegrationGridComponent } from "./integration-grid.component"; describe("IntegrationGridComponent", () => { let component: IntegrationGridComponent; let fixture: ComponentFixture; + const mockActivatedRoute = mock(); + const mockOrgIntegrationApiService = mock(); const integrations: Integration[] = [ { name: "Integration 1", @@ -37,6 +45,12 @@ describe("IntegrationGridComponent", () => { ]; beforeEach(() => { + mockActivatedRoute.snapshot = { + paramMap: { + get: jest.fn().mockReturnValue("test-organization-id"), + }, + } as any; + TestBed.configureTestingModule({ imports: [IntegrationGridComponent, IntegrationCardComponent, SharedModule], providers: [ @@ -56,6 +70,18 @@ describe("IntegrationGridComponent", () => { provide: I18nService, useValue: mock({ t: (key, p1) => key + " " + p1 }), }, + { + provide: ActivatedRoute, + useValue: mockActivatedRoute, + }, + { + provide: OrganizationIntegrationApiService, + useValue: mockOrgIntegrationApiService, + }, + { + provide: ToastService, + useValue: mock(), + }, ], }); diff --git a/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-grid/integration-grid.stories.ts b/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-grid/integration-grid.stories.ts deleted file mode 100644 index b6580af2881..00000000000 --- a/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-grid/integration-grid.stories.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { importProvidersFrom } from "@angular/core"; -import { Meta, StoryObj, applicationConfig, moduleMetadata } from "@storybook/angular"; -import { of } from "rxjs"; - -import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-tokens"; -import { IntegrationType } from "@bitwarden/common/enums"; -import { ThemeTypes } from "@bitwarden/common/platform/enums"; -import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; - -import { PreloadedEnglishI18nModule } from "../../../../../../core/tests"; -import { IntegrationCardComponent } from "../integration-card/integration-card.component"; -import { IntegrationGridComponent } from "../integration-grid/integration-grid.component"; - -class MockThemeService implements Partial {} - -export default { - title: "Web/Integration Layout/Integration Grid", - component: IntegrationGridComponent, - decorators: [ - applicationConfig({ - providers: [importProvidersFrom(PreloadedEnglishI18nModule)], - }), - moduleMetadata({ - imports: [IntegrationCardComponent], - providers: [ - { - provide: ThemeStateService, - useClass: MockThemeService, - }, - { - provide: SYSTEM_THEME_OBSERVABLE, - useValue: of(ThemeTypes.Dark), - }, - ], - }), - ], -} as Meta; - -type Story = StoryObj; - -export const Default: Story = { - render: (args) => ({ - props: args, - template: /*html*/ ` - - `, - }), - args: { - integrations: [ - { - name: "Card 1", - linkURL: "https://bitwarden.com", - image: "/integrations/bitwarden-vertical-blue.svg", - type: IntegrationType.SSO, - }, - { - name: "Card 2", - linkURL: "https://bitwarden.com", - image: "/integrations/bitwarden-vertical-blue.svg", - type: IntegrationType.SDK, - }, - { - name: "Card 3", - linkURL: "https://bitwarden.com", - image: "/integrations/bitwarden-vertical-blue.svg", - type: IntegrationType.SCIM, - }, - ], - }, -}; diff --git a/apps/web/src/app/admin-console/organizations/shared/components/integrations/models.ts b/apps/web/src/app/admin-console/organizations/shared/components/integrations/models.ts index a231523b578..b3d24ffb3b0 100644 --- a/apps/web/src/app/admin-console/organizations/shared/components/integrations/models.ts +++ b/apps/web/src/app/admin-console/organizations/shared/components/integrations/models.ts @@ -20,4 +20,5 @@ export type Integration = { description?: string; isConnected?: boolean; canSetupConnection?: boolean; + configuration?: string; }; diff --git a/apps/web/src/app/core/core.module.ts b/apps/web/src/app/core/core.module.ts index 92f9eaaee03..a222b668043 100644 --- a/apps/web/src/app/core/core.module.ts +++ b/apps/web/src/app/core/core.module.ts @@ -41,6 +41,8 @@ import { InternalUserDecryptionOptionsServiceAbstraction, LoginEmailService, } from "@bitwarden/auth/common"; +// eslint-disable-next-line no-restricted-imports +import { OrganizationIntegrationApiService } from "@bitwarden/bit-common/dirt/integrations"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; @@ -392,6 +394,11 @@ const safeProviders: SafeProvider[] = [ useClass: DefaultDeviceManagementComponentService, deps: [], }), + safeProvider({ + provide: OrganizationIntegrationApiService, + useClass: OrganizationIntegrationApiService, + deps: [ApiService], + }), ]; @NgModule({ diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 88a26c6f594..2a75bf51900 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -9481,6 +9481,9 @@ "crowdstrikeEventIntegrationDesc": { "message": "Send event data to your Logscale instance" }, + "failedToSaveIntegration": { + "message": "Failed to save integration. Please try again later." + }, "deviceIdMissing": { "message": "Device ID is missing" }, @@ -9562,6 +9565,15 @@ "createNewClientToManageAsProvider": { "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." }, + "url": { + "message": "URL" + }, + "bearerToken": { + "message": "Bearer Token" + }, + "index": { + "message": "Index" + }, "selectAPlan": { "message": "Select a plan" }, diff --git a/bitwarden_license/bit-common/src/dirt/integrations/index.ts b/bitwarden_license/bit-common/src/dirt/integrations/index.ts index b2221a94a89..d2c1d173e3c 100644 --- a/bitwarden_license/bit-common/src/dirt/integrations/index.ts +++ b/bitwarden_license/bit-common/src/dirt/integrations/index.ts @@ -1 +1,6 @@ export * from "./services"; +export * from "./models/organization-integration-type"; +export * from "./models/organization-integration-request"; +export * from "./models/organization-integration-response"; +export * from "./models/organization-integration-configuration-request"; +export * from "./models/organization-integration-configuration-response"; diff --git a/bitwarden_license/bit-common/src/dirt/integrations/models/organization-integration-response.ts b/bitwarden_license/bit-common/src/dirt/integrations/models/organization-integration-response.ts index 00880ea4740..d4a836df4c3 100644 --- a/bitwarden_license/bit-common/src/dirt/integrations/models/organization-integration-response.ts +++ b/bitwarden_license/bit-common/src/dirt/integrations/models/organization-integration-response.ts @@ -5,11 +5,13 @@ import { OrganizationIntegrationType } from "./organization-integration-type"; export class OrganizationIntegrationResponse extends BaseResponse { id: OrganizationIntegrationId; - organizationIntegrationType: OrganizationIntegrationType; + type: OrganizationIntegrationType; + configuration: string; constructor(response: any) { super(response); this.id = this.getResponseProperty("Id"); - this.organizationIntegrationType = this.getResponseProperty("Type"); + this.type = this.getResponseProperty("Type"); + this.configuration = this.getResponseProperty("Configuration"); } } diff --git a/bitwarden_license/bit-common/src/dirt/integrations/services/organization-integration-api.service.spec.ts b/bitwarden_license/bit-common/src/dirt/integrations/services/organization-integration-api.service.spec.ts index bf3e16ed430..10ea87486b4 100644 --- a/bitwarden_license/bit-common/src/dirt/integrations/services/organization-integration-api.service.spec.ts +++ b/bitwarden_license/bit-common/src/dirt/integrations/services/organization-integration-api.service.spec.ts @@ -11,17 +11,17 @@ import { OrganizationIntegrationApiService } from "./organization-integration-ap export const mockIntegrationResponse: any = { id: "1" as OrganizationIntegrationId, - organizationIntegrationType: OrganizationIntegrationType.Hec, + type: OrganizationIntegrationType.Hec, }; export const mockIntegrationResponses: any[] = [ { id: "1" as OrganizationIntegrationId, - OrganizationIntegrationType: OrganizationIntegrationType.Hec, + type: OrganizationIntegrationType.Hec, }, { id: "2" as OrganizationIntegrationId, - OrganizationIntegrationType: OrganizationIntegrationType.Webhook, + type: OrganizationIntegrationType.Webhook, }, ]; @@ -46,7 +46,7 @@ describe("OrganizationIntegrationApiService", () => { expect(result).toEqual(mockIntegrationResponses); expect(apiService.send).toHaveBeenCalledWith( "GET", - `organizations/${orgId}/integrations`, + `/organizations/${orgId}/integrations`, null, true, true, @@ -63,12 +63,10 @@ describe("OrganizationIntegrationApiService", () => { apiService.send.mockReturnValue(Promise.resolve(mockIntegrationResponse)); const result = await service.createOrganizationIntegration(orgId, request); - expect(result.organizationIntegrationType).toEqual( - mockIntegrationResponse.organizationIntegrationType, - ); + expect(result.type).toEqual(mockIntegrationResponse.type); expect(apiService.send).toHaveBeenCalledWith( "POST", - `organizations/${orgId.toString()}/integrations`, + `/organizations/${orgId.toString()}/integrations`, request, true, true, @@ -86,12 +84,10 @@ describe("OrganizationIntegrationApiService", () => { apiService.send.mockReturnValue(Promise.resolve(mockIntegrationResponse)); const result = await service.updateOrganizationIntegration(orgId, integrationId, request); - expect(result.organizationIntegrationType).toEqual( - mockIntegrationResponse.organizationIntegrationType, - ); + expect(result.type).toEqual(mockIntegrationResponse.type); expect(apiService.send).toHaveBeenCalledWith( "PUT", - `organizations/${orgId}/integrations/${integrationId}`, + `/organizations/${orgId}/integrations/${integrationId}`, request, true, true, @@ -106,7 +102,7 @@ describe("OrganizationIntegrationApiService", () => { expect(apiService.send).toHaveBeenCalledWith( "DELETE", - `organizations/${orgId}/integrations/${integrationId}`, + `/organizations/${orgId}/integrations/${integrationId}`, null, true, false, diff --git a/bitwarden_license/bit-common/src/dirt/integrations/services/organization-integration-api.service.ts b/bitwarden_license/bit-common/src/dirt/integrations/services/organization-integration-api.service.ts index 5cf8efefb05..2c2266940e0 100644 --- a/bitwarden_license/bit-common/src/dirt/integrations/services/organization-integration-api.service.ts +++ b/bitwarden_license/bit-common/src/dirt/integrations/services/organization-integration-api.service.ts @@ -15,7 +15,7 @@ export class OrganizationIntegrationApiService { ): Promise { const response = await this.apiService.send( "GET", - `organizations/${orgId}/integrations`, + `/organizations/${orgId}/integrations`, null, true, true, @@ -29,7 +29,7 @@ export class OrganizationIntegrationApiService { ): Promise { const response = await this.apiService.send( "POST", - `organizations/${orgId}/integrations`, + `/organizations/${orgId}/integrations`, request, true, true, @@ -44,7 +44,7 @@ export class OrganizationIntegrationApiService { ): Promise { const response = await this.apiService.send( "PUT", - `organizations/${orgId}/integrations/${integrationId}`, + `/organizations/${orgId}/integrations/${integrationId}`, request, true, true, @@ -58,7 +58,7 @@ export class OrganizationIntegrationApiService { ): Promise { await this.apiService.send( "DELETE", - `organizations/${orgId}/integrations/${integrationId}`, + `/organizations/${orgId}/integrations/${integrationId}`, null, true, false, diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.spec.ts b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.spec.ts index b563591f32f..be4b5725ecc 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.spec.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.spec.ts @@ -1,6 +1,7 @@ import { Component } from "@angular/core"; import { ComponentFixture, TestBed } from "@angular/core/testing"; import { By } from "@angular/platform-browser"; +import { ActivatedRoute } from "@angular/router"; import { mock } from "jest-mock-extended"; import { of } from "rxjs"; @@ -8,9 +9,12 @@ import {} from "@bitwarden/web-vault/app/shared"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-tokens"; +import { OrganizationIntegrationApiService } from "@bitwarden/bit-common/dirt/integrations"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { ThemeType } from "@bitwarden/common/platform/enums"; import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; +import { ToastService } from "@bitwarden/components"; +import { I18nPipe } from "@bitwarden/ui-common"; import { IntegrationCardComponent } from "@bitwarden/web-vault/app/admin-console/organizations/shared/components/integrations/integration-card/integration-card.component"; import { IntegrationGridComponent } from "@bitwarden/web-vault/app/admin-console/organizations/shared/components/integrations/integration-grid/integration-grid.component"; @@ -33,23 +37,25 @@ class MockNewMenuComponent {} describe("IntegrationsComponent", () => { let fixture: ComponentFixture; + const mockOrgIntegrationApiService = mock(); + const activatedRouteMock = { + snapshot: { paramMap: { get: jest.fn() } }, + }; + const mockI18nService = mock(); + beforeEach(async () => { await TestBed.configureTestingModule({ declarations: [IntegrationsComponent, MockHeaderComponent, MockNewMenuComponent], imports: [JslibModule, IntegrationGridComponent, IntegrationCardComponent], providers: [ - { - provide: I18nService, - useValue: mock(), - }, - { - provide: ThemeStateService, - useValue: mock(), - }, - { - provide: SYSTEM_THEME_OBSERVABLE, - useValue: of(ThemeType.Light), - }, + { provide: I18nService, useValue: mock() }, + { provide: ThemeStateService, useValue: mock() }, + { provide: SYSTEM_THEME_OBSERVABLE, useValue: of(ThemeType.Light) }, + { provide: ActivatedRoute, useValue: activatedRouteMock }, + { provide: OrganizationIntegrationApiService, useValue: mockOrgIntegrationApiService }, + { provide: ToastService, useValue: mock() }, + { provide: I18nPipe, useValue: mock() }, + { provide: I18nService, useValue: mockI18nService }, ], }).compileComponents(); fixture = TestBed.createComponent(IntegrationsComponent);