1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-16 16:23:44 +00:00

[PM-23826] Crowdstrike integration dialog (#15757)

This commit is contained in:
Vijay Oommen
2025-07-31 11:45:35 -05:00
committed by GitHub
parent 95b1ab0cb7
commit 13a8b46d30
20 changed files with 678 additions and 360 deletions

View File

@@ -2,8 +2,10 @@
// @ts-strict-ignore // @ts-strict-ignore
import { Component, OnDestroy, OnInit } from "@angular/core"; import { Component, OnDestroy, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router"; 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 { import {
getOrganizationById, getOrganizationById,
OrganizationService, OrganizationService,
@@ -33,13 +35,192 @@ import { Integration } from "../shared/components/integrations/models";
], ],
}) })
export class AdminConsoleIntegrationsComponent implements OnInit, OnDestroy { export class AdminConsoleIntegrationsComponent implements OnInit, OnDestroy {
integrationsList: Integration[] = []; // integrationsList: Integration[] = [];
tabIndex: number; tabIndex: number;
organization$: Observable<Organization>; organization$: Observable<Organization>;
isEventBasedIntegrationsEnabled: boolean = false; isEventBasedIntegrationsEnabled: boolean = false;
private destroy$ = new Subject<void>(); private destroy$ = new Subject<void>();
// 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 { ngOnInit(): void {
const orgId = this.route.snapshot.params.organizationId;
this.organization$ = this.route.params.pipe( this.organization$ = this.route.params.pipe(
switchMap((params) => switchMap((params) =>
this.accountService.activeAccount$.pipe( 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( constructor(
@@ -58,6 +258,7 @@ export class AdminConsoleIntegrationsComponent implements OnInit, OnDestroy {
private organizationService: OrganizationService, private organizationService: OrganizationService,
private accountService: AccountService, private accountService: AccountService,
private configService: ConfigService, private configService: ConfigService,
private orgIntegrationApiService: OrganizationIntegrationApiService,
) { ) {
this.configService this.configService
.getFeatureFlag$(FeatureFlag.EventBasedOrganizationIntegrations) .getFeatureFlag$(FeatureFlag.EventBasedOrganizationIntegrations)
@@ -66,182 +267,6 @@ export class AdminConsoleIntegrationsComponent implements OnInit, OnDestroy {
this.isEventBasedIntegrationsEnabled = isEnabled; 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) { if (this.isEventBasedIntegrationsEnabled) {
this.integrationsList.push({ this.integrationsList.push({
name: "Crowdstrike", name: "Crowdstrike",

View File

@@ -33,7 +33,7 @@
<p class="tw-mb-0">{{ description }}</p> <p class="tw-mb-0">{{ description }}</p>
@if (canSetupConnection) { @if (canSetupConnection) {
<button type="button" class="tw-mt-3" bitButton (click)="setupConnection(name)"> <button type="button" class="tw-mt-3" bitButton (click)="setupConnection()">
<span>{{ "connectIntegrationButtonDesc" | i18n: name }}</span> <span>{{ "connectIntegrationButtonDesc" | i18n: name }}</span>
</button> </button>
} }

View File

@@ -1,12 +1,15 @@
import { ComponentFixture, TestBed } from "@angular/core/testing"; import { ComponentFixture, TestBed } from "@angular/core/testing";
import { ActivatedRoute } from "@angular/router";
import { mock } from "jest-mock-extended"; import { mock } from "jest-mock-extended";
import { BehaviorSubject } from "rxjs"; import { BehaviorSubject } from "rxjs";
import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-tokens"; 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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { ThemeType } from "@bitwarden/common/platform/enums"; import { ThemeType } from "@bitwarden/common/platform/enums";
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; 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 // eslint-disable-next-line no-restricted-imports
import { SharedModule } from "@bitwarden/components/src/shared"; import { SharedModule } from "@bitwarden/components/src/shared";
import { I18nPipe } from "@bitwarden/ui-common"; import { I18nPipe } from "@bitwarden/ui-common";
@@ -17,6 +20,8 @@ describe("IntegrationCardComponent", () => {
let component: IntegrationCardComponent; let component: IntegrationCardComponent;
let fixture: ComponentFixture<IntegrationCardComponent>; let fixture: ComponentFixture<IntegrationCardComponent>;
const mockI18nService = mock<I18nService>(); const mockI18nService = mock<I18nService>();
const activatedRoute = mock<ActivatedRoute>();
const mockOrgIntegrationApiService = mock<OrganizationIntegrationApiService>();
const systemTheme$ = new BehaviorSubject<ThemeType>(ThemeType.Light); const systemTheme$ = new BehaviorSubject<ThemeType>(ThemeType.Light);
const usersPreferenceTheme$ = new BehaviorSubject<ThemeType>(ThemeType.Light); const usersPreferenceTheme$ = new BehaviorSubject<ThemeType>(ThemeType.Light);
@@ -24,26 +29,22 @@ describe("IntegrationCardComponent", () => {
beforeEach(async () => { beforeEach(async () => {
// reset system theme // reset system theme
systemTheme$.next(ThemeType.Light); systemTheme$.next(ThemeType.Light);
activatedRoute.snapshot = {
paramMap: {
get: jest.fn().mockReturnValue("test-organization-id"),
},
} as any;
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [IntegrationCardComponent, SharedModule], imports: [IntegrationCardComponent, SharedModule],
providers: [ providers: [
{ { provide: ThemeStateService, useValue: { selectedTheme$: usersPreferenceTheme$ } },
provide: ThemeStateService, { provide: SYSTEM_THEME_OBSERVABLE, useValue: systemTheme$ },
useValue: { selectedTheme$: usersPreferenceTheme$ }, { provide: I18nPipe, useValue: mock<I18nPipe>() },
}, { provide: I18nService, useValue: mockI18nService },
{ { provide: ActivatedRoute, useValue: activatedRoute },
provide: SYSTEM_THEME_OBSERVABLE, { provide: OrganizationIntegrationApiService, useValue: mockOrgIntegrationApiService },
useValue: systemTheme$, { provide: ToastService, useValue: mock<ToastService>() },
},
{
provide: I18nPipe,
useValue: mock<I18nPipe>(),
},
{
provide: I18nService,
useValue: mockI18nService,
},
], ],
}).compileComponents(); }).compileComponents();
}); });

View File

@@ -9,13 +9,26 @@ import {
OnDestroy, OnDestroy,
ViewChild, ViewChild,
} from "@angular/core"; } 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"; 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 { ThemeType } from "@bitwarden/common/platform/enums";
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; 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 { SharedModule } from "../../../../../../shared/shared.module";
import { openHecConnectDialog } from "../integration-dialog/index";
import { Integration } from "../models";
@Component({ @Component({
selector: "app-integration-card", selector: "app-integration-card",
@@ -30,6 +43,7 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy {
@Input() image: string; @Input() image: string;
@Input() imageDarkMode?: string; @Input() imageDarkMode?: string;
@Input() linkURL: string; @Input() linkURL: string;
@Input() integrationSettings: Integration;
/** Adds relevant `rel` attribute to external links */ /** Adds relevant `rel` attribute to external links */
@Input() externalURL?: boolean; @Input() externalURL?: boolean;
@@ -49,6 +63,11 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy {
private themeStateService: ThemeStateService, private themeStateService: ThemeStateService,
@Inject(SYSTEM_THEME_OBSERVABLE) @Inject(SYSTEM_THEME_OBSERVABLE)
private systemTheme$: Observable<ThemeType>, private systemTheme$: Observable<ThemeType>,
private dialogService: DialogService,
private activatedRoute: ActivatedRoute,
private apiService: OrganizationIntegrationApiService,
private toastService: ToastService,
private i18nService: I18nService,
) {} ) {}
ngAfterViewInit() { ngAfterViewInit() {
@@ -101,9 +120,58 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy {
return this.isConnected !== undefined; return this.isConnected !== undefined;
} }
setupConnection(app: string) { async setupConnection() {
// This method can be used to handle the connection logic for the integration // invoke the dialog to connect the integration
// For example, it could open a modal or redirect to a setup page const dialog = openHecConnectDialog(this.dialogService, {
this.isConnected = !this.isConnected; // Toggle connection state for demonstration 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<OrganizationIntegrationResponse> {
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);
}
} }
} }

View File

@@ -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<ThemeStateService> {}
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<IntegrationCardComponent>;
export const Default: Story = {
render: (args) => ({
props: args,
template: /*html*/ `
<app-integration-card
[name]="name"
[image]="image"
[linkURL]="linkURL"
></app-integration-card>
`,
}),
args: {
name: "Bitwarden",
image: "/integrations/bitwarden-vertical-blue.svg",
linkURL: "https://bitwarden.com",
},
};

View File

@@ -0,0 +1,38 @@
<form [formGroup]="formGroup" [bitSubmit]="submit">
<bit-dialog dialogSize="large" [loading]="loading">
<span bitDialogTitle>
{{ "connectIntegrationButtonDesc" | i18n: connectInfo.settings.name }}
</span>
<div bitDialogContent class="tw-flex tw-flex-col tw-gap-4">
@if (loading) {
<ng-container #spinner>
<i class="bwi bwi-spinner bwi-lg bwi-spin" aria-hidden="true"></i>
</ng-container>
}
@if (!loading) {
<ng-container>
<bit-form-field>
<bit-label>{{ "url" | i18n }}</bit-label>
<input bitInput formControlName="url" />
</bit-form-field>
<bit-form-field>
<bit-label>{{ "bearerToken" | i18n }}</bit-label>
<input bitInput formControlName="bearerToken" />
</bit-form-field>
<bit-form-field>
<bit-label>{{ "index" | i18n }}</bit-label>
<input bitInput formControlName="index" />
</bit-form-field>
</ng-container>
}
</div>
<ng-container bitDialogFooter>
<button type="submit" bitButton bitFormButton buttonType="primary" [disabled]="loading">
{{ "save" | i18n }}
</button>
<button type="button" bitButton bitDialogClose buttonType="secondary" [disabled]="loading">
{{ "cancel" | i18n }}
</button>
</ng-container>
</bit-dialog>
</form>

View File

@@ -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<ConnectHecDialogComponent>;
let dialogRefMock = mock<DialogRef<HecConnectDialogResult>>();
const mockI18nService = mock<I18nService>();
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<DialogRef<HecConnectDialogResult>>();
await TestBed.configureTestingModule({
imports: [ReactiveFormsModule, SharedModule, BrowserAnimationsModule],
providers: [
FormBuilder,
{ provide: DIALOG_DATA, useValue: connectInfo },
{ provide: DialogRef, useValue: dialogRefMock },
{ provide: I18nPipe, useValue: mock<I18nPipe>() },
{ 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<DialogService>();
const config: DialogConfig<HecConnectDialogParams, DialogRef<HecConnectDialogResult>> = {
data: { settings: { name: "Test" } as Integration },
} as any;
openHecConnectDialog(dialogServiceMock, config);
expect(dialogServiceMock.open).toHaveBeenCalledWith(ConnectHecDialogComponent, config);
});
});

View File

@@ -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<HecConnectDialogResult>,
) {}
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<void> => {
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<HecConnectDialogParams, DialogRef<HecConnectDialogResult>>,
) {
return dialogService.open<HecConnectDialogResult>(ConnectHecDialogComponent, config);
}

View File

@@ -0,0 +1 @@
export * from "./connect-dialog/connect-dialog-hec.component";

View File

@@ -16,6 +16,7 @@
[description]="integration.description | i18n" [description]="integration.description | i18n"
[isConnected]="integration.isConnected" [isConnected]="integration.isConnected"
[canSetupConnection]="integration.canSetupConnection" [canSetupConnection]="integration.canSetupConnection"
[integrationSettings]="integration"
></app-integration-card> ></app-integration-card>
</li> </li>
</ul> </ul>

View File

@@ -1,14 +1,20 @@
import { ComponentFixture, TestBed } from "@angular/core/testing"; import { ComponentFixture, TestBed } from "@angular/core/testing";
import { By } from "@angular/platform-browser"; import { By } from "@angular/platform-browser";
import { ActivatedRoute } from "@angular/router";
import { mock } from "jest-mock-extended"; import { mock } from "jest-mock-extended";
import { of } from "rxjs"; import { of } from "rxjs";
import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-tokens"; 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 { IntegrationType } from "@bitwarden/common/enums";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { ThemeTypes } from "@bitwarden/common/platform/enums"; import { ThemeTypes } from "@bitwarden/common/platform/enums";
// eslint-disable-next-line import/order
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
// FIXME: remove `src` and fix import // FIXME: remove `src` and fix import
import { ToastService } from "@bitwarden/components";
// eslint-disable-next-line no-restricted-imports // eslint-disable-next-line no-restricted-imports
import { SharedModule } from "@bitwarden/components/src/shared"; import { SharedModule } from "@bitwarden/components/src/shared";
import { I18nPipe } from "@bitwarden/ui-common"; import { I18nPipe } from "@bitwarden/ui-common";
@@ -21,6 +27,8 @@ import { IntegrationGridComponent } from "./integration-grid.component";
describe("IntegrationGridComponent", () => { describe("IntegrationGridComponent", () => {
let component: IntegrationGridComponent; let component: IntegrationGridComponent;
let fixture: ComponentFixture<IntegrationGridComponent>; let fixture: ComponentFixture<IntegrationGridComponent>;
const mockActivatedRoute = mock<ActivatedRoute>();
const mockOrgIntegrationApiService = mock<OrganizationIntegrationApiService>();
const integrations: Integration[] = [ const integrations: Integration[] = [
{ {
name: "Integration 1", name: "Integration 1",
@@ -37,6 +45,12 @@ describe("IntegrationGridComponent", () => {
]; ];
beforeEach(() => { beforeEach(() => {
mockActivatedRoute.snapshot = {
paramMap: {
get: jest.fn().mockReturnValue("test-organization-id"),
},
} as any;
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [IntegrationGridComponent, IntegrationCardComponent, SharedModule], imports: [IntegrationGridComponent, IntegrationCardComponent, SharedModule],
providers: [ providers: [
@@ -56,6 +70,18 @@ describe("IntegrationGridComponent", () => {
provide: I18nService, provide: I18nService,
useValue: mock<I18nService>({ t: (key, p1) => key + " " + p1 }), useValue: mock<I18nService>({ t: (key, p1) => key + " " + p1 }),
}, },
{
provide: ActivatedRoute,
useValue: mockActivatedRoute,
},
{
provide: OrganizationIntegrationApiService,
useValue: mockOrgIntegrationApiService,
},
{
provide: ToastService,
useValue: mock<ToastService>(),
},
], ],
}); });

View File

@@ -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<ThemeStateService> {}
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<IntegrationGridComponent>;
export const Default: Story = {
render: (args) => ({
props: args,
template: /*html*/ `
<app-integration-grid [integrations]="integrations"></app-integration-grid>
`,
}),
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,
},
],
},
};

View File

@@ -20,4 +20,5 @@ export type Integration = {
description?: string; description?: string;
isConnected?: boolean; isConnected?: boolean;
canSetupConnection?: boolean; canSetupConnection?: boolean;
configuration?: string;
}; };

View File

@@ -41,6 +41,8 @@ import {
InternalUserDecryptionOptionsServiceAbstraction, InternalUserDecryptionOptionsServiceAbstraction,
LoginEmailService, LoginEmailService,
} from "@bitwarden/auth/common"; } 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 { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; 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"; import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
@@ -392,6 +394,11 @@ const safeProviders: SafeProvider[] = [
useClass: DefaultDeviceManagementComponentService, useClass: DefaultDeviceManagementComponentService,
deps: [], deps: [],
}), }),
safeProvider({
provide: OrganizationIntegrationApiService,
useClass: OrganizationIntegrationApiService,
deps: [ApiService],
}),
]; ];
@NgModule({ @NgModule({

View File

@@ -9481,6 +9481,9 @@
"crowdstrikeEventIntegrationDesc": { "crowdstrikeEventIntegrationDesc": {
"message": "Send event data to your Logscale instance" "message": "Send event data to your Logscale instance"
}, },
"failedToSaveIntegration": {
"message": "Failed to save integration. Please try again later."
},
"deviceIdMissing": { "deviceIdMissing": {
"message": "Device ID is missing" "message": "Device ID is missing"
}, },
@@ -9562,6 +9565,15 @@
"createNewClientToManageAsProvider": { "createNewClientToManageAsProvider": {
"message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." "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": { "selectAPlan": {
"message": "Select a plan" "message": "Select a plan"
}, },

View File

@@ -1 +1,6 @@
export * from "./services"; 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";

View File

@@ -5,11 +5,13 @@ import { OrganizationIntegrationType } from "./organization-integration-type";
export class OrganizationIntegrationResponse extends BaseResponse { export class OrganizationIntegrationResponse extends BaseResponse {
id: OrganizationIntegrationId; id: OrganizationIntegrationId;
organizationIntegrationType: OrganizationIntegrationType; type: OrganizationIntegrationType;
configuration: string;
constructor(response: any) { constructor(response: any) {
super(response); super(response);
this.id = this.getResponseProperty("Id"); this.id = this.getResponseProperty("Id");
this.organizationIntegrationType = this.getResponseProperty("Type"); this.type = this.getResponseProperty("Type");
this.configuration = this.getResponseProperty("Configuration");
} }
} }

View File

@@ -11,17 +11,17 @@ import { OrganizationIntegrationApiService } from "./organization-integration-ap
export const mockIntegrationResponse: any = { export const mockIntegrationResponse: any = {
id: "1" as OrganizationIntegrationId, id: "1" as OrganizationIntegrationId,
organizationIntegrationType: OrganizationIntegrationType.Hec, type: OrganizationIntegrationType.Hec,
}; };
export const mockIntegrationResponses: any[] = [ export const mockIntegrationResponses: any[] = [
{ {
id: "1" as OrganizationIntegrationId, id: "1" as OrganizationIntegrationId,
OrganizationIntegrationType: OrganizationIntegrationType.Hec, type: OrganizationIntegrationType.Hec,
}, },
{ {
id: "2" as OrganizationIntegrationId, id: "2" as OrganizationIntegrationId,
OrganizationIntegrationType: OrganizationIntegrationType.Webhook, type: OrganizationIntegrationType.Webhook,
}, },
]; ];
@@ -46,7 +46,7 @@ describe("OrganizationIntegrationApiService", () => {
expect(result).toEqual(mockIntegrationResponses); expect(result).toEqual(mockIntegrationResponses);
expect(apiService.send).toHaveBeenCalledWith( expect(apiService.send).toHaveBeenCalledWith(
"GET", "GET",
`organizations/${orgId}/integrations`, `/organizations/${orgId}/integrations`,
null, null,
true, true,
true, true,
@@ -63,12 +63,10 @@ describe("OrganizationIntegrationApiService", () => {
apiService.send.mockReturnValue(Promise.resolve(mockIntegrationResponse)); apiService.send.mockReturnValue(Promise.resolve(mockIntegrationResponse));
const result = await service.createOrganizationIntegration(orgId, request); const result = await service.createOrganizationIntegration(orgId, request);
expect(result.organizationIntegrationType).toEqual( expect(result.type).toEqual(mockIntegrationResponse.type);
mockIntegrationResponse.organizationIntegrationType,
);
expect(apiService.send).toHaveBeenCalledWith( expect(apiService.send).toHaveBeenCalledWith(
"POST", "POST",
`organizations/${orgId.toString()}/integrations`, `/organizations/${orgId.toString()}/integrations`,
request, request,
true, true,
true, true,
@@ -86,12 +84,10 @@ describe("OrganizationIntegrationApiService", () => {
apiService.send.mockReturnValue(Promise.resolve(mockIntegrationResponse)); apiService.send.mockReturnValue(Promise.resolve(mockIntegrationResponse));
const result = await service.updateOrganizationIntegration(orgId, integrationId, request); const result = await service.updateOrganizationIntegration(orgId, integrationId, request);
expect(result.organizationIntegrationType).toEqual( expect(result.type).toEqual(mockIntegrationResponse.type);
mockIntegrationResponse.organizationIntegrationType,
);
expect(apiService.send).toHaveBeenCalledWith( expect(apiService.send).toHaveBeenCalledWith(
"PUT", "PUT",
`organizations/${orgId}/integrations/${integrationId}`, `/organizations/${orgId}/integrations/${integrationId}`,
request, request,
true, true,
true, true,
@@ -106,7 +102,7 @@ describe("OrganizationIntegrationApiService", () => {
expect(apiService.send).toHaveBeenCalledWith( expect(apiService.send).toHaveBeenCalledWith(
"DELETE", "DELETE",
`organizations/${orgId}/integrations/${integrationId}`, `/organizations/${orgId}/integrations/${integrationId}`,
null, null,
true, true,
false, false,

View File

@@ -15,7 +15,7 @@ export class OrganizationIntegrationApiService {
): Promise<OrganizationIntegrationResponse[]> { ): Promise<OrganizationIntegrationResponse[]> {
const response = await this.apiService.send( const response = await this.apiService.send(
"GET", "GET",
`organizations/${orgId}/integrations`, `/organizations/${orgId}/integrations`,
null, null,
true, true,
true, true,
@@ -29,7 +29,7 @@ export class OrganizationIntegrationApiService {
): Promise<OrganizationIntegrationResponse> { ): Promise<OrganizationIntegrationResponse> {
const response = await this.apiService.send( const response = await this.apiService.send(
"POST", "POST",
`organizations/${orgId}/integrations`, `/organizations/${orgId}/integrations`,
request, request,
true, true,
true, true,
@@ -44,7 +44,7 @@ export class OrganizationIntegrationApiService {
): Promise<OrganizationIntegrationResponse> { ): Promise<OrganizationIntegrationResponse> {
const response = await this.apiService.send( const response = await this.apiService.send(
"PUT", "PUT",
`organizations/${orgId}/integrations/${integrationId}`, `/organizations/${orgId}/integrations/${integrationId}`,
request, request,
true, true,
true, true,
@@ -58,7 +58,7 @@ export class OrganizationIntegrationApiService {
): Promise<any> { ): Promise<any> {
await this.apiService.send( await this.apiService.send(
"DELETE", "DELETE",
`organizations/${orgId}/integrations/${integrationId}`, `/organizations/${orgId}/integrations/${integrationId}`,
null, null,
true, true,
false, false,

View File

@@ -1,6 +1,7 @@
import { Component } from "@angular/core"; import { Component } from "@angular/core";
import { ComponentFixture, TestBed } from "@angular/core/testing"; import { ComponentFixture, TestBed } from "@angular/core/testing";
import { By } from "@angular/platform-browser"; import { By } from "@angular/platform-browser";
import { ActivatedRoute } from "@angular/router";
import { mock } from "jest-mock-extended"; import { mock } from "jest-mock-extended";
import { of } from "rxjs"; import { of } from "rxjs";
@@ -8,9 +9,12 @@ import {} from "@bitwarden/web-vault/app/shared";
import { JslibModule } from "@bitwarden/angular/jslib.module"; import { JslibModule } from "@bitwarden/angular/jslib.module";
import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-tokens"; 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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { ThemeType } from "@bitwarden/common/platform/enums"; import { ThemeType } from "@bitwarden/common/platform/enums";
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; 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 { 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"; 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", () => { describe("IntegrationsComponent", () => {
let fixture: ComponentFixture<IntegrationsComponent>; let fixture: ComponentFixture<IntegrationsComponent>;
const mockOrgIntegrationApiService = mock<OrganizationIntegrationApiService>();
const activatedRouteMock = {
snapshot: { paramMap: { get: jest.fn() } },
};
const mockI18nService = mock<I18nService>();
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [IntegrationsComponent, MockHeaderComponent, MockNewMenuComponent], declarations: [IntegrationsComponent, MockHeaderComponent, MockNewMenuComponent],
imports: [JslibModule, IntegrationGridComponent, IntegrationCardComponent], imports: [JslibModule, IntegrationGridComponent, IntegrationCardComponent],
providers: [ providers: [
{ { provide: I18nService, useValue: mock<I18nService>() },
provide: I18nService, { provide: ThemeStateService, useValue: mock<ThemeStateService>() },
useValue: mock<I18nService>(), { provide: SYSTEM_THEME_OBSERVABLE, useValue: of(ThemeType.Light) },
}, { provide: ActivatedRoute, useValue: activatedRouteMock },
{ { provide: OrganizationIntegrationApiService, useValue: mockOrgIntegrationApiService },
provide: ThemeStateService, { provide: ToastService, useValue: mock<ToastService>() },
useValue: mock<ThemeStateService>(), { provide: I18nPipe, useValue: mock<I18nPipe>() },
}, { provide: I18nService, useValue: mockI18nService },
{
provide: SYSTEM_THEME_OBSERVABLE,
useValue: of(ThemeType.Light),
},
], ],
}).compileComponents(); }).compileComponents();
fixture = TestBed.createComponent(IntegrationsComponent); fixture = TestBed.createComponent(IntegrationsComponent);