From 2f29903136ed6ecc6571bd11990d3cea3ec7fd6e Mon Sep 17 00:00:00 2001 From: Alec Rippberger Date: Tue, 15 Oct 2024 21:40:48 -0500 Subject: [PATCH] Clean up and flush out register form tests. --- .../register-form.component.spec.ts | 243 ++++++++++++++---- .../src/auth/components/register.component.ts | 1 - 2 files changed, 192 insertions(+), 52 deletions(-) diff --git a/apps/web/src/app/auth/register-form/register-form.component.spec.ts b/apps/web/src/app/auth/register-form/register-form.component.spec.ts index f2d38e9284..14e58dad4b 100644 --- a/apps/web/src/app/auth/register-form/register-form.component.spec.ts +++ b/apps/web/src/app/auth/register-form/register-form.component.spec.ts @@ -1,9 +1,11 @@ import { ComponentFixture, TestBed } from "@angular/core/testing"; import { ReactiveFormsModule } from "@angular/forms"; import { RouterTestingModule } from "@angular/router/testing"; +import { mock } from "jest-mock-extended"; import { of } from "rxjs"; import { OrganizationUserApiService } from "@bitwarden/admin-console/common"; +import { RegisterComponent as BaseRegisterComponent } from "@bitwarden/angular/auth/components/register.component"; import { FormValidationErrorsService } from "@bitwarden/angular/platform/abstractions/form-validation-errors.service"; import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; @@ -11,8 +13,10 @@ import { AuditService } from "@bitwarden/common/abstractions/audit.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"; 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 { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { RegisterRequest } from "@bitwarden/common/models/request/register.request"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; @@ -23,59 +27,72 @@ import { StateService } from "@bitwarden/common/platform/abstractions/state.serv import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { GlobalStateProvider, StateProvider } from "@bitwarden/common/platform/state"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; -import { DialogService, ToastService } from "@bitwarden/components"; +import { ToastService } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { SharedModule } from "../../shared"; import { AcceptOrganizationInviteService } from "../organization-invite/accept-organization.service"; +import { OrganizationInvite } from "../organization-invite/organization-invite"; import { RegisterFormComponent } from "./register-form.component"; -// Mock I18nService -class MockI18nService { - t(key: string): string { - return key; - } -} - -// Mock PlatformUtilsService -class MockPlatformUtilsService { - isSelfHost(): boolean { - return false; - } -} - -// Mock ValidationService -class MockValidationService {} - -// Mock PasswordStrengthServiceAbstraction -class MockPasswordStrengthServiceAbstraction { - getPasswordStrength(password: string, email: string, name: string[]): any { - return { score: 3 }; - } -} describe("RegisterFormComponent", () => { let component: RegisterFormComponent; let fixture: ComponentFixture; + let policyServiceMock: jest.Mocked; + let toastServiceMock: jest.Mocked; + let acceptOrgInviteServiceMock: jest.Mocked; + let logServiceMock: jest.Mocked; beforeEach(async () => { - const authServiceMock = { - authStatuses$: of([]), - }; - const encryptServiceMock = {}; - const policyApiServiceMock = {}; - const organizationApiServiceMock = {}; - const organizationUserApiServiceMock = {}; - const globalStateProviderMock = { + const authServiceMock = mock({ authStatuses$: of({}) }); + const encryptServiceMock = mock(); + const policyApiServiceMock = mock(); + const organizationApiServiceMock = mock(); + const organizationUserApiServiceMock = mock(); + const globalStateProviderMock = mock({ get: jest.fn().mockReturnValue(of({})), - }; - const accountServiceMock = {}; - const stateProviderMock = { - getGlobal: jest.fn().mockReturnValue({ - state$: of({}), - }), - }; + }); + const accountServiceMock = mock(); + const stateProviderMock = mock({ + getGlobal: jest.fn().mockReturnValue({ state$: of({}) }), + }); + const i18nServiceMock = mock(); + i18nServiceMock.t.mockImplementation((key: string) => key); + const platformUtilsServiceMock = mock(); + platformUtilsServiceMock.isSelfHost.mockReturnValue(false); + const validationServiceMock = mock(); + const passwordStrengthServiceMock = mock(); + passwordStrengthServiceMock.getPasswordStrength.mockReturnValue({ + score: 3, + guesses: 1000, + guesses_log10: 3, + crack_times_seconds: { + online_throttling_100_per_hour: 36000, + online_no_throttling_10_per_second: 100, + offline_slow_hashing_1e4_per_second: 0.1, + offline_fast_hashing_1e10_per_second: 0.0001, + }, + crack_times_display: { + online_throttling_100_per_hour: "10 hours", + online_no_throttling_10_per_second: "1 minute", + offline_slow_hashing_1e4_per_second: "0.1 seconds", + offline_fast_hashing_1e10_per_second: "less than a second", + }, + feedback: { warning: "", suggestions: [] }, + calc_time: 100, + sequence: [], + }); + const toastServiceMockTemp = mock(); + logServiceMock = mock(); + const cryptoServiceMock = mock({ + makeMasterKey: jest.fn().mockResolvedValue({} as any), + }); + policyServiceMock = mock(); + toastServiceMock = mock(); + acceptOrgInviteServiceMock = mock(); + logServiceMock = mock(); await TestBed.configureTestingModule({ imports: [ReactiveFormsModule, RouterTestingModule, SharedModule], @@ -89,26 +106,26 @@ describe("RegisterFormComponent", () => { { provide: GlobalStateProvider, useValue: globalStateProviderMock }, { provide: AccountService, useValue: accountServiceMock }, { provide: StateProvider, useValue: stateProviderMock }, - { provide: I18nService, useClass: MockI18nService }, - { provide: PlatformUtilsService, useClass: MockPlatformUtilsService }, - { provide: ValidationService, useClass: MockValidationService }, - { - provide: PasswordStrengthServiceAbstraction, - useClass: MockPasswordStrengthServiceAbstraction, - }, + { provide: I18nService, useValue: i18nServiceMock }, + { provide: PlatformUtilsService, useValue: platformUtilsServiceMock }, + { provide: ValidationService, useValue: validationServiceMock }, + { provide: PasswordStrengthServiceAbstraction, useValue: passwordStrengthServiceMock }, + { provide: ToastService, useValue: toastServiceMockTemp }, + { provide: LogService, useValue: logServiceMock }, + { provide: CryptoService, useValue: cryptoServiceMock }, FormValidationErrorsService, LoginStrategyServiceAbstraction, ApiService, AuditService, PolicyService, - CryptoService, EnvironmentService, - LogService, StateService, - DialogService, - ToastService, PasswordGenerationServiceAbstraction, AcceptOrganizationInviteService, + { provide: PolicyService, useValue: policyServiceMock }, + { provide: ToastService, useValue: toastServiceMock }, + { provide: AcceptOrganizationInviteService, useValue: acceptOrgInviteServiceMock }, + { provide: LogService, useValue: logServiceMock }, ], }).compileComponents(); @@ -116,7 +133,131 @@ describe("RegisterFormComponent", () => { component = fixture.componentInstance; }); - it("creates without error", () => { + it("creates component", () => { expect(component).toBeTruthy(); }); + + describe("ngOnInit", () => { + it("sets email from queryParamEmail", async () => { + component.queryParamEmail = "test@bitwarden.com"; + await component.ngOnInit(); + expect(component.formGroup.get("email").value).toBe("test@bitwarden.com"); + }); + + it("sets characterMinimumMessage based on enforcedPolicyOptions", async () => { + const testCases: Array<{ + enforcedPolicyOptions: MasterPasswordPolicyOptions | null; + minimumLength?: number; + expected: string; + }> = [ + { enforcedPolicyOptions: null, minimumLength: 8, expected: "characterMinimum" }, + { enforcedPolicyOptions: { minLength: 10 } as MasterPasswordPolicyOptions, expected: "" }, + ]; + + for (const { enforcedPolicyOptions, minimumLength, expected } of testCases) { + component.enforcedPolicyOptions = enforcedPolicyOptions; + component.minimumLength = minimumLength; + await component.ngOnInit(); + expect(component.characterMinimumMessage).toBe(expected); + } + }); + }); + + describe("submit", () => { + it("shows error toast when password policy is not met", async () => { + setupPasswordPolicyTest(false, 2, "weak"); + await component.submit(); + expect(toastServiceMock.showToast).toHaveBeenCalledWith({ + variant: "error", + title: "errorOccurred", + message: "masterPasswordPolicyRequirementsNotMet", + }); + }); + + it("calls super.submit when password policy is met", async () => { + const submitSpy = setupPasswordPolicyTest(true, 4, "superStrongPassword123!"); + await component.submit(); + expect(submitSpy).toHaveBeenCalledWith(false); + }); + }); + + describe("modifyRegisterRequest", () => { + it("adds organization invite details to register request when invite exists", async () => { + const request = { + email: "test@bitwarden.com", + masterPasswordHash: "hashedPassword", + masterPasswordHint: "hint", + key: "encryptedKey", + kdf: 0, + kdfIterations: 100000, + referenceData: null, + captchaResponse: null, + keys: { + publicKey: "mockPublicKey", + encryptedPrivateKey: "mockEncryptedPrivateKey", + }, + token: null, + organizationUserId: null, + name: null, + } as RegisterRequest; + const orgInvite: OrganizationInvite = { + organizationUserId: "123", + token: "abc123", + email: "test@bitwarden.com", + initOrganization: false, + orgSsoIdentifier: null, + orgUserHasExistingUser: false, + organizationId: "456", + organizationName: "Test Org", + }; + acceptOrgInviteServiceMock.getOrganizationInvite.mockResolvedValue(orgInvite); + + await component["modifyRegisterRequest"](request); + + expect(request.token).toBe(orgInvite.token); + expect(request.organizationUserId).toBe(orgInvite.organizationUserId); + }); + + it("does not modify request when no organization invite exists", async () => { + const request = { + email: "test@bitwarden.com", + masterPasswordHash: "hashedPassword", + masterPasswordHint: "hint", + key: "encryptedKey", + kdf: 0, + kdfIterations: 100000, + referenceData: null, + captchaResponse: null, + keys: { + publicKey: "mockPublicKey", + encryptedPrivateKey: "mockEncryptedPrivateKey", + }, + token: null, + organizationUserId: null, + name: null, + } as RegisterRequest; + acceptOrgInviteServiceMock.getOrganizationInvite.mockResolvedValue(null); + + await component["modifyRegisterRequest"](request); + + expect(request.token).toBeNull(); + expect(request.organizationUserId).toBeNull(); + }); + }); + + /** + * Sets up the password policy test. + * + * @param policyMet - Whether the password policy is met. + * @param passwordScore - The score of the password. + * @param password - The password to set. + * @returns The spy on the submit method. + */ + function setupPasswordPolicyTest(policyMet: boolean, passwordScore: number, password: string) { + policyServiceMock.evaluateMasterPassword.mockReturnValue(policyMet); + component.enforcedPolicyOptions = { minLength: 10 } as MasterPasswordPolicyOptions; + component.passwordStrengthResult = { score: passwordScore }; + component.formGroup.patchValue({ masterPassword: password }); + return jest.spyOn(BaseRegisterComponent.prototype, "submit"); + } }); diff --git a/libs/angular/src/auth/components/register.component.ts b/libs/angular/src/auth/components/register.component.ts index 8f6e8f4f06..60adcba4d9 100644 --- a/libs/angular/src/auth/components/register.component.ts +++ b/libs/angular/src/auth/components/register.component.ts @@ -78,7 +78,6 @@ export class RegisterComponent extends CaptchaProtectedComponent implements OnIn protected captchaBypassToken: string = null; - // allows for extending classes to modify the register request before sending // allows for extending classes to modify the register request before sending // currently used by web to add organization invitation details protected modifyRegisterRequest: (request: RegisterRequest) => Promise;