From 68a9976d2ffa1a624217c2ecfc79624d4f463d68 Mon Sep 17 00:00:00 2001 From: rr-bw <102181210+rr-bw@users.noreply.github.com> Date: Mon, 20 Oct 2025 15:13:08 -0700 Subject: [PATCH] add tests --- ...ion-auth-request-answering.service.spec.ts | 159 +++++++++++---- ...top-auth-request-answering.service.spec.ts | 188 ++++++++++++++++++ .../desktop-auth-request-answering.service.ts | 31 +-- ...ult-auth-request-answering.service.spec.ts | 188 ++++++++++++++++++ 4 files changed, 496 insertions(+), 70 deletions(-) rename libs/common/src/auth/services/auth-request-answering/auth-request-answering.service.spec.ts => apps/browser/src/auth/services/auth-request-answering/extension-auth-request-answering.service.spec.ts (55%) create mode 100644 apps/desktop/src/auth/services/auth-request-answering/desktop-auth-request-answering.service.spec.ts create mode 100644 libs/common/src/auth/services/auth-request-answering/default-auth-request-answering.service.spec.ts diff --git a/libs/common/src/auth/services/auth-request-answering/auth-request-answering.service.spec.ts b/apps/browser/src/auth/services/auth-request-answering/extension-auth-request-answering.service.spec.ts similarity index 55% rename from libs/common/src/auth/services/auth-request-answering/auth-request-answering.service.spec.ts rename to apps/browser/src/auth/services/auth-request-answering/extension-auth-request-answering.service.spec.ts index d19979c5b43..283b0309ebb 100644 --- a/libs/common/src/auth/services/auth-request-answering/auth-request-answering.service.spec.ts +++ b/apps/browser/src/auth/services/auth-request-answering/extension-auth-request-answering.service.spec.ts @@ -2,10 +2,12 @@ import { mock, MockProxy } from "jest-mock-extended"; import { of } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { AuthRequestAnsweringService } from "@bitwarden/common/auth/abstractions/auth-request-answering/auth-request-answering.service.abstraction"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthServerNotificationTags } from "@bitwarden/common/auth/enums/auth-server-notification-tags"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; +import { PendingAuthRequestsStateService } from "@bitwarden/common/auth/services/auth-request-answering/pending-auth-requests.state"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -17,33 +19,34 @@ import { } from "@bitwarden/common/platform/system-notifications/system-notifications.service"; import { UserId } from "@bitwarden/user-core"; -import { AuthRequestAnsweringService } from "../../abstractions/auth-request-answering/auth-request-answering.service.abstraction"; +import { ExtensionAuthRequestAnsweringService } from "./extension-auth-request-answering.service"; -import { PendingAuthRequestsStateService } from "./pending-auth-requests.state"; - -describe("AuthRequestAnsweringService", () => { +describe("ExtensionAuthRequestAnsweringService", () => { let accountService: MockProxy; - let actionService: MockProxy; let authService: MockProxy; - let i18nService: MockProxy; let masterPasswordService: any; // MasterPasswordServiceAbstraction has many members; we only use forceSetPasswordReason$ let messagingService: MockProxy; let pendingAuthRequestsState: MockProxy; + let actionService: MockProxy; + let i18nService: MockProxy; let platformUtilsService: MockProxy; let systemNotificationsService: MockProxy; let sut: AuthRequestAnsweringService; const userId = "9f4c3452-6a45-48af-a7d0-74d3e8b65e4c" as UserId; + const authRequestId = "auth-request-id-123"; beforeEach(() => { accountService = mock(); - actionService = mock(); authService = mock(); - i18nService = mock(); - masterPasswordService = { forceSetPasswordReason$: jest.fn() }; + masterPasswordService = { + forceSetPasswordReason$: jest.fn().mockReturnValue(of(ForceSetPasswordReason.None)), + }; messagingService = mock(); pendingAuthRequestsState = mock(); + actionService = mock(); + i18nService = mock(); platformUtilsService = mock(); systemNotificationsService = mock(); @@ -58,63 +61,77 @@ describe("AuthRequestAnsweringService", () => { accountService.accounts$ = of({ [userId]: { email: "user@example.com", emailVerified: true, name: "User" }, }); - (masterPasswordService.forceSetPasswordReason$ as jest.Mock).mockReturnValue( - of(ForceSetPasswordReason.None), - ); platformUtilsService.isPopupOpen.mockResolvedValue(false); i18nService.t.mockImplementation( (key: string, p1?: any) => `${key}${p1 != null ? ":" + p1 : ""}`, ); systemNotificationsService.create.mockResolvedValue("notif-id"); - sut = new AuthRequestAnsweringService( + sut = new ExtensionAuthRequestAnsweringService( accountService, - actionService, authService, - i18nService, masterPasswordService, messagingService, pendingAuthRequestsState, + actionService, + i18nService, platformUtilsService, systemNotificationsService, ); }); - describe("handleAuthRequestNotificationClicked", () => { - it("clears notification and opens popup when notification body is clicked", async () => { - const event: SystemNotificationEvent = { - id: "123", - buttonIdentifier: ButtonLocation.NotificationButton, - }; + describe("receivedPendingAuthRequest()", () => { + it("should throw if authRequestId not given", async () => { + // Act + const promise = sut.receivedPendingAuthRequest(userId, undefined); - await sut.handleAuthRequestNotificationClicked(event); - - expect(systemNotificationsService.clear).toHaveBeenCalledWith({ id: "123" }); - expect(actionService.openPopup).toHaveBeenCalledTimes(1); + // Assert + await expect(promise).rejects.toThrow("authRequestId not found."); }); - it("does nothing when an optional button is clicked", async () => { - const event: SystemNotificationEvent = { - id: "123", - buttonIdentifier: ButtonLocation.FirstOptionalButton, - }; + it("should add a pending marker for the user to state", async () => { + // Act + await sut.receivedPendingAuthRequest(userId, authRequestId); - await sut.handleAuthRequestNotificationClicked(event); - - expect(systemNotificationsService.clear).not.toHaveBeenCalled(); - expect(actionService.openPopup).not.toHaveBeenCalled(); + // Assert + expect(pendingAuthRequestsState.add).toHaveBeenCalledTimes(1); + expect(pendingAuthRequestsState.add).toHaveBeenCalledWith(userId); }); - }); - describe("receivedPendingAuthRequest", () => { - const authRequestId = "req-abc"; + it("should send an 'openLoginApproval' message if the popup is open and the user is Unlocked, active, and not required to set/change their master password", async () => { + // Arrange + platformUtilsService.isPopupOpen.mockResolvedValue(true); + authService.activeAccountStatus$ = of(AuthenticationStatus.Unlocked); - it("creates a system notification when popup is not open", async () => { + // Act + await sut.receivedPendingAuthRequest(userId, authRequestId); + + // Assert + expect(messagingService.send).toHaveBeenCalledTimes(1); + expect(messagingService.send).toHaveBeenCalledWith("openLoginApproval"); + }); + + it("should not send an 'openLoginApproval' message if the popup is closed", async () => { + // Arrange platformUtilsService.isPopupOpen.mockResolvedValue(false); authService.activeAccountStatus$ = of(AuthenticationStatus.Unlocked); + // Act await sut.receivedPendingAuthRequest(userId, authRequestId); + // Assert + expect(messagingService.send).not.toHaveBeenCalled(); + }); + + it("should create a system notification if the popup is closed", async () => { + // Arrange + platformUtilsService.isPopupOpen.mockResolvedValue(false); + authService.activeAccountStatus$ = of(AuthenticationStatus.Unlocked); + + // Act + await sut.receivedPendingAuthRequest(userId, authRequestId); + + // Assert expect(i18nService.t).toHaveBeenCalledWith("accountAccessRequested"); expect(i18nService.t).toHaveBeenCalledWith("confirmAccessAttempt", "user@example.com"); expect(systemNotificationsService.create).toHaveBeenCalledWith({ @@ -125,16 +142,74 @@ describe("AuthRequestAnsweringService", () => { }); }); - it("does not create a notification when popup is open, user is active, unlocked, and no force set password", async () => { + it("should not create a system notification if the popup is open and the user is Unlocked, active, and not required to set/change their master password", async () => { + // Arrange platformUtilsService.isPopupOpen.mockResolvedValue(true); authService.activeAccountStatus$ = of(AuthenticationStatus.Unlocked); - (masterPasswordService.forceSetPasswordReason$ as jest.Mock).mockReturnValue( - of(ForceSetPasswordReason.None), - ); + // Act await sut.receivedPendingAuthRequest(userId, authRequestId); + // Assert expect(systemNotificationsService.create).not.toHaveBeenCalled(); }); }); + + describe("userMeetsConditionsToShowApprovalDialog()", () => { + it("should return true if popup is open and user is Unlocked, active, and not required to set/change their master password", async () => { + // Arrange + platformUtilsService.isPopupOpen.mockResolvedValue(true); + authService.activeAccountStatus$ = of(AuthenticationStatus.Unlocked); + + // Act + const result = await sut.userMeetsConditionsToShowApprovalDialog(userId); + + // Assert + expect(result).toBe(true); + }); + + it("should return false if popup is closed", async () => { + // Arrange + platformUtilsService.isPopupOpen.mockResolvedValue(false); + authService.activeAccountStatus$ = of(AuthenticationStatus.Unlocked); + + // Act + const result = await sut.userMeetsConditionsToShowApprovalDialog(userId); + + // Assert + expect(result).toBe(false); + }); + }); + + describe("handleAuthRequestNotificationClicked()", () => { + it("should clear notification and open popup when notification body is clicked", async () => { + // Arrange + const event: SystemNotificationEvent = { + id: "123", + buttonIdentifier: ButtonLocation.NotificationButton, + }; + + // Act + await sut.handleAuthRequestNotificationClicked(event); + + // Assert + expect(systemNotificationsService.clear).toHaveBeenCalledWith({ id: "123" }); + expect(actionService.openPopup).toHaveBeenCalledTimes(1); + }); + + it("should do nothing when an optional notification button is clicked", async () => { + // Arrange + const event: SystemNotificationEvent = { + id: "123", + buttonIdentifier: ButtonLocation.FirstOptionalButton, + }; + + // Act + await sut.handleAuthRequestNotificationClicked(event); + + // Assert + expect(systemNotificationsService.clear).not.toHaveBeenCalled(); + expect(actionService.openPopup).not.toHaveBeenCalled(); + }); + }); }); diff --git a/apps/desktop/src/auth/services/auth-request-answering/desktop-auth-request-answering.service.spec.ts b/apps/desktop/src/auth/services/auth-request-answering/desktop-auth-request-answering.service.spec.ts new file mode 100644 index 00000000000..c16f5633f3d --- /dev/null +++ b/apps/desktop/src/auth/services/auth-request-answering/desktop-auth-request-answering.service.spec.ts @@ -0,0 +1,188 @@ +import { mock, MockProxy } from "jest-mock-extended"; +import { of } from "rxjs"; + +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { AuthRequestAnsweringService } from "@bitwarden/common/auth/abstractions/auth-request-answering/auth-request-answering.service.abstraction"; +import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; +import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; +import { PendingAuthRequestsStateService } from "@bitwarden/common/auth/services/auth-request-answering/pending-auth-requests.state"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; +import { + ButtonLocation, + SystemNotificationEvent, +} from "@bitwarden/common/platform/system-notifications/system-notifications.service"; +import { UserId } from "@bitwarden/user-core"; + +import { DesktopAuthRequestAnsweringService } from "./desktop-auth-request-answering.service"; + +describe("DesktopAuthRequestAnsweringService", () => { + let accountService: MockProxy; + let authService: MockProxy; + let masterPasswordService: any; // MasterPasswordServiceAbstraction has many members; we only use forceSetPasswordReason$ + let messagingService: MockProxy; + let pendingAuthRequestsState: MockProxy; + let i18nService: MockProxy; + + let sut: AuthRequestAnsweringService; + + const userId = "9f4c3452-6a45-48af-a7d0-74d3e8b65e4c" as UserId; + const authRequestId = "auth-request-id-123"; + + beforeEach(() => { + (global as any).ipc = { + platform: { + isWindowVisible: jest.fn(), + }, + auth: { + loginRequest: jest.fn(), + }, + }; + + accountService = mock(); + authService = mock(); + masterPasswordService = { + forceSetPasswordReason$: jest.fn().mockReturnValue(of(ForceSetPasswordReason.None)), + }; + messagingService = mock(); + pendingAuthRequestsState = mock(); + i18nService = mock(); + + // Common defaults + authService.activeAccountStatus$ = of(AuthenticationStatus.Locked); + accountService.activeAccount$ = of({ + id: userId, + email: "user@example.com", + emailVerified: true, + name: "User", + }); + accountService.accounts$ = of({ + [userId]: { email: "user@example.com", emailVerified: true, name: "User" }, + }); + (global as any).ipc.platform.isWindowVisible.mockResolvedValue(false); + i18nService.t.mockImplementation( + (key: string, p1?: any) => `${key}${p1 != null ? ":" + p1 : ""}`, + ); + + sut = new DesktopAuthRequestAnsweringService( + accountService, + authService, + masterPasswordService, + messagingService, + pendingAuthRequestsState, + i18nService, + ); + }); + + describe("receivedPendingAuthRequest()", () => { + it("should add a pending marker for the user to state", async () => { + // Act + await sut.receivedPendingAuthRequest(userId, authRequestId); + + // Assert + expect(pendingAuthRequestsState.add).toHaveBeenCalledTimes(1); + expect(pendingAuthRequestsState.add).toHaveBeenCalledWith(userId); + }); + + it("should send an 'openLoginApproval' message if the desktop window is visible and the user is Unlocked, active, and not required to set/change their master password", async () => { + // Arrange + (global as any).ipc.platform.isWindowVisible.mockResolvedValue(true); + authService.activeAccountStatus$ = of(AuthenticationStatus.Unlocked); + + // Act + await sut.receivedPendingAuthRequest(userId, authRequestId); + + // Assert + expect(messagingService.send).toHaveBeenCalledTimes(1); + expect(messagingService.send).toHaveBeenCalledWith("openLoginApproval"); + }); + + it("should not send an 'openLoginApproval' message if the desktop window is not visible", async () => { + (global as any).ipc.platform.isWindowVisible.mockResolvedValue(false); + authService.activeAccountStatus$ = of(AuthenticationStatus.Unlocked); + + // Act + await sut.receivedPendingAuthRequest(userId, authRequestId); + + // Assert + expect(messagingService.send).not.toHaveBeenCalled(); + }); + + it("should create a system notification if the desktop window is not visible", async () => { + // Arrange + (global as any).ipc.platform.isWindowVisible.mockResolvedValue(false); + authService.activeAccountStatus$ = of(AuthenticationStatus.Unlocked); + + // Act + await sut.receivedPendingAuthRequest(userId, authRequestId); + + // Assert + expect(i18nService.t).toHaveBeenCalledWith("accountAccessRequested"); + expect(i18nService.t).toHaveBeenCalledWith("confirmAccessAttempt", "user@example.com"); + expect(i18nService.t).toHaveBeenCalledWith("close"); + + expect((global as any).ipc.auth.loginRequest).toHaveBeenCalledWith( + "accountAccessRequested", + "confirmAccessAttempt:user@example.com", + "close", + ); + }); + + it("should not create a system notification if the desktop window is visible and the user is Unlocked, active, and not required to set/change their master password", async () => { + // Arrange + (global as any).ipc.platform.isWindowVisible.mockResolvedValue(true); + authService.activeAccountStatus$ = of(AuthenticationStatus.Unlocked); + + // Act + await sut.receivedPendingAuthRequest(userId, authRequestId); + + // Assert + expect((global as any).ipc.auth.loginRequest).not.toHaveBeenCalled(); + }); + }); + + describe("userMeetsConditionsToShowApprovalDialog()", () => { + it("should return true if desktop window is visible and user is Unlocked, active, and not required to set/change their master password", async () => { + // Arrange + (global as any).ipc.platform.isWindowVisible.mockResolvedValue(true); + authService.activeAccountStatus$ = of(AuthenticationStatus.Unlocked); + + // Act + const result = await sut.userMeetsConditionsToShowApprovalDialog(userId); + + // Assert + expect(result).toBe(true); + }); + + it("should return false if desktop window is not visible", async () => { + // Arrange + (global as any).ipc.platform.isWindowVisible.mockResolvedValue(false); + authService.activeAccountStatus$ = of(AuthenticationStatus.Unlocked); + + // Act + const result = await sut.userMeetsConditionsToShowApprovalDialog(userId); + + // Assert + expect(result).toBe(false); + }); + }); + + describe("handleAuthRequestNotificationClicked()", () => { + it("should throw an error", async () => { + // Arrange + const event: SystemNotificationEvent = { + id: "123", + buttonIdentifier: ButtonLocation.NotificationButton, + }; + + // Act + const promise = sut.handleAuthRequestNotificationClicked(event); + + // Assert + await expect(promise).rejects.toThrow( + "handleAuthRequestNotificationClicked() not implemented for this client", + ); + }); + }); +}); diff --git a/apps/desktop/src/auth/services/auth-request-answering/desktop-auth-request-answering.service.ts b/apps/desktop/src/auth/services/auth-request-answering/desktop-auth-request-answering.service.ts index cd7647e5ec4..87ed5d33b77 100644 --- a/apps/desktop/src/auth/services/auth-request-answering/desktop-auth-request-answering.service.ts +++ b/apps/desktop/src/auth/services/auth-request-answering/desktop-auth-request-answering.service.ts @@ -3,12 +3,8 @@ import { firstValueFrom } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthRequestAnsweringService } from "@bitwarden/common/auth/abstractions/auth-request-answering/auth-request-answering.service.abstraction"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; -import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { DefaultAuthRequestAnsweringService } from "@bitwarden/common/auth/services/auth-request-answering/default-auth-request-answering.service"; -import { - PendingAuthRequestsStateService, - PendingAuthUserMarker, -} from "@bitwarden/common/auth/services/auth-request-answering/pending-auth-requests.state"; +import { PendingAuthRequestsStateService } from "@bitwarden/common/auth/services/auth-request-answering/pending-auth-requests.state"; import { MasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; @@ -68,29 +64,8 @@ export class DesktopAuthRequestAnsweringService } async handleAuthRequestNotificationClicked(event: SystemNotificationEvent) { + // Not implemented for Desktop because click handling is already setup in electron-main-messaging.service.ts. + // See click handler in ipcMain.handle("loginRequest"... throw new Error("handleAuthRequestNotificationClicked() not implemented for this client"); } - - async processPendingAuthRequests(): Promise { - // Prune any stale pending requests (older than 15 minutes) - // This comes from GlobalSettings.cs - // public TimeSpan UserRequestExpiration { get; set; } = TimeSpan.FromMinutes(15); - const fifteenMinutesMs = 15 * 60 * 1000; - - await this.pendingAuthRequestsState.pruneOlderThan(fifteenMinutesMs); - - const pendingAuthRequestsInState: PendingAuthUserMarker[] = - (await firstValueFrom(this.pendingAuthRequestsState.getAll$())) ?? []; - - if (pendingAuthRequestsInState.length > 0) { - const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); - const pendingAuthRequestsForActiveUser = pendingAuthRequestsInState.some( - (e) => e.userId === activeUserId, - ); - - if (pendingAuthRequestsForActiveUser) { - this.messagingService.send("openLoginApproval"); - } - } - } } diff --git a/libs/common/src/auth/services/auth-request-answering/default-auth-request-answering.service.spec.ts b/libs/common/src/auth/services/auth-request-answering/default-auth-request-answering.service.spec.ts new file mode 100644 index 00000000000..12210db9520 --- /dev/null +++ b/libs/common/src/auth/services/auth-request-answering/default-auth-request-answering.service.spec.ts @@ -0,0 +1,188 @@ +import { mock, MockProxy } from "jest-mock-extended"; +import { of } from "rxjs"; + +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; +import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; +import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; +import { + ButtonLocation, + SystemNotificationEvent, +} from "@bitwarden/common/platform/system-notifications/system-notifications.service"; +import { UserId } from "@bitwarden/user-core"; + +import { AuthRequestAnsweringService } from "../../abstractions/auth-request-answering/auth-request-answering.service.abstraction"; + +import { DefaultAuthRequestAnsweringService } from "./default-auth-request-answering.service"; +import { + PendingAuthRequestsStateService, + PendingAuthUserMarker, +} from "./pending-auth-requests.state"; + +describe("DefaultAuthRequestAnsweringService", () => { + let accountService: MockProxy; + let authService: MockProxy; + let masterPasswordService: any; // MasterPasswordServiceAbstraction has many members; we only use forceSetPasswordReason$ + let messagingService: MockProxy; + let pendingAuthRequestsState: MockProxy; + + let sut: AuthRequestAnsweringService; + + const userId = "9f4c3452-6a45-48af-a7d0-74d3e8b65e4c" as UserId; + const otherUserId = "554c3112-9a75-23af-ab80-8dk3e9bl5i8e" as UserId; + + beforeEach(() => { + accountService = mock(); + authService = mock(); + masterPasswordService = { + forceSetPasswordReason$: jest.fn().mockReturnValue(of(ForceSetPasswordReason.None)), + }; + messagingService = mock(); + pendingAuthRequestsState = mock(); + + // Common defaults + authService.activeAccountStatus$ = of(AuthenticationStatus.Locked); + accountService.activeAccount$ = of({ + id: userId, + email: "user@example.com", + emailVerified: true, + name: "User", + }); + accountService.accounts$ = of({ + [userId]: { email: "user@example.com", emailVerified: true, name: "User" }, + }); + + sut = new DefaultAuthRequestAnsweringService( + accountService, + authService, + masterPasswordService, + messagingService, + pendingAuthRequestsState, + ); + }); + + describe("receivedPendingAuthRequest()", () => { + it("should throw an error", async () => { + // Act + const promise = sut.receivedPendingAuthRequest(userId); + + // Assert + await expect(promise).rejects.toThrow( + "receivedPendingAuthRequest() not implemented for this client", + ); + }); + }); + + describe("userMeetsConditionsToShowApprovalDialog()", () => { + it("should return true if user is Unlocked, active, and not required to set/change their master password", async () => { + // Arrange + authService.activeAccountStatus$ = of(AuthenticationStatus.Unlocked); + + // Act + const result = await sut.userMeetsConditionsToShowApprovalDialog(userId); + + // Assert + expect(result).toBe(true); + }); + + it("should return false if user is not Unlocked", async () => { + // Arrange + authService.activeAccountStatus$ = of(AuthenticationStatus.Locked); + + // Act + const result = await sut.userMeetsConditionsToShowApprovalDialog(userId); + + // Assert + expect(result).toBe(false); + }); + + it("should return false if user is not the active user", async () => { + // Arrange + accountService.activeAccount$ = of({ + id: otherUserId, + email: "other-user@example.com", + emailVerified: true, + name: "Other User", + }); + + // Act + const result = await sut.userMeetsConditionsToShowApprovalDialog(userId); + + // Assert + expect(result).toBe(false); + }); + + it("should return false if user is required to set/change their master password", async () => { + // Arrange + masterPasswordService.forceSetPasswordReason$.mockReturnValue( + of(ForceSetPasswordReason.WeakMasterPassword), + ); + + // Act + const result = await sut.userMeetsConditionsToShowApprovalDialog(userId); + + // Assert + expect(result).toBe(false); + }); + }); + + describe("handleAuthRequestNotificationClicked()", () => { + it("should throw an error", async () => { + // Arrange + const event: SystemNotificationEvent = { + id: "123", + buttonIdentifier: ButtonLocation.NotificationButton, + }; + + // Act + const promise = sut.handleAuthRequestNotificationClicked(event); + + // Assert + await expect(promise).rejects.toThrow( + "handleAuthRequestNotificationClicked() not implemented for this client", + ); + }); + }); + + describe("processPendingAuthRequests()", () => { + it("should send an 'openLoginApproval' message if there is at least one pending auth request for the user in state", async () => { + // Arrange + const pendingRequests: PendingAuthUserMarker[] = [{ userId, receivedAtMs: Date.now() }]; + pendingAuthRequestsState.getAll$.mockReturnValue(of(pendingRequests)); + + // Act + await sut.processPendingAuthRequests(); + + // Assert + expect(messagingService.send).toHaveBeenCalledWith("openLoginApproval"); + expect(messagingService.send).toHaveBeenCalledTimes(1); + }); + + it("should NOT send a message if there are no pending auth requests in state", async () => { + // Arrange + const pendingRequests: PendingAuthUserMarker[] = []; + pendingAuthRequestsState.getAll$.mockReturnValue(of(pendingRequests)); + + // Act + await sut.processPendingAuthRequests(); + + // Assert + expect(messagingService.send).not.toHaveBeenCalled(); + }); + + it("should NOT send a message if there are no pending auth requests in state for the active user", async () => { + // Arrange + const pendingRequests: PendingAuthUserMarker[] = [ + { userId: otherUserId, receivedAtMs: Date.now() }, + ]; // pending auth marker for a different user + pendingAuthRequestsState.getAll$.mockReturnValue(of(pendingRequests)); + + // Act + await sut.processPendingAuthRequests(); + + // Assert + expect(messagingService.send).not.toHaveBeenCalled(); + }); + }); +});