187 lines
6.9 KiB
TypeScript
187 lines
6.9 KiB
TypeScript
import { matches, mock } from "jest-mock-extended";
|
|
import { BehaviorSubject, ReplaySubject, firstValueFrom, of, timeout } from "rxjs";
|
|
|
|
import { AccountInfo, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
|
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
|
import { AvatarService } from "@bitwarden/common/auth/abstractions/avatar.service";
|
|
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
|
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
|
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
|
import { UserId } from "@bitwarden/common/types/guid";
|
|
|
|
import { AccountSwitcherService } from "./account-switcher.service";
|
|
|
|
describe("AccountSwitcherService", () => {
|
|
let accountsSubject: BehaviorSubject<Record<UserId, AccountInfo>>;
|
|
let activeAccountSubject: BehaviorSubject<{ id: UserId } & AccountInfo>;
|
|
let authStatusSubject: ReplaySubject<Record<UserId, AuthenticationStatus>>;
|
|
|
|
const accountService = mock<AccountService>();
|
|
const avatarService = mock<AvatarService>();
|
|
const messagingService = mock<MessagingService>();
|
|
const environmentService = mock<EnvironmentService>();
|
|
const logService = mock<LogService>();
|
|
const authService = mock<AuthService>();
|
|
|
|
let accountSwitcherService: AccountSwitcherService;
|
|
|
|
beforeEach(() => {
|
|
jest.resetAllMocks();
|
|
accountsSubject = new BehaviorSubject<Record<UserId, AccountInfo>>(null);
|
|
activeAccountSubject = new BehaviorSubject<{ id: UserId } & AccountInfo>(null);
|
|
authStatusSubject = new ReplaySubject<Record<UserId, AuthenticationStatus>>(1);
|
|
|
|
// Use subject to allow for easy updates
|
|
accountService.accounts$ = accountsSubject;
|
|
accountService.activeAccount$ = activeAccountSubject;
|
|
authService.authStatuses$ = authStatusSubject;
|
|
|
|
accountSwitcherService = new AccountSwitcherService(
|
|
accountService,
|
|
avatarService,
|
|
messagingService,
|
|
environmentService,
|
|
logService,
|
|
authService,
|
|
);
|
|
});
|
|
|
|
afterEach(() => {
|
|
accountsSubject.complete();
|
|
activeAccountSubject.complete();
|
|
authStatusSubject.complete();
|
|
});
|
|
|
|
describe("availableAccounts$", () => {
|
|
it("should return all logged in accounts and an add account option when accounts are less than 5", async () => {
|
|
const accountInfo: AccountInfo = {
|
|
name: "Test User 1",
|
|
email: "test1@email.com",
|
|
};
|
|
|
|
avatarService.getUserAvatarColor$.mockReturnValue(of("#cccccc"));
|
|
accountsSubject.next({ ["1" as UserId]: accountInfo, ["2" as UserId]: accountInfo });
|
|
authStatusSubject.next({
|
|
["1" as UserId]: AuthenticationStatus.Unlocked,
|
|
["2" as UserId]: AuthenticationStatus.Locked,
|
|
});
|
|
activeAccountSubject.next(Object.assign(accountInfo, { id: "1" as UserId }));
|
|
|
|
const accounts = await firstValueFrom(
|
|
accountSwitcherService.availableAccounts$.pipe(timeout(20)),
|
|
);
|
|
expect(accounts).toHaveLength(3);
|
|
expect(accounts[0].id).toBe("1");
|
|
expect(accounts[0].isActive).toBeTruthy();
|
|
expect(accounts[1].id).toBe("2");
|
|
expect(accounts[1].isActive).toBeFalsy();
|
|
|
|
expect(accounts[2].id).toBe("addAccount");
|
|
expect(accounts[2].isActive).toBeFalsy();
|
|
});
|
|
|
|
it.each([5, 6])(
|
|
"should return only accounts if there are %i accounts",
|
|
async (numberOfAccounts) => {
|
|
const seedAccounts: Record<UserId, AccountInfo> = {};
|
|
const seedStatuses: Record<UserId, AuthenticationStatus> = {};
|
|
for (let i = 0; i < numberOfAccounts; i++) {
|
|
seedAccounts[`${i}` as UserId] = {
|
|
email: `test${i}@email.com`,
|
|
name: "Test User ${i}",
|
|
};
|
|
seedStatuses[`${i}` as UserId] = AuthenticationStatus.Unlocked;
|
|
}
|
|
avatarService.getUserAvatarColor$.mockReturnValue(of("#cccccc"));
|
|
accountsSubject.next(seedAccounts);
|
|
authStatusSubject.next(seedStatuses);
|
|
activeAccountSubject.next(
|
|
Object.assign(seedAccounts["1" as UserId], { id: "1" as UserId }),
|
|
);
|
|
|
|
const accounts = await firstValueFrom(accountSwitcherService.availableAccounts$);
|
|
|
|
expect(accounts).toHaveLength(numberOfAccounts);
|
|
accounts.forEach((account) => {
|
|
expect(account.id).not.toBe("addAccount");
|
|
});
|
|
},
|
|
);
|
|
|
|
it("excludes logged out accounts", async () => {
|
|
const user1AccountInfo: AccountInfo = {
|
|
name: "Test User 1",
|
|
email: "",
|
|
};
|
|
accountsSubject.next({ ["1" as UserId]: user1AccountInfo });
|
|
authStatusSubject.next({ ["1" as UserId]: AuthenticationStatus.LoggedOut });
|
|
accountsSubject.next({
|
|
"1": user1AccountInfo,
|
|
} as Record<UserId, AccountInfo>);
|
|
|
|
const accounts = await firstValueFrom(
|
|
accountSwitcherService.availableAccounts$.pipe(timeout(20)),
|
|
);
|
|
|
|
// Add account only
|
|
expect(accounts).toHaveLength(1);
|
|
expect(accounts[0].id).toBe("addAccount");
|
|
});
|
|
});
|
|
|
|
describe("selectAccount", () => {
|
|
it("initiates an add account logic when add account is selected", async () => {
|
|
let listener: (
|
|
message: { command: string; userId: string },
|
|
sender: unknown,
|
|
sendResponse: unknown,
|
|
) => void = null;
|
|
jest.spyOn(chrome.runtime.onMessage, "addListener").mockImplementation((addedListener) => {
|
|
listener = addedListener;
|
|
});
|
|
|
|
const removeListenerSpy = jest.spyOn(chrome.runtime.onMessage, "removeListener");
|
|
|
|
const selectAccountPromise = accountSwitcherService.selectAccount("addAccount");
|
|
|
|
expect(listener).not.toBeNull();
|
|
listener({ command: "switchAccountFinish", userId: null }, undefined, undefined);
|
|
|
|
await selectAccountPromise;
|
|
|
|
expect(accountService.switchAccount).toBeCalledWith(null);
|
|
|
|
expect(removeListenerSpy).toBeCalledTimes(1);
|
|
});
|
|
|
|
it("initiates an account switch with an account id", async () => {
|
|
let listener: (
|
|
message: { command: string; userId: string },
|
|
sender: unknown,
|
|
sendResponse: unknown,
|
|
) => void;
|
|
jest.spyOn(chrome.runtime.onMessage, "addListener").mockImplementation((addedListener) => {
|
|
listener = addedListener;
|
|
});
|
|
|
|
const removeListenerSpy = jest.spyOn(chrome.runtime.onMessage, "removeListener");
|
|
|
|
const selectAccountPromise = accountSwitcherService.selectAccount("1");
|
|
|
|
listener({ command: "switchAccountFinish", userId: "1" }, undefined, undefined);
|
|
|
|
await selectAccountPromise;
|
|
|
|
expect(accountService.switchAccount).toBeCalledWith("1");
|
|
expect(messagingService.send).toBeCalledWith(
|
|
"switchAccount",
|
|
matches((payload) => {
|
|
return payload.userId === "1";
|
|
}),
|
|
);
|
|
expect(removeListenerSpy).toBeCalledTimes(1);
|
|
});
|
|
});
|
|
});
|