mirror of
https://github.com/bitwarden/browser
synced 2025-12-16 08:13:42 +00:00
* Moved has-premium.guard under billing * Moved free-trial.ts to billing * Moved premium directives to billing * Moved families-policy.service.ts to billing * Moved trial initiation from auth to billing
337 lines
14 KiB
TypeScript
337 lines
14 KiB
TypeScript
// FIXME: Update this file to be type safe and remove this and next line
|
|
// @ts-strict-ignore
|
|
import { StepperSelectionEvent } from "@angular/cdk/stepper";
|
|
import { TitleCasePipe } from "@angular/common";
|
|
import { NO_ERRORS_SCHEMA } from "@angular/core";
|
|
import { ComponentFixture, fakeAsync, TestBed, tick } from "@angular/core/testing";
|
|
import { FormBuilder, UntypedFormBuilder } from "@angular/forms";
|
|
import { ActivatedRoute, Router } from "@angular/router";
|
|
import { RouterTestingModule } from "@angular/router/testing";
|
|
import { mock, MockProxy } from "jest-mock-extended";
|
|
import { BehaviorSubject, of } from "rxjs";
|
|
|
|
import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
|
|
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 { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
|
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
|
import { OrganizationBillingServiceAbstraction as OrganizationBillingService } from "@bitwarden/common/billing/abstractions/organization-billing.service";
|
|
import { PlanType } from "@bitwarden/common/billing/enums";
|
|
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
|
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
|
|
|
import { AcceptOrganizationInviteService } from "../../auth/organization-invite/accept-organization.service";
|
|
import { OrganizationInvite } from "../../auth/organization-invite/organization-invite";
|
|
import { RouterService } from "../../core";
|
|
import { SharedModule } from "../../shared";
|
|
|
|
import { TrialInitiationComponent } from "./trial-initiation.component";
|
|
import { VerticalStepperComponent } from "./vertical-stepper/vertical-stepper.component";
|
|
|
|
describe("TrialInitiationComponent", () => {
|
|
let component: TrialInitiationComponent;
|
|
let fixture: ComponentFixture<TrialInitiationComponent>;
|
|
const mockQueryParams = new BehaviorSubject<any>({ org: "enterprise" });
|
|
const testOrgId = "91329456-5b9f-44b3-9279-6bb9ee6a0974";
|
|
const formBuilder: FormBuilder = new FormBuilder();
|
|
let routerSpy: jest.SpyInstance;
|
|
|
|
let stateServiceMock: MockProxy<StateService>;
|
|
let policyApiServiceMock: MockProxy<PolicyApiServiceAbstraction>;
|
|
let policyServiceMock: MockProxy<PolicyService>;
|
|
let routerServiceMock: MockProxy<RouterService>;
|
|
let acceptOrgInviteServiceMock: MockProxy<AcceptOrganizationInviteService>;
|
|
let organizationBillingServiceMock: MockProxy<OrganizationBillingService>;
|
|
let configServiceMock: MockProxy<ConfigService>;
|
|
|
|
beforeEach(() => {
|
|
// only define services directly that we want to mock return values in this component
|
|
stateServiceMock = mock<StateService>();
|
|
policyApiServiceMock = mock<PolicyApiServiceAbstraction>();
|
|
policyServiceMock = mock<PolicyService>();
|
|
routerServiceMock = mock<RouterService>();
|
|
acceptOrgInviteServiceMock = mock<AcceptOrganizationInviteService>();
|
|
organizationBillingServiceMock = mock<OrganizationBillingService>();
|
|
configServiceMock = mock<ConfigService>();
|
|
|
|
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
TestBed.configureTestingModule({
|
|
imports: [
|
|
SharedModule,
|
|
RouterTestingModule.withRoutes([
|
|
{ path: "trial", component: TrialInitiationComponent },
|
|
{
|
|
path: `organizations/${testOrgId}/vault`,
|
|
component: BlankComponent,
|
|
},
|
|
{
|
|
path: `organizations/${testOrgId}/members`,
|
|
component: BlankComponent,
|
|
},
|
|
]),
|
|
],
|
|
declarations: [TrialInitiationComponent, I18nPipe],
|
|
providers: [
|
|
UntypedFormBuilder,
|
|
{
|
|
provide: ActivatedRoute,
|
|
useValue: {
|
|
queryParams: mockQueryParams.asObservable(),
|
|
},
|
|
},
|
|
{ provide: StateService, useValue: stateServiceMock },
|
|
{ provide: PolicyService, useValue: policyServiceMock },
|
|
{ provide: PolicyApiServiceAbstraction, useValue: policyApiServiceMock },
|
|
{ provide: LogService, useValue: mock<LogService>() },
|
|
{ provide: I18nService, useValue: mock<I18nService>() },
|
|
{ provide: TitleCasePipe, useValue: mock<TitleCasePipe>() },
|
|
{
|
|
provide: VerticalStepperComponent,
|
|
useClass: VerticalStepperStubComponent,
|
|
},
|
|
{
|
|
provide: RouterService,
|
|
useValue: routerServiceMock,
|
|
},
|
|
{
|
|
provide: AcceptOrganizationInviteService,
|
|
useValue: acceptOrgInviteServiceMock,
|
|
},
|
|
{
|
|
provide: OrganizationBillingService,
|
|
useValue: organizationBillingServiceMock,
|
|
},
|
|
{
|
|
provide: ConfigService,
|
|
useValue: configServiceMock,
|
|
},
|
|
],
|
|
schemas: [NO_ERRORS_SCHEMA], // Allows child components to be ignored (such as register component)
|
|
}).compileComponents();
|
|
});
|
|
|
|
beforeEach(() => {
|
|
fixture = TestBed.createComponent(TrialInitiationComponent);
|
|
component = fixture.componentInstance;
|
|
fixture.detectChanges();
|
|
});
|
|
|
|
it("should create", () => {
|
|
expect(component).toBeTruthy();
|
|
});
|
|
|
|
// These tests demonstrate mocking service calls
|
|
describe("onInit() enforcedPolicyOptions", () => {
|
|
it("should not set enforcedPolicyOptions if there isn't an org invite in deep linked url", async () => {
|
|
acceptOrgInviteServiceMock.getOrganizationInvite.mockResolvedValueOnce(null);
|
|
// Need to recreate component with new service mock
|
|
fixture = TestBed.createComponent(TrialInitiationComponent);
|
|
component = fixture.componentInstance;
|
|
await component.ngOnInit();
|
|
|
|
expect(component.enforcedPolicyOptions).toBe(undefined);
|
|
});
|
|
it("should set enforcedPolicyOptions if the deep linked url has an org invite", async () => {
|
|
// Set up service method mocks
|
|
acceptOrgInviteServiceMock.getOrganizationInvite.mockResolvedValueOnce({
|
|
organizationId: testOrgId,
|
|
token: "token",
|
|
email: "testEmail",
|
|
organizationUserId: "123",
|
|
} as OrganizationInvite);
|
|
policyApiServiceMock.getPoliciesByToken.mockReturnValueOnce(
|
|
Promise.resolve([
|
|
{
|
|
id: "345",
|
|
organizationId: testOrgId,
|
|
type: 1,
|
|
data: {
|
|
minComplexity: 4,
|
|
minLength: 10,
|
|
requireLower: null,
|
|
requireNumbers: null,
|
|
requireSpecial: null,
|
|
requireUpper: null,
|
|
},
|
|
enabled: true,
|
|
},
|
|
] as Policy[]),
|
|
);
|
|
policyServiceMock.masterPasswordPolicyOptions$.mockReturnValue(
|
|
of({
|
|
minComplexity: 4,
|
|
minLength: 10,
|
|
requireLower: null,
|
|
requireNumbers: null,
|
|
requireSpecial: null,
|
|
requireUpper: null,
|
|
} as MasterPasswordPolicyOptions),
|
|
);
|
|
|
|
// Need to recreate component with new service mocks
|
|
fixture = TestBed.createComponent(TrialInitiationComponent);
|
|
component = fixture.componentInstance;
|
|
await component.ngOnInit();
|
|
expect(component.enforcedPolicyOptions).toMatchObject({
|
|
minComplexity: 4,
|
|
minLength: 10,
|
|
requireLower: null,
|
|
requireNumbers: null,
|
|
requireSpecial: null,
|
|
requireUpper: null,
|
|
});
|
|
});
|
|
});
|
|
|
|
// These tests demonstrate route params
|
|
describe("Route params", () => {
|
|
it("should set org variable to be enterprise and plan to EnterpriseAnnually if org param is enterprise", fakeAsync(() => {
|
|
mockQueryParams.next({ org: "enterprise" });
|
|
tick(); // wait for resolution
|
|
fixture = TestBed.createComponent(TrialInitiationComponent);
|
|
component = fixture.componentInstance;
|
|
fixture.detectChanges();
|
|
expect(component.org).toBe("enterprise");
|
|
expect(component.plan).toBe(PlanType.EnterpriseAnnually);
|
|
}));
|
|
it("should not set org variable if no org param is provided", fakeAsync(() => {
|
|
mockQueryParams.next({});
|
|
tick(); // wait for resolution
|
|
fixture = TestBed.createComponent(TrialInitiationComponent);
|
|
component = fixture.componentInstance;
|
|
fixture.detectChanges();
|
|
expect(component.org).toBe("");
|
|
expect(component.accountCreateOnly).toBe(true);
|
|
}));
|
|
it("should not set the org if org param is invalid ", fakeAsync(async () => {
|
|
mockQueryParams.next({ org: "hahahaha" });
|
|
tick(); // wait for resolution
|
|
fixture = TestBed.createComponent(TrialInitiationComponent);
|
|
component = fixture.componentInstance;
|
|
fixture.detectChanges();
|
|
expect(component.org).toBe("");
|
|
expect(component.accountCreateOnly).toBe(true);
|
|
}));
|
|
it("should set the layout variable if layout param is valid ", fakeAsync(async () => {
|
|
mockQueryParams.next({ layout: "teams1" });
|
|
tick(); // wait for resolution
|
|
fixture = TestBed.createComponent(TrialInitiationComponent);
|
|
component = fixture.componentInstance;
|
|
fixture.detectChanges();
|
|
expect(component.layout).toBe("teams1");
|
|
expect(component.accountCreateOnly).toBe(false);
|
|
}));
|
|
it("should not set the layout variable and leave as 'default' if layout param is invalid ", fakeAsync(async () => {
|
|
mockQueryParams.next({ layout: "asdfasdf" });
|
|
tick(); // wait for resolution
|
|
fixture = TestBed.createComponent(TrialInitiationComponent);
|
|
component = fixture.componentInstance;
|
|
fixture.detectChanges();
|
|
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
component.ngOnInit();
|
|
expect(component.layout).toBe("default");
|
|
expect(component.accountCreateOnly).toBe(true);
|
|
}));
|
|
});
|
|
|
|
// These tests demonstrate the use of a stub component
|
|
describe("createAccount()", () => {
|
|
beforeEach(() => {
|
|
component.verticalStepper = TestBed.createComponent(VerticalStepperStubComponent)
|
|
.componentInstance as VerticalStepperComponent;
|
|
});
|
|
|
|
it("should set email and call verticalStepper.next()", fakeAsync(() => {
|
|
const verticalStepperNext = jest.spyOn(component.verticalStepper, "next");
|
|
component.createdAccount("test@email.com");
|
|
expect(verticalStepperNext).toHaveBeenCalled();
|
|
expect(component.email).toBe("test@email.com");
|
|
}));
|
|
});
|
|
|
|
describe("billingSuccess()", () => {
|
|
beforeEach(() => {
|
|
component.verticalStepper = TestBed.createComponent(VerticalStepperStubComponent)
|
|
.componentInstance as VerticalStepperComponent;
|
|
});
|
|
|
|
it("should set orgId and call verticalStepper.next()", () => {
|
|
const verticalStepperNext = jest.spyOn(component.verticalStepper, "next");
|
|
component.billingSuccess({ orgId: testOrgId });
|
|
expect(verticalStepperNext).toHaveBeenCalled();
|
|
expect(component.orgId).toBe(testOrgId);
|
|
});
|
|
});
|
|
|
|
describe("stepSelectionChange()", () => {
|
|
beforeEach(() => {
|
|
component.verticalStepper = TestBed.createComponent(VerticalStepperStubComponent)
|
|
.componentInstance as VerticalStepperComponent;
|
|
});
|
|
|
|
it("on step 2 should show organization copy text", () => {
|
|
component.stepSelectionChange({
|
|
selectedIndex: 1,
|
|
previouslySelectedIndex: 0,
|
|
} as StepperSelectionEvent);
|
|
|
|
expect(component.orgInfoSubLabel).toContain("Enter your");
|
|
expect(component.orgInfoSubLabel).toContain(" organization information");
|
|
});
|
|
it("going from step 2 to 3 should set the orgInforSubLabel to be the Org name from orgInfoFormGroup", () => {
|
|
component.orgInfoFormGroup = formBuilder.group({
|
|
name: ["Hooli"],
|
|
email: [""],
|
|
});
|
|
component.stepSelectionChange({
|
|
selectedIndex: 2,
|
|
previouslySelectedIndex: 1,
|
|
} as StepperSelectionEvent);
|
|
|
|
expect(component.orgInfoSubLabel).toContain("Hooli");
|
|
});
|
|
});
|
|
|
|
describe("previousStep()", () => {
|
|
beforeEach(() => {
|
|
component.verticalStepper = TestBed.createComponent(VerticalStepperStubComponent)
|
|
.componentInstance as VerticalStepperComponent;
|
|
});
|
|
|
|
it("should call verticalStepper.previous()", fakeAsync(() => {
|
|
const verticalStepperPrevious = jest.spyOn(component.verticalStepper, "previous");
|
|
component.previousStep();
|
|
expect(verticalStepperPrevious).toHaveBeenCalled();
|
|
}));
|
|
});
|
|
|
|
// These tests demonstrate router navigation
|
|
describe("navigation methods", () => {
|
|
beforeEach(() => {
|
|
component.orgId = testOrgId;
|
|
const router = TestBed.inject(Router);
|
|
fixture.detectChanges();
|
|
routerSpy = jest.spyOn(router, "navigate");
|
|
});
|
|
describe("navigateToOrgVault", () => {
|
|
it("should call verticalStepper.previous()", fakeAsync(() => {
|
|
component.navigateToOrgVault();
|
|
expect(routerSpy).toHaveBeenCalledWith(["organizations", testOrgId, "vault"]);
|
|
}));
|
|
});
|
|
describe("navigateToOrgVault", () => {
|
|
it("should call verticalStepper.previous()", fakeAsync(() => {
|
|
component.navigateToOrgInvite();
|
|
expect(routerSpy).toHaveBeenCalledWith(["organizations", testOrgId, "members"]);
|
|
}));
|
|
});
|
|
});
|
|
});
|
|
|
|
export class VerticalStepperStubComponent extends VerticalStepperComponent {}
|
|
export class BlankComponent {} // For router tests
|