import { TestBed } from "@angular/core/testing"; import { Router } from "@angular/router"; import { RouterTestingModule } from "@angular/router/testing"; import { MockProxy, mock } from "jest-mock-extended"; import { BehaviorSubject, of } from "rxjs"; import { EmptyComponent } from "@bitwarden/angular/platform/guard/feature-flag.guard.spec"; import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { ClientType } from "@bitwarden/common/enums"; import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction"; import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service"; import { KeyConnectorDomainConfirmation } from "@bitwarden/common/key-management/key-connector/models/key-connector-domain-confirmation"; import { VaultTimeoutSettingsService } from "@bitwarden/common/key-management/vault-timeout"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { mockAccountInfoWith } from "@bitwarden/common/spec"; import { UserId } from "@bitwarden/common/types/guid"; import { KeyService } from "@bitwarden/key-management"; import { lockGuard } from "./lock.guard"; interface SetupParams { authStatus: AuthenticationStatus; canLock?: boolean; clientType?: ClientType; everHadUserKey?: boolean; supportsDeviceTrust?: boolean; hasMasterPassword?: boolean; } describe("lockGuard", () => { const keyConnectorService = mock(); const setup = (setupParams: SetupParams) => { const authService: MockProxy = mock(); authService.authStatusFor$.mockReturnValue(of(setupParams.authStatus)); const vaultTimeoutSettingsService: MockProxy = mock(); vaultTimeoutSettingsService.canLock.mockResolvedValue(setupParams.canLock ?? true); const keyService: MockProxy = mock(); keyService.everHadUserKey$.mockReturnValue(of(setupParams.everHadUserKey ?? true)); const platformUtilService: MockProxy = mock(); platformUtilService.getClientType.mockReturnValue(setupParams.clientType ?? ClientType.Web); const messagingService: MockProxy = mock(); const deviceTrustService: MockProxy = mock(); deviceTrustService.supportsDeviceTrust$ = of(setupParams.supportsDeviceTrust ?? false); const userVerificationService: MockProxy = mock(); userVerificationService.hasMasterPassword.mockResolvedValue( setupParams.hasMasterPassword ?? true, ); keyConnectorService.requiresDomainConfirmation$.mockReturnValue(of(null)); const accountService: MockProxy = mock(); const activeAccountSubject = new BehaviorSubject(null); accountService.activeAccount$ = activeAccountSubject; activeAccountSubject.next({ id: "test-id" as UserId, ...mockAccountInfoWith({ name: "Test User 1", email: "test@email.com", }), }); const testBed = TestBed.configureTestingModule({ imports: [ RouterTestingModule.withRoutes([ { path: "", component: EmptyComponent }, { path: "lock", component: EmptyComponent, canActivate: [lockGuard()] }, { path: "non-lock-route", component: EmptyComponent }, { path: "confirm-key-connector-domain", component: EmptyComponent }, ]), ], providers: [ { provide: AuthService, useValue: authService }, { provide: MessagingService, useValue: messagingService }, { provide: AccountService, useValue: accountService }, { provide: VaultTimeoutSettingsService, useValue: vaultTimeoutSettingsService }, { provide: KeyService, useValue: keyService }, { provide: PlatformUtilsService, useValue: platformUtilService }, { provide: DeviceTrustServiceAbstraction, useValue: deviceTrustService }, { provide: UserVerificationService, useValue: userVerificationService }, { provide: KeyConnectorService, useValue: keyConnectorService }, ], }); return { router: testBed.inject(Router), messagingService, }; }; it("should be created", () => { const { router } = setup({ authStatus: AuthenticationStatus.Locked, }); expect(router).toBeTruthy(); }); it("should redirect to the root route when the user is Unlocked", async () => { const { router } = setup({ authStatus: AuthenticationStatus.Unlocked, }); await router.navigate(["lock"]); expect(router.url).toBe("/"); }); it("should redirect to the root route when the user is LoggedOut", async () => { const { router } = setup({ authStatus: AuthenticationStatus.LoggedOut, }); await router.navigate(["lock"]); expect(router.url).toBe("/"); }); it("should allow navigation to the lock route when the user is Locked and they can lock", async () => { const { router } = setup({ authStatus: AuthenticationStatus.Locked, canLock: true, }); await router.navigate(["lock"]); expect(router.url).toBe("/lock"); }); it("should allow navigation to the lock route when the user is locked, they can lock, and device trust is not supported", async () => { const { router } = setup({ authStatus: AuthenticationStatus.Locked, canLock: true, supportsDeviceTrust: false, }); await router.navigate(["lock"]); expect(router.url).toBe("/lock"); }); it("should not allow navigation to the lock route when canLock is false", async () => { const { router } = setup({ authStatus: AuthenticationStatus.Locked, canLock: false, }); await router.navigate(["lock"]); expect(router.url).toBe("/"); }); it("should allow navigation to the lock route when device trust is supported, the user has a MP, and the user is coming from the login-initiated page", async () => { const { router } = setup({ authStatus: AuthenticationStatus.Locked, canLock: true, clientType: ClientType.Web, everHadUserKey: false, supportsDeviceTrust: true, hasMasterPassword: true, }); await router.navigate(["lock"], { queryParams: { from: "login-initiated" } }); expect(router.url).toBe("/lock?from=login-initiated"); }); it("should allow navigation to the lock route when TDE is disabled, the user doesn't have a MP, and the user has had a user key", async () => { const { router } = setup({ authStatus: AuthenticationStatus.Locked, canLock: true, supportsDeviceTrust: false, hasMasterPassword: false, everHadUserKey: true, }); await router.navigate(["lock"]); expect(router.url).toBe("/lock"); }); it("should not allow navigation to the lock route when device trust is supported and the user has not ever had a user key", async () => { const { router } = setup({ authStatus: AuthenticationStatus.Locked, canLock: true, clientType: ClientType.Web, everHadUserKey: false, supportsDeviceTrust: true, hasMasterPassword: false, }); await router.navigate(["lock"]); expect(router.url).toBe("/"); }); it("should redirect to the confirm-key-connector-domain route when the auth status is locked, can't lock and requires key connector domain confirmation", async () => { const { router } = setup({ authStatus: AuthenticationStatus.Locked, canLock: false, }); keyConnectorService.requiresDomainConfirmation$.mockReturnValue( of({ keyConnectorUrl: "https://example.com", } as KeyConnectorDomainConfirmation), ); await router.navigate(["lock"]); expect(router.url).toBe("/confirm-key-connector-domain"); }); });