1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-17 16:53:34 +00:00

Observable auth statuses (#8537)

* Observable has token

* Allow access to user key state observable

* Create observable auth status

* Fix DI
This commit is contained in:
Matt Gibson
2024-04-01 14:15:54 -05:00
committed by GitHub
parent c3c895230f
commit 136226b6be
12 changed files with 260 additions and 26 deletions

View File

@@ -1,13 +1,21 @@
import { MockProxy, mock } from "jest-mock-extended";
import { firstValueFrom } from "rxjs";
import { firstValueFrom, of } from "rxjs";
import { FakeAccountService, mockAccountServiceWith } from "../../../spec";
import {
FakeAccountService,
makeStaticByteArray,
mockAccountServiceWith,
trackEmissions,
} from "../../../spec";
import { ApiService } from "../../abstractions/api.service";
import { CryptoService } from "../../platform/abstractions/crypto.service";
import { MessagingService } from "../../platform/abstractions/messaging.service";
import { StateService } from "../../platform/abstractions/state.service";
import { Utils } from "../../platform/misc/utils";
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
import { UserId } from "../../types/guid";
import { UserKey } from "../../types/key";
import { TokenService } from "../abstractions/token.service";
import { AuthenticationStatus } from "../enums/authentication-status";
import { AuthService } from "./auth.service";
@@ -20,15 +28,18 @@ describe("AuthService", () => {
let cryptoService: MockProxy<CryptoService>;
let apiService: MockProxy<ApiService>;
let stateService: MockProxy<StateService>;
let tokenService: MockProxy<TokenService>;
const userId = Utils.newGuid() as UserId;
const userKey = new SymmetricCryptoKey(makeStaticByteArray(32) as Uint8Array) as UserKey;
beforeEach(() => {
accountService = mockAccountServiceWith(userId);
messagingService = mock<MessagingService>();
cryptoService = mock<CryptoService>();
apiService = mock<ApiService>();
stateService = mock<StateService>();
messagingService = mock();
cryptoService = mock();
apiService = mock();
stateService = mock();
tokenService = mock();
sut = new AuthService(
accountService,
@@ -36,26 +47,115 @@ describe("AuthService", () => {
cryptoService,
apiService,
stateService,
tokenService,
);
});
describe("activeAccountStatus$", () => {
test.each([
AuthenticationStatus.LoggedOut,
AuthenticationStatus.Locked,
AuthenticationStatus.Unlocked,
])(
`should emit %p when activeAccount$ emits an account with %p auth status`,
async (status) => {
accountService.activeAccountSubject.next({
id: userId,
email: "email",
name: "name",
status,
});
const accountInfo = {
status: AuthenticationStatus.Unlocked,
id: userId,
email: "email",
name: "name",
};
expect(await firstValueFrom(sut.activeAccountStatus$)).toEqual(status);
},
);
beforeEach(() => {
accountService.activeAccountSubject.next(accountInfo);
tokenService.hasAccessToken$.mockReturnValue(of(true));
cryptoService.getInMemoryUserKeyFor$.mockReturnValue(of(undefined));
});
it("emits LoggedOut when there is no active account", async () => {
accountService.activeAccountSubject.next(undefined);
expect(await firstValueFrom(sut.activeAccountStatus$)).toEqual(
AuthenticationStatus.LoggedOut,
);
});
it("emits LoggedOut when there is no access token", async () => {
tokenService.hasAccessToken$.mockReturnValue(of(false));
expect(await firstValueFrom(sut.activeAccountStatus$)).toEqual(
AuthenticationStatus.LoggedOut,
);
});
it("emits LoggedOut when there is no access token but has a user key", async () => {
tokenService.hasAccessToken$.mockReturnValue(of(false));
cryptoService.getInMemoryUserKeyFor$.mockReturnValue(of(userKey));
expect(await firstValueFrom(sut.activeAccountStatus$)).toEqual(
AuthenticationStatus.LoggedOut,
);
});
it("emits Locked when there is an access token and no user key", async () => {
tokenService.hasAccessToken$.mockReturnValue(of(true));
cryptoService.getInMemoryUserKeyFor$.mockReturnValue(of(undefined));
expect(await firstValueFrom(sut.activeAccountStatus$)).toEqual(AuthenticationStatus.Locked);
});
it("emits Unlocked when there is an access token and user key", async () => {
tokenService.hasAccessToken$.mockReturnValue(of(true));
cryptoService.getInMemoryUserKeyFor$.mockReturnValue(of(userKey));
expect(await firstValueFrom(sut.activeAccountStatus$)).toEqual(AuthenticationStatus.Unlocked);
});
it("follows the current active user", async () => {
const accountInfo2 = {
status: AuthenticationStatus.Unlocked,
id: Utils.newGuid() as UserId,
email: "email2",
name: "name2",
};
const emissions = trackEmissions(sut.activeAccountStatus$);
tokenService.hasAccessToken$.mockReturnValue(of(true));
cryptoService.getInMemoryUserKeyFor$.mockReturnValue(of(userKey));
accountService.activeAccountSubject.next(accountInfo2);
expect(emissions).toEqual([AuthenticationStatus.Locked, AuthenticationStatus.Unlocked]);
});
});
describe("authStatusFor$", () => {
beforeEach(() => {
tokenService.hasAccessToken$.mockReturnValue(of(true));
cryptoService.getInMemoryUserKeyFor$.mockReturnValue(of(undefined));
});
it("emits LoggedOut when userId is null", async () => {
expect(await firstValueFrom(sut.authStatusFor$(null))).toEqual(
AuthenticationStatus.LoggedOut,
);
});
it("emits LoggedOut when there is no access token", async () => {
tokenService.hasAccessToken$.mockReturnValue(of(false));
expect(await firstValueFrom(sut.authStatusFor$(userId))).toEqual(
AuthenticationStatus.LoggedOut,
);
});
it("emits Locked when there is an access token and no user key", async () => {
tokenService.hasAccessToken$.mockReturnValue(of(true));
cryptoService.getInMemoryUserKeyFor$.mockReturnValue(of(undefined));
expect(await firstValueFrom(sut.authStatusFor$(userId))).toEqual(AuthenticationStatus.Locked);
});
it("emits Unlocked when there is an access token and user key", async () => {
tokenService.hasAccessToken$.mockReturnValue(of(true));
cryptoService.getInMemoryUserKeyFor$.mockReturnValue(of(userKey));
expect(await firstValueFrom(sut.authStatusFor$(userId))).toEqual(
AuthenticationStatus.Unlocked,
);
});
});
});