diff --git a/libs/common/spec/utils.ts b/libs/common/spec/utils.ts index 4013d3ac36f..87a9d630aea 100644 --- a/libs/common/spec/utils.ts +++ b/libs/common/spec/utils.ts @@ -1,5 +1,6 @@ // eslint-disable-next-line no-restricted-imports import { Substitute, Arg } from "@fluffy-spoon/substitute"; +import { Observable } from "rxjs"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; @@ -41,3 +42,22 @@ export function makeStaticByteArray(length: number, start = 0) { * Use to mock a return value of a static fromJSON method. */ export const mockFromJson = (stub: any) => (stub + "_fromJSON") as any; + +export function trackEmissions(observable: Observable): T[] { + const emissions: T[] = []; + observable.subscribe((value) => { + switch (typeof value) { + case "string": + case "number": + case "boolean": + emissions.push(value); + break; + case "object": + emissions.push({ ...value }); + break; + default: + emissions.push(JSON.parse(JSON.stringify(value))); + } + }); + return emissions; +} diff --git a/libs/common/src/auth/services/account.service.spec.ts b/libs/common/src/auth/services/account.service.spec.ts new file mode 100644 index 00000000000..3ded75f59e0 --- /dev/null +++ b/libs/common/src/auth/services/account.service.spec.ts @@ -0,0 +1,109 @@ +import { MockProxy, mock } from "jest-mock-extended"; +import { BehaviorSubject } from "rxjs"; + +import { trackEmissions } from "../../../spec/utils"; +import { LogService } from "../../platform/abstractions/log.service"; +import { MessagingService } from "../../platform/abstractions/messaging.service"; +import { UserId } from "../../types/guid"; +import { AuthenticationStatus } from "../enums/authentication-status"; + +import { AccountServiceImplementation } from "./account.service"; + +describe("accountService", () => { + let messagingService: MockProxy; + let logService: MockProxy; + let sut: AccountServiceImplementation; + const userId = "userId" as UserId; + + beforeEach(() => { + messagingService = mock(); + logService = mock(); + + sut = new AccountServiceImplementation(messagingService, logService); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe("setAccountStatus", () => { + let sutAccounts: BehaviorSubject>; + let accountsNext: jest.SpyInstance; + let emissions: Record[]; + + beforeEach(() => { + sutAccounts = sut["accounts"]; + accountsNext = jest.spyOn(sutAccounts, "next"); + emissions = []; + sutAccounts.subscribe((value) => emissions.push(JSON.parse(JSON.stringify(value)))); + }); + + it("should not emit if the status is the same", () => { + sut.setAccountStatus(userId, AuthenticationStatus.Locked); + sut.setAccountStatus(userId, AuthenticationStatus.Locked); + + expect(sutAccounts.value).toEqual({ userId: AuthenticationStatus.Locked }); + expect(accountsNext).toHaveBeenCalledTimes(1); + }); + + it("should emit if the status is different", () => { + const emissions = trackEmissions(sutAccounts); + sut.setAccountStatus(userId, AuthenticationStatus.Unlocked); + sut.setAccountStatus(userId, AuthenticationStatus.Locked); + + expect(emissions).toEqual([ + {}, // initial value + { userId: AuthenticationStatus.Unlocked }, + { userId: AuthenticationStatus.Locked }, + ]); + }); + + it("should emit logout if the status is logged out", () => { + const emissions = trackEmissions(sut.accountLogout$); + sut.setAccountStatus(userId, AuthenticationStatus.Unlocked); + sut.setAccountStatus(userId, AuthenticationStatus.LoggedOut); + + expect(emissions).toEqual([userId]); + }); + + it("should emit lock if the status is locked", () => { + const emissions = trackEmissions(sut.accountLock$); + sut.setAccountStatus(userId, AuthenticationStatus.Unlocked); + sut.setAccountStatus(userId, AuthenticationStatus.Locked); + + expect(emissions).toEqual([userId]); + }); + }); + + describe("switchAccount", () => { + let emissions: { id: string; status: AuthenticationStatus }[]; + + beforeEach(() => { + emissions = []; + sut.activeAccount$.subscribe((value) => emissions.push(value)); + }); + + it("should emit undefined if no account is provided", () => { + sut.switchAccount(undefined); + + expect(emissions).toEqual([undefined]); + }); + + it("should emit the active account and status", () => { + sut.setAccountStatus(userId, AuthenticationStatus.Unlocked); + sut.switchAccount(userId); + sut.setAccountStatus(userId, AuthenticationStatus.Locked); + sut.switchAccount(undefined); + sut.switchAccount(undefined); + expect(emissions).toEqual([ + undefined, // initial value + { id: userId, status: AuthenticationStatus.Unlocked }, + { id: userId, status: AuthenticationStatus.Locked }, + ]); + }); + + it("should throw if switched to an unknown account", () => { + expect(() => sut.switchAccount(userId)).toThrowError("Account does not exist"); + }); + }); +}); diff --git a/libs/common/src/auth/services/account.service.ts b/libs/common/src/auth/services/account.service.ts index 5b73745f25a..db2ab71c886 100644 --- a/libs/common/src/auth/services/account.service.ts +++ b/libs/common/src/auth/services/account.service.ts @@ -23,7 +23,7 @@ export class AccountServiceImplementation implements InternalAccountService { activeAccount$ = this.activeAccountId.pipe( combineLatestWith(this.accounts$), map(([id, accounts]) => (id ? { id, status: accounts[id] } : undefined)), - distinctUntilChanged((a, b) => a.id === b.id && a.status === b.status), + distinctUntilChanged(), share() ); accountLock$ = this.lock.asObservable();