1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-02 09:43:29 +00:00

add tests for subscriptions

This commit is contained in:
rr-bw
2025-10-31 14:12:31 -07:00
parent 5e8f24db99
commit d503f17dab

View File

@@ -1,5 +1,5 @@
import { mock, MockProxy } from "jest-mock-extended";
import { of } from "rxjs";
import { BehaviorSubject, of, Subject } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
@@ -31,6 +31,7 @@ describe("DefaultAuthRequestAnsweringService", () => {
const userId = "9f4c3452-6a45-48af-a7d0-74d3e8b65e4c" as UserId;
const otherUserId = "554c3112-9a75-23af-ab80-8dk3e9bl5i8e" as UserId;
const authRequestId = "auth-request-id-123";
beforeEach(() => {
accountService = mock<AccountService>();
@@ -65,7 +66,7 @@ describe("DefaultAuthRequestAnsweringService", () => {
describe("receivedPendingAuthRequest()", () => {
it("should throw an error", async () => {
// Act
const promise = sut.receivedPendingAuthRequest(userId);
const promise = sut.receivedPendingAuthRequest(userId, authRequestId);
// Assert
await expect(promise).rejects.toThrow(
@@ -185,4 +186,278 @@ describe("DefaultAuthRequestAnsweringService", () => {
expect(messagingService.send).not.toHaveBeenCalled();
});
});
describe("setupUnlockListenersForProcessingAuthRequests()", () => {
let destroy$: Subject<void>;
let activeAccount$: BehaviorSubject<any>;
let activeAccountStatus$: BehaviorSubject<AuthenticationStatus>;
let authStatusForSubjects: Map<UserId, BehaviorSubject<AuthenticationStatus>>;
beforeEach(() => {
destroy$ = new Subject<void>();
activeAccount$ = new BehaviorSubject({
id: userId,
email: "user@example.com",
emailVerified: true,
name: "User",
});
activeAccountStatus$ = new BehaviorSubject(AuthenticationStatus.Locked);
authStatusForSubjects = new Map();
accountService.activeAccount$ = activeAccount$;
authService.activeAccountStatus$ = activeAccountStatus$;
authService.authStatusFor$.mockImplementation((id: UserId) => {
if (!authStatusForSubjects.has(id)) {
authStatusForSubjects.set(id, new BehaviorSubject(AuthenticationStatus.Locked));
}
return authStatusForSubjects.get(id)!;
});
pendingAuthRequestsState.getAll$.mockReturnValue(of([]));
});
afterEach(() => {
destroy$.next();
destroy$.complete();
});
describe("active account switching", () => {
it("should process pending auth requests when switching to an unlocked user", async () => {
// Arrange
const processSpy = jest.spyOn(sut, "processPendingAuthRequests");
authStatusForSubjects.set(otherUserId, new BehaviorSubject(AuthenticationStatus.Unlocked));
// Act
sut.setupUnlockListenersForProcessingAuthRequests(destroy$);
// Simulate account switching to an Unlocked account
activeAccount$.next({
id: otherUserId,
email: "other@example.com",
emailVerified: true,
name: "Other",
});
// Assert
await new Promise((resolve) => setTimeout(resolve, 0)); // Allows observable chain to complete before assertion
expect(processSpy).toHaveBeenCalledTimes(1);
});
it("should NOT process pending auth requests when switching to a locked user", async () => {
// Arrange
const processSpy = jest.spyOn(sut, "processPendingAuthRequests");
authStatusForSubjects.set(otherUserId, new BehaviorSubject(AuthenticationStatus.Locked));
// Act
sut.setupUnlockListenersForProcessingAuthRequests(destroy$);
activeAccount$.next({
id: otherUserId,
email: "other@example.com",
emailVerified: true,
name: "Other",
});
// Assert
await new Promise((resolve) => setTimeout(resolve, 0));
expect(processSpy).not.toHaveBeenCalled();
});
it("should NOT process pending auth requests when switching to a logged out user", async () => {
// Arrange
const processSpy = jest.spyOn(sut, "processPendingAuthRequests");
authStatusForSubjects.set(otherUserId, new BehaviorSubject(AuthenticationStatus.LoggedOut));
// Act
sut.setupUnlockListenersForProcessingAuthRequests(destroy$);
activeAccount$.next({
id: otherUserId,
email: "other@example.com",
emailVerified: true,
name: "Other",
});
// Assert
await new Promise((resolve) => setTimeout(resolve, 0));
expect(processSpy).not.toHaveBeenCalled();
});
it("should NOT process pending auth requests when active account becomes null", async () => {
// Arrange
const processSpy = jest.spyOn(sut, "processPendingAuthRequests");
// Act
sut.setupUnlockListenersForProcessingAuthRequests(destroy$);
activeAccount$.next(null);
// Assert
await new Promise((resolve) => setTimeout(resolve, 0));
expect(processSpy).not.toHaveBeenCalled();
});
it("should handle multiple user switches correctly", async () => {
// Arrange
const processSpy = jest.spyOn(sut, "processPendingAuthRequests");
const secondUserId = "second-user-id" as UserId;
authStatusForSubjects.set(otherUserId, new BehaviorSubject(AuthenticationStatus.Unlocked));
authStatusForSubjects.set(secondUserId, new BehaviorSubject(AuthenticationStatus.Locked));
// Act
sut.setupUnlockListenersForProcessingAuthRequests(destroy$);
// Switch to unlocked user (should trigger)
activeAccount$.next({
id: otherUserId,
email: "other@example.com",
emailVerified: true,
name: "Other",
});
await new Promise((resolve) => setTimeout(resolve, 0));
// Switch to locked user (should NOT trigger)
activeAccount$.next({
id: secondUserId,
email: "second@example.com",
emailVerified: true,
name: "Second",
});
await new Promise((resolve) => setTimeout(resolve, 0));
// Assert
expect(processSpy).toHaveBeenCalledTimes(1);
});
});
describe("authentication status transitions", () => {
it("should process pending auth requests when active account transitions to Unlocked", async () => {
// Arrange
const processSpy = jest.spyOn(sut, "processPendingAuthRequests");
activeAccountStatus$.next(AuthenticationStatus.Locked);
// Act
sut.setupUnlockListenersForProcessingAuthRequests(destroy$);
activeAccountStatus$.next(AuthenticationStatus.Unlocked);
// Assert
await new Promise((resolve) => setTimeout(resolve, 0));
expect(processSpy).toHaveBeenCalledTimes(1);
});
it("should process pending auth requests when transitioning from LoggedOut to Unlocked", async () => {
// Arrange
const processSpy = jest.spyOn(sut, "processPendingAuthRequests");
activeAccountStatus$.next(AuthenticationStatus.LoggedOut);
// Act
sut.setupUnlockListenersForProcessingAuthRequests(destroy$);
activeAccountStatus$.next(AuthenticationStatus.Unlocked);
// Assert
await new Promise((resolve) => setTimeout(resolve, 0));
expect(processSpy).toHaveBeenCalledTimes(1);
});
it("should NOT process pending auth requests when transitioning from Unlocked to Locked", async () => {
// Arrange
const processSpy = jest.spyOn(sut, "processPendingAuthRequests");
activeAccountStatus$.next(AuthenticationStatus.Unlocked);
// Act
sut.setupUnlockListenersForProcessingAuthRequests(destroy$);
await new Promise((resolve) => setTimeout(resolve, 0));
// Reset spy to ignore the initial trigger (from null -> Unlocked)
processSpy.mockClear();
activeAccountStatus$.next(AuthenticationStatus.Locked);
// Assert
await new Promise((resolve) => setTimeout(resolve, 0));
expect(processSpy).not.toHaveBeenCalled();
});
it("should NOT process pending auth requests when transitioning from Locked to LoggedOut", async () => {
// Arrange
const processSpy = jest.spyOn(sut, "processPendingAuthRequests");
activeAccountStatus$.next(AuthenticationStatus.Locked);
// Act
sut.setupUnlockListenersForProcessingAuthRequests(destroy$);
activeAccountStatus$.next(AuthenticationStatus.LoggedOut);
// Assert
await new Promise((resolve) => setTimeout(resolve, 0));
expect(processSpy).not.toHaveBeenCalled();
});
it("should NOT process pending auth requests when staying in Unlocked status", async () => {
// Arrange
const processSpy = jest.spyOn(sut, "processPendingAuthRequests");
activeAccountStatus$.next(AuthenticationStatus.Unlocked);
// Act
sut.setupUnlockListenersForProcessingAuthRequests(destroy$);
await new Promise((resolve) => setTimeout(resolve, 0));
// Reset spy to ignore the initial trigger (from null -> Unlocked)
processSpy.mockClear();
activeAccountStatus$.next(AuthenticationStatus.Unlocked);
// Assert
await new Promise((resolve) => setTimeout(resolve, 0));
expect(processSpy).not.toHaveBeenCalled();
});
it("should handle multiple status transitions correctly", async () => {
// Arrange
const processSpy = jest.spyOn(sut, "processPendingAuthRequests");
activeAccountStatus$.next(AuthenticationStatus.Locked);
// Act
sut.setupUnlockListenersForProcessingAuthRequests(destroy$);
// Transition to Unlocked (should trigger)
activeAccountStatus$.next(AuthenticationStatus.Unlocked);
await new Promise((resolve) => setTimeout(resolve, 0));
// Transition to Locked (should NOT trigger)
activeAccountStatus$.next(AuthenticationStatus.Locked);
await new Promise((resolve) => setTimeout(resolve, 0));
// Transition back to Unlocked (should trigger again)
activeAccountStatus$.next(AuthenticationStatus.Unlocked);
await new Promise((resolve) => setTimeout(resolve, 0));
// Assert
expect(processSpy).toHaveBeenCalledTimes(2);
});
});
describe("subscription cleanup", () => {
it("should stop processing when destroy$ emits", async () => {
// Arrange
const processSpy = jest.spyOn(sut, "processPendingAuthRequests");
authStatusForSubjects.set(otherUserId, new BehaviorSubject(AuthenticationStatus.Unlocked));
// Act
sut.setupUnlockListenersForProcessingAuthRequests(destroy$);
// Emit destroy signal
destroy$.next();
// Try to trigger processing after cleanup
activeAccount$.next({
id: otherUserId,
email: "other@example.com",
emailVerified: true,
name: "Other",
});
activeAccountStatus$.next(AuthenticationStatus.Unlocked);
// Assert
await new Promise((resolve) => setTimeout(resolve, 0));
expect(processSpy).not.toHaveBeenCalled();
});
});
});
});