1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-13 23:13:36 +00:00

Rollback single org enablement when auto confirm enablement fails.

This commit is contained in:
Jared McCannon
2026-01-26 14:24:46 -06:00
parent ad577860be
commit 3695b57bba
2 changed files with 292 additions and 15 deletions

View File

@@ -0,0 +1,270 @@
import { NO_ERRORS_SCHEMA } from "@angular/core";
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { FormBuilder } from "@angular/forms";
import { Router } from "@angular/router";
import { mock, MockProxy } from "jest-mock-extended";
import { of } from "rxjs";
import { AutomaticUserConfirmationService } from "@bitwarden/auto-confirm";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
import { OrganizationId, UserId } from "@bitwarden/common/types/guid";
import { DIALOG_DATA, DialogRef, ToastService } from "@bitwarden/components";
import { newGuid } from "@bitwarden/guid";
import { KeyService } from "@bitwarden/key-management";
import {
AutoConfirmPolicyDialogComponent,
AutoConfirmPolicyDialogData,
} from "./auto-confirm-edit-policy-dialog.component";
describe("AutoConfirmPolicyDialogComponent", () => {
let component: AutoConfirmPolicyDialogComponent;
let fixture: ComponentFixture<AutoConfirmPolicyDialogComponent>;
let mockPolicyApiService: MockProxy<PolicyApiServiceAbstraction>;
let mockAccountService: FakeAccountService;
let mockOrganizationService: MockProxy<OrganizationService>;
let mockPolicyService: MockProxy<PolicyService>;
let mockRouter: MockProxy<Router>;
let mockAutoConfirmService: MockProxy<AutomaticUserConfirmationService>;
let mockDialogRef: MockProxy<DialogRef>;
let mockToastService: MockProxy<ToastService>;
let mockI18nService: MockProxy<I18nService>;
let mockKeyService: MockProxy<KeyService>;
const mockUserId = newGuid() as UserId;
const mockOrgId = newGuid() as OrganizationId;
const mockDialogData: AutoConfirmPolicyDialogData = {
organizationId: mockOrgId,
policy: {
name: "autoConfirm",
description: "Auto Confirm Policy",
type: PolicyType.AutoConfirm,
component: {} as any,
showDescription: true,
display$: () => of(true),
},
firstTimeDialog: false,
};
const mockOrg = {
id: mockOrgId,
name: "Test Organization",
enabled: true,
isAdmin: true,
canManagePolicies: true,
} as Organization;
beforeEach(async () => {
mockPolicyApiService = mock<PolicyApiServiceAbstraction>();
mockAccountService = mockAccountServiceWith(mockUserId);
mockOrganizationService = mock<OrganizationService>();
mockPolicyService = mock<PolicyService>();
mockRouter = mock<Router>();
mockAutoConfirmService = mock<AutomaticUserConfirmationService>();
mockDialogRef = mock<DialogRef>();
mockToastService = mock<ToastService>();
mockI18nService = mock<I18nService>();
mockKeyService = mock<KeyService>();
mockPolicyService.policies$.mockReturnValue(of([]));
mockOrganizationService.organizations$.mockReturnValue(of([mockOrg]));
await TestBed.configureTestingModule({
imports: [AutoConfirmPolicyDialogComponent],
providers: [
FormBuilder,
{ provide: DIALOG_DATA, useValue: mockDialogData },
{ provide: AccountService, useValue: mockAccountService },
{ provide: PolicyApiServiceAbstraction, useValue: mockPolicyApiService },
{ provide: I18nService, useValue: mockI18nService },
{ provide: DialogRef, useValue: mockDialogRef },
{ provide: ToastService, useValue: mockToastService },
{ provide: KeyService, useValue: mockKeyService },
{ provide: OrganizationService, useValue: mockOrganizationService },
{ provide: PolicyService, useValue: mockPolicyService },
{ provide: Router, useValue: mockRouter },
{ provide: AutomaticUserConfirmationService, useValue: mockAutoConfirmService },
],
schemas: [NO_ERRORS_SCHEMA],
})
.overrideComponent(AutoConfirmPolicyDialogComponent, {
set: { template: "<div></div>" },
})
.compileComponents();
fixture = TestBed.createComponent(AutoConfirmPolicyDialogComponent);
component = fixture.componentInstance;
});
afterEach(() => {
jest.resetAllMocks();
});
it("should create", () => {
expect(component).toBeTruthy();
});
describe("handleSubmit", () => {
beforeEach(() => {
// Mock the policyComponent
component.policyComponent = {
buildRequest: jest.fn().mockResolvedValue({ enabled: true, data: null }),
enabled: { value: true },
setSingleOrgEnabled: jest.fn(),
} as any;
mockAutoConfirmService.configuration$.mockReturnValue(
of({ enabled: false, showSetupDialog: true, showBrowserNotification: undefined }),
);
mockAutoConfirmService.upsert.mockResolvedValue(undefined);
mockI18nService.t.mockReturnValue("Policy updated");
});
it("should enable SingleOrg policy when it was not already enabled", async () => {
mockPolicyApiService.putPolicyVNext.mockResolvedValue({} as any);
// Call handleSubmit with singleOrgEnabled = false (meaning it needs to be enabled)
await component["handleSubmit"](false);
// First call should be SingleOrg enable
expect(mockPolicyApiService.putPolicyVNext).toHaveBeenNthCalledWith(
1,
mockOrgId,
PolicyType.SingleOrg,
{ policy: { enabled: true, data: null } },
);
});
it("should not enable SingleOrg policy when it was already enabled", async () => {
mockPolicyApiService.putPolicyVNext.mockResolvedValue({} as any);
// Call handleSubmit with singleOrgEnabled = true (meaning it's already enabled)
await component["handleSubmit"](true);
// Should only call putPolicyVNext once (for AutoConfirm, not SingleOrg)
expect(mockPolicyApiService.putPolicyVNext).toHaveBeenCalledTimes(1);
expect(mockPolicyApiService.putPolicyVNext).toHaveBeenCalledWith(
mockOrgId,
PolicyType.AutoConfirm,
{ policy: { enabled: true, data: null } },
);
});
it("should rollback SingleOrg policy when AutoConfirm fails and SingleOrg was enabled during action", async () => {
const autoConfirmError = new Error("AutoConfirm failed");
// First call (SingleOrg enable) succeeds, second call (AutoConfirm) fails, third call (SingleOrg rollback) succeeds
mockPolicyApiService.putPolicyVNext
.mockResolvedValueOnce({} as any) // SingleOrg enable
.mockRejectedValueOnce(autoConfirmError) // AutoConfirm fails
.mockResolvedValueOnce({} as any); // SingleOrg rollback
await expect(component["handleSubmit"](false)).rejects.toThrow("AutoConfirm failed");
// Verify: SingleOrg enabled, AutoConfirm attempted, SingleOrg rolled back
expect(mockPolicyApiService.putPolicyVNext).toHaveBeenCalledTimes(3);
expect(mockPolicyApiService.putPolicyVNext).toHaveBeenNthCalledWith(
1,
mockOrgId,
PolicyType.SingleOrg,
{ policy: { enabled: true, data: null } },
);
expect(mockPolicyApiService.putPolicyVNext).toHaveBeenNthCalledWith(
2,
mockOrgId,
PolicyType.AutoConfirm,
{ policy: { enabled: true, data: null } },
);
expect(mockPolicyApiService.putPolicyVNext).toHaveBeenNthCalledWith(
3,
mockOrgId,
PolicyType.SingleOrg,
{ policy: { enabled: false, data: null } },
);
});
it("should not rollback SingleOrg policy when AutoConfirm fails but SingleOrg was already enabled", async () => {
const autoConfirmError = new Error("AutoConfirm failed");
// AutoConfirm call fails (SingleOrg was already enabled, so no SingleOrg calls)
mockPolicyApiService.putPolicyVNext.mockRejectedValue(autoConfirmError);
await expect(component["handleSubmit"](true)).rejects.toThrow("AutoConfirm failed");
// Verify only AutoConfirm was called (no SingleOrg enable/rollback)
expect(mockPolicyApiService.putPolicyVNext).toHaveBeenCalledTimes(1);
expect(mockPolicyApiService.putPolicyVNext).toHaveBeenCalledWith(
mockOrgId,
PolicyType.AutoConfirm,
{ policy: { enabled: true, data: null } },
);
});
it("should keep both policies enabled when both submissions succeed", async () => {
mockPolicyApiService.putPolicyVNext.mockResolvedValue({} as any);
await component["handleSubmit"](false);
// Verify two calls: SingleOrg enable and AutoConfirm enable
expect(mockPolicyApiService.putPolicyVNext).toHaveBeenCalledTimes(2);
expect(mockPolicyApiService.putPolicyVNext).toHaveBeenNthCalledWith(
1,
mockOrgId,
PolicyType.SingleOrg,
{ policy: { enabled: true, data: null } },
);
expect(mockPolicyApiService.putPolicyVNext).toHaveBeenNthCalledWith(
2,
mockOrgId,
PolicyType.AutoConfirm,
{ policy: { enabled: true, data: null } },
);
});
it("should re-throw the error after rollback", async () => {
const autoConfirmError = new Error("Network error");
mockPolicyApiService.putPolicyVNext
.mockResolvedValueOnce({} as any) // SingleOrg enable
.mockRejectedValueOnce(autoConfirmError) // AutoConfirm fails
.mockResolvedValueOnce({} as any); // SingleOrg rollback
await expect(component["handleSubmit"](false)).rejects.toThrow("Network error");
});
});
describe("setSingleOrgPolicy", () => {
it("should call putPolicyVNext with enabled: true when enabling", async () => {
mockPolicyApiService.putPolicyVNext.mockResolvedValue({} as any);
await component["setSingleOrgPolicy"](true);
expect(mockPolicyApiService.putPolicyVNext).toHaveBeenCalledWith(
mockOrgId,
PolicyType.SingleOrg,
{ policy: { enabled: true, data: null } },
);
});
it("should call putPolicyVNext with enabled: false when disabling", async () => {
mockPolicyApiService.putPolicyVNext.mockResolvedValue({} as any);
await component["setSingleOrgPolicy"](false);
expect(mockPolicyApiService.putPolicyVNext).toHaveBeenCalledWith(
mockOrgId,
PolicyType.SingleOrg,
{ policy: { enabled: false, data: null } },
);
});
});
});

View File

@@ -181,10 +181,21 @@ export class AutoConfirmPolicyDialogComponent
}
private async handleSubmit(singleOrgEnabled: boolean) {
if (!singleOrgEnabled) {
await this.submitSingleOrg();
const enabledSingleOrgDuringAction = !singleOrgEnabled;
if (enabledSingleOrgDuringAction) {
await this.setSingleOrgPolicy(true);
}
try {
await this.submitAutoConfirm();
} catch (error) {
// Roll back SingleOrg if we enabled it during this action
if (enabledSingleOrgDuringAction) {
await this.setSingleOrgPolicy(false);
}
throw error;
}
await this.submitAutoConfirm();
}
/**
@@ -198,11 +209,9 @@ export class AutoConfirmPolicyDialogComponent
const autoConfirmRequest = await this.policyComponent.buildRequest();
await this.policyApiService.putPolicy(
this.data.organizationId,
this.data.policy.type,
autoConfirmRequest,
);
await this.policyApiService.putPolicyVNext(this.data.organizationId, this.data.policy.type, {
policy: autoConfirmRequest,
});
const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
@@ -225,17 +234,15 @@ export class AutoConfirmPolicyDialogComponent
}
}
private async submitSingleOrg(): Promise<void> {
private async setSingleOrgPolicy(enabled: boolean): Promise<void> {
const singleOrgRequest: PolicyRequest = {
enabled: true,
enabled,
data: null,
};
await this.policyApiService.putPolicyVNext(
this.data.organizationId,
PolicyType.SingleOrg,
singleOrgRequest,
);
await this.policyApiService.putPolicyVNext(this.data.organizationId, PolicyType.SingleOrg, {
policy: singleOrgRequest,
});
}
private async openBrowserExtension() {