bitwarden-estensione-browser/libs/common/src/auth/services/account.service.spec.ts

331 lines
10 KiB
TypeScript

/**
* need to update test environment so structuredClone works appropriately
* @jest-environment ../../libs/shared/test.environment.ts
*/
import { MockProxy, mock } from "jest-mock-extended";
import { firstValueFrom } from "rxjs";
import { FakeGlobalState } from "../../../spec/fake-state";
import { FakeGlobalStateProvider } from "../../../spec/fake-state-provider";
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 { AccountInfo, accountInfoEqual } from "../abstractions/account.service";
import {
ACCOUNT_ACCOUNTS,
ACCOUNT_ACTIVE_ACCOUNT_ID,
ACCOUNT_ACTIVITY,
AccountServiceImplementation,
} from "./account.service";
describe("accountInfoEqual", () => {
const accountInfo: AccountInfo = { name: "name", email: "email", emailVerified: true };
it("compares nulls", () => {
expect(accountInfoEqual(null, null)).toBe(true);
expect(accountInfoEqual(null, accountInfo)).toBe(false);
expect(accountInfoEqual(accountInfo, null)).toBe(false);
});
it("compares all keys, not just those defined in AccountInfo", () => {
const different = { ...accountInfo, extra: "extra" };
expect(accountInfoEqual(accountInfo, different)).toBe(false);
});
it("compares name", () => {
const same = { ...accountInfo };
const different = { ...accountInfo, name: "name2" };
expect(accountInfoEqual(accountInfo, same)).toBe(true);
expect(accountInfoEqual(accountInfo, different)).toBe(false);
});
it("compares email", () => {
const same = { ...accountInfo };
const different = { ...accountInfo, email: "email2" };
expect(accountInfoEqual(accountInfo, same)).toBe(true);
expect(accountInfoEqual(accountInfo, different)).toBe(false);
});
it("compares emailVerified", () => {
const same = { ...accountInfo };
const different = { ...accountInfo, emailVerified: false };
expect(accountInfoEqual(accountInfo, same)).toBe(true);
expect(accountInfoEqual(accountInfo, different)).toBe(false);
});
});
describe("accountService", () => {
let messagingService: MockProxy<MessagingService>;
let logService: MockProxy<LogService>;
let globalStateProvider: FakeGlobalStateProvider;
let sut: AccountServiceImplementation;
let accountsState: FakeGlobalState<Record<UserId, AccountInfo>>;
let activeAccountIdState: FakeGlobalState<UserId>;
const userId = "userId" as UserId;
const userInfo = { email: "email", name: "name", emailVerified: true };
beforeEach(() => {
messagingService = mock();
logService = mock();
globalStateProvider = new FakeGlobalStateProvider();
sut = new AccountServiceImplementation(messagingService, logService, globalStateProvider);
accountsState = globalStateProvider.getFake(ACCOUNT_ACCOUNTS);
activeAccountIdState = globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID);
});
afterEach(() => {
jest.resetAllMocks();
});
describe("activeAccount$", () => {
it("should emit undefined if no account is active", () => {
const emissions = trackEmissions(sut.activeAccount$);
expect(emissions).toEqual([undefined]);
});
it("should emit the active account", async () => {
const emissions = trackEmissions(sut.activeAccount$);
accountsState.stateSubject.next({ [userId]: userInfo });
activeAccountIdState.stateSubject.next(userId);
expect(emissions).toEqual([
undefined, // initial value
{ id: userId, ...userInfo },
]);
});
it("should remember the last emitted value", async () => {
accountsState.stateSubject.next({ [userId]: userInfo });
activeAccountIdState.stateSubject.next(userId);
expect(await firstValueFrom(sut.activeAccount$)).toEqual({
id: userId,
...userInfo,
});
});
});
describe("accounts$", () => {
it("should maintain an accounts cache", async () => {
accountsState.stateSubject.next({ [userId]: userInfo });
accountsState.stateSubject.next({ [userId]: userInfo });
expect(await firstValueFrom(sut.accounts$)).toEqual({
[userId]: userInfo,
});
});
});
describe("addAccount", () => {
it("should emit the new account", async () => {
await sut.addAccount(userId, userInfo);
const currentValue = await firstValueFrom(sut.accounts$);
expect(currentValue).toEqual({ [userId]: userInfo });
});
it("sets the last active date of the account to now", async () => {
const state = globalStateProvider.getFake(ACCOUNT_ACTIVITY);
state.stateSubject.next({});
await sut.addAccount(userId, userInfo);
expect(state.nextMock).toHaveBeenCalledWith({ userId: expect.any(Date) });
});
});
describe("setAccountName", () => {
const initialState = { [userId]: userInfo };
beforeEach(() => {
accountsState.stateSubject.next(initialState);
});
it("should update the account", async () => {
await sut.setAccountName(userId, "new name");
const currentState = await firstValueFrom(accountsState.state$);
expect(currentState).toEqual({
[userId]: { ...userInfo, name: "new name" },
});
});
it("should not update if the name is the same", async () => {
await sut.setAccountName(userId, "name");
const currentState = await firstValueFrom(accountsState.state$);
expect(currentState).toEqual(initialState);
});
});
describe("setAccountEmail", () => {
const initialState = { [userId]: userInfo };
beforeEach(() => {
accountsState.stateSubject.next(initialState);
});
it("should update the account", async () => {
await sut.setAccountEmail(userId, "new email");
const currentState = await firstValueFrom(accountsState.state$);
expect(currentState).toEqual({
[userId]: { ...userInfo, email: "new email" },
});
});
it("should not update if the email is the same", async () => {
await sut.setAccountEmail(userId, "email");
const currentState = await firstValueFrom(accountsState.state$);
expect(currentState).toEqual(initialState);
});
});
describe("setAccountEmailVerified", () => {
const initialState = { [userId]: userInfo };
initialState[userId].emailVerified = false;
beforeEach(() => {
accountsState.stateSubject.next(initialState);
});
it("should update the account", async () => {
await sut.setAccountEmailVerified(userId, true);
const currentState = await firstValueFrom(accountsState.state$);
expect(currentState).toEqual({
[userId]: { ...userInfo, emailVerified: true },
});
});
it("should not update if the email is the same", async () => {
await sut.setAccountEmailVerified(userId, false);
const currentState = await firstValueFrom(accountsState.state$);
expect(currentState).toEqual(initialState);
});
});
describe("clean", () => {
beforeEach(() => {
accountsState.stateSubject.next({ [userId]: userInfo });
});
it("removes account info of the given user", async () => {
await sut.clean(userId);
const currentState = await firstValueFrom(accountsState.state$);
expect(currentState).toEqual({
[userId]: {
email: "",
emailVerified: false,
name: undefined,
},
});
});
it("removes account activity of the given user", async () => {
const state = globalStateProvider.getFake(ACCOUNT_ACTIVITY);
state.stateSubject.next({ [userId]: new Date() });
await sut.clean(userId);
expect(state.nextMock).toHaveBeenCalledWith({});
});
});
describe("switchAccount", () => {
beforeEach(() => {
accountsState.stateSubject.next({ [userId]: userInfo });
activeAccountIdState.stateSubject.next(userId);
});
it("should emit undefined if no account is provided", async () => {
await sut.switchAccount(null);
const currentState = await firstValueFrom(sut.activeAccount$);
expect(currentState).toBeUndefined();
});
it("should throw if the account does not exist", () => {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
expect(sut.switchAccount("unknown" as UserId)).rejects.toThrowError("Account does not exist");
});
});
describe("account activity", () => {
let state: FakeGlobalState<Record<UserId, Date>>;
beforeEach(() => {
state = globalStateProvider.getFake(ACCOUNT_ACTIVITY);
});
describe("accountActivity$", () => {
it("returns the account activity state", async () => {
state.stateSubject.next({
[toId("user1")]: new Date(1),
[toId("user2")]: new Date(2),
});
await expect(firstValueFrom(sut.accountActivity$)).resolves.toEqual({
[toId("user1")]: new Date(1),
[toId("user2")]: new Date(2),
});
});
it("returns an empty object when account activity is null", async () => {
state.stateSubject.next(null);
await expect(firstValueFrom(sut.accountActivity$)).resolves.toEqual({});
});
});
describe("sortedUserIds$", () => {
it("returns the sorted user ids by date with most recent first", async () => {
state.stateSubject.next({
[toId("user1")]: new Date(3),
[toId("user2")]: new Date(2),
[toId("user3")]: new Date(1),
});
await expect(firstValueFrom(sut.sortedUserIds$)).resolves.toEqual([
"user1" as UserId,
"user2" as UserId,
"user3" as UserId,
]);
});
it("returns an empty array when account activity is null", async () => {
state.stateSubject.next(null);
await expect(firstValueFrom(sut.sortedUserIds$)).resolves.toEqual([]);
});
});
describe("setAccountActivity", () => {
it("sets the account activity", async () => {
await sut.setAccountActivity("user1" as UserId, new Date(1));
expect(state.nextMock).toHaveBeenCalledWith({ user1: new Date(1) });
});
it("does not update if the activity is the same", async () => {
state.stateSubject.next({ [toId("user1")]: new Date(1) });
await sut.setAccountActivity("user1" as UserId, new Date(1));
expect(state.nextMock).not.toHaveBeenCalled();
});
});
});
});
function toId(userId: string) {
return userId as UserId;
}