mirror of
https://github.com/bitwarden/browser
synced 2025-12-10 05:13:29 +00:00
[PM-24280] Remove account service from state (#15828)
* Introduce ActiveUserAccessor * Use ActiveUserAccessor over AccountService * Updates tests and testing utils to support ActiveUserAccessor * Update all injection points * Fix types test * Use ternary instead
This commit is contained in:
@@ -43,6 +43,7 @@ import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authenticatio
|
|||||||
import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service";
|
import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service";
|
||||||
import { AuthService } from "@bitwarden/common/auth/services/auth.service";
|
import { AuthService } from "@bitwarden/common/auth/services/auth.service";
|
||||||
import { AvatarService } from "@bitwarden/common/auth/services/avatar.service";
|
import { AvatarService } from "@bitwarden/common/auth/services/avatar.service";
|
||||||
|
import { DefaultActiveUserAccessor } from "@bitwarden/common/auth/services/default-active-user.accessor";
|
||||||
import { DevicesServiceImplementation } from "@bitwarden/common/auth/services/devices/devices.service.implementation";
|
import { DevicesServiceImplementation } from "@bitwarden/common/auth/services/devices/devices.service.implementation";
|
||||||
import { DevicesApiServiceImplementation } from "@bitwarden/common/auth/services/devices-api.service.implementation";
|
import { DevicesApiServiceImplementation } from "@bitwarden/common/auth/services/devices-api.service.implementation";
|
||||||
import { SsoLoginService } from "@bitwarden/common/auth/services/sso-login.service";
|
import { SsoLoginService } from "@bitwarden/common/auth/services/sso-login.service";
|
||||||
@@ -591,7 +592,7 @@ export default class MainBackground {
|
|||||||
this.singleUserStateProvider,
|
this.singleUserStateProvider,
|
||||||
);
|
);
|
||||||
this.activeUserStateProvider = new DefaultActiveUserStateProvider(
|
this.activeUserStateProvider = new DefaultActiveUserStateProvider(
|
||||||
this.accountService,
|
new DefaultActiveUserAccessor(this.accountService),
|
||||||
this.singleUserStateProvider,
|
this.singleUserStateProvider,
|
||||||
);
|
);
|
||||||
this.derivedStateProvider = new InlineDerivedStateProvider();
|
this.derivedStateProvider = new InlineDerivedStateProvider();
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ import {
|
|||||||
} from "@bitwarden/common/auth/services/account.service";
|
} from "@bitwarden/common/auth/services/account.service";
|
||||||
import { AuthService } from "@bitwarden/common/auth/services/auth.service";
|
import { AuthService } from "@bitwarden/common/auth/services/auth.service";
|
||||||
import { AvatarService } from "@bitwarden/common/auth/services/avatar.service";
|
import { AvatarService } from "@bitwarden/common/auth/services/avatar.service";
|
||||||
|
import { DefaultActiveUserAccessor } from "@bitwarden/common/auth/services/default-active-user.accessor";
|
||||||
import { DevicesApiServiceImplementation } from "@bitwarden/common/auth/services/devices-api.service.implementation";
|
import { DevicesApiServiceImplementation } from "@bitwarden/common/auth/services/devices-api.service.implementation";
|
||||||
import { MasterPasswordApiService } from "@bitwarden/common/auth/services/master-password/master-password-api.service.implementation";
|
import { MasterPasswordApiService } from "@bitwarden/common/auth/services/master-password/master-password-api.service.implementation";
|
||||||
import { TokenService } from "@bitwarden/common/auth/services/token.service";
|
import { TokenService } from "@bitwarden/common/auth/services/token.service";
|
||||||
@@ -377,7 +378,7 @@ export class ServiceContainer {
|
|||||||
);
|
);
|
||||||
|
|
||||||
this.activeUserStateProvider = new DefaultActiveUserStateProvider(
|
this.activeUserStateProvider = new DefaultActiveUserStateProvider(
|
||||||
this.accountService,
|
new DefaultActiveUserAccessor(this.accountService),
|
||||||
this.singleUserStateProvider,
|
this.singleUserStateProvider,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { Subject, firstValueFrom } from "rxjs";
|
|||||||
|
|
||||||
import { SsoUrlService } from "@bitwarden/auth/common";
|
import { SsoUrlService } from "@bitwarden/auth/common";
|
||||||
import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service";
|
import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service";
|
||||||
|
import { DefaultActiveUserAccessor } from "@bitwarden/common/auth/services/default-active-user.accessor";
|
||||||
import { ClientType } from "@bitwarden/common/enums";
|
import { ClientType } from "@bitwarden/common/enums";
|
||||||
import { EncryptServiceImplementation } from "@bitwarden/common/key-management/crypto/services/encrypt.service.implementation";
|
import { EncryptServiceImplementation } from "@bitwarden/common/key-management/crypto/services/encrypt.service.implementation";
|
||||||
import { RegionConfig } from "@bitwarden/common/platform/abstractions/environment.service";
|
import { RegionConfig } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||||
@@ -170,7 +171,7 @@ export class Main {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const activeUserStateProvider = new DefaultActiveUserStateProvider(
|
const activeUserStateProvider = new DefaultActiveUserStateProvider(
|
||||||
accountService,
|
new DefaultActiveUserAccessor(accountService),
|
||||||
singleUserStateProvider,
|
singleUserStateProvider,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -110,6 +110,7 @@ import { AccountServiceImplementation } from "@bitwarden/common/auth/services/ac
|
|||||||
import { AnonymousHubService } from "@bitwarden/common/auth/services/anonymous-hub.service";
|
import { AnonymousHubService } from "@bitwarden/common/auth/services/anonymous-hub.service";
|
||||||
import { AuthService } from "@bitwarden/common/auth/services/auth.service";
|
import { AuthService } from "@bitwarden/common/auth/services/auth.service";
|
||||||
import { AvatarService } from "@bitwarden/common/auth/services/avatar.service";
|
import { AvatarService } from "@bitwarden/common/auth/services/avatar.service";
|
||||||
|
import { DefaultActiveUserAccessor } from "@bitwarden/common/auth/services/default-active-user.accessor";
|
||||||
import { DevicesServiceImplementation } from "@bitwarden/common/auth/services/devices/devices.service.implementation";
|
import { DevicesServiceImplementation } from "@bitwarden/common/auth/services/devices/devices.service.implementation";
|
||||||
import { DevicesApiServiceImplementation } from "@bitwarden/common/auth/services/devices-api.service.implementation";
|
import { DevicesApiServiceImplementation } from "@bitwarden/common/auth/services/devices-api.service.implementation";
|
||||||
import { MasterPasswordApiService } from "@bitwarden/common/auth/services/master-password/master-password-api.service.implementation";
|
import { MasterPasswordApiService } from "@bitwarden/common/auth/services/master-password/master-password-api.service.implementation";
|
||||||
@@ -232,6 +233,7 @@ import { StorageServiceProvider } from "@bitwarden/common/platform/services/stor
|
|||||||
import { UserAutoUnlockKeyService } from "@bitwarden/common/platform/services/user-auto-unlock-key.service";
|
import { UserAutoUnlockKeyService } from "@bitwarden/common/platform/services/user-auto-unlock-key.service";
|
||||||
import { ValidationService } from "@bitwarden/common/platform/services/validation.service";
|
import { ValidationService } from "@bitwarden/common/platform/services/validation.service";
|
||||||
import {
|
import {
|
||||||
|
ActiveUserAccessor,
|
||||||
ActiveUserStateProvider,
|
ActiveUserStateProvider,
|
||||||
DerivedStateProvider,
|
DerivedStateProvider,
|
||||||
GlobalStateProvider,
|
GlobalStateProvider,
|
||||||
@@ -1271,10 +1273,15 @@ const safeProviders: SafeProvider[] = [
|
|||||||
useClass: DefaultGlobalStateProvider,
|
useClass: DefaultGlobalStateProvider,
|
||||||
deps: [StorageServiceProvider, LogService],
|
deps: [StorageServiceProvider, LogService],
|
||||||
}),
|
}),
|
||||||
|
safeProvider({
|
||||||
|
provide: ActiveUserAccessor,
|
||||||
|
useClass: DefaultActiveUserAccessor,
|
||||||
|
deps: [AccountServiceAbstraction],
|
||||||
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: ActiveUserStateProvider,
|
provide: ActiveUserStateProvider,
|
||||||
useClass: DefaultActiveUserStateProvider,
|
useClass: DefaultActiveUserStateProvider,
|
||||||
deps: [AccountServiceAbstraction, SingleUserStateProvider],
|
deps: [ActiveUserAccessor, SingleUserStateProvider],
|
||||||
}),
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: SingleUserStateProvider,
|
provide: SingleUserStateProvider,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
// FIXME: Update this file to be type safe and remove this and next line
|
||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { mock } from "jest-mock-extended";
|
import { mock } from "jest-mock-extended";
|
||||||
import { Observable, map, of, switchMap, take } from "rxjs";
|
import { BehaviorSubject, map, Observable, of, switchMap, take } from "rxjs";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
GlobalState,
|
GlobalState,
|
||||||
@@ -16,11 +16,11 @@ import {
|
|||||||
DeriveDefinition,
|
DeriveDefinition,
|
||||||
DerivedStateProvider,
|
DerivedStateProvider,
|
||||||
UserKeyDefinition,
|
UserKeyDefinition,
|
||||||
|
ActiveUserAccessor,
|
||||||
} from "../src/platform/state";
|
} from "../src/platform/state";
|
||||||
import { UserId } from "../src/types/guid";
|
import { UserId } from "../src/types/guid";
|
||||||
import { DerivedStateDependencies } from "../src/types/state";
|
import { DerivedStateDependencies } from "../src/types/state";
|
||||||
|
|
||||||
import { FakeAccountService } from "./fake-account-service";
|
|
||||||
import {
|
import {
|
||||||
FakeActiveUserState,
|
FakeActiveUserState,
|
||||||
FakeDerivedState,
|
FakeDerivedState,
|
||||||
@@ -28,6 +28,35 @@ import {
|
|||||||
FakeSingleUserState,
|
FakeSingleUserState,
|
||||||
} from "./fake-state";
|
} from "./fake-state";
|
||||||
|
|
||||||
|
export interface MinimalAccountService {
|
||||||
|
activeUserId: UserId | null;
|
||||||
|
activeAccount$: Observable<{ id: UserId } | null>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FakeActiveUserAccessor implements MinimalAccountService, ActiveUserAccessor {
|
||||||
|
private _subject: BehaviorSubject<UserId | null>;
|
||||||
|
|
||||||
|
constructor(startingUser: UserId | null) {
|
||||||
|
this._subject = new BehaviorSubject(startingUser);
|
||||||
|
this.activeAccount$ = this._subject
|
||||||
|
.asObservable()
|
||||||
|
.pipe(map((id) => (id != null ? { id } : null)));
|
||||||
|
this.activeUserId$ = this._subject.asObservable();
|
||||||
|
}
|
||||||
|
|
||||||
|
get activeUserId(): UserId {
|
||||||
|
return this._subject.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
activeUserId$: Observable<UserId>;
|
||||||
|
|
||||||
|
activeAccount$: Observable<{ id: UserId }>;
|
||||||
|
|
||||||
|
switch(user: UserId | null) {
|
||||||
|
this._subject.next(user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class FakeGlobalStateProvider implements GlobalStateProvider {
|
export class FakeGlobalStateProvider implements GlobalStateProvider {
|
||||||
mock = mock<GlobalStateProvider>();
|
mock = mock<GlobalStateProvider>();
|
||||||
establishedMocks: Map<string, FakeGlobalState<unknown>> = new Map();
|
establishedMocks: Map<string, FakeGlobalState<unknown>> = new Map();
|
||||||
@@ -138,18 +167,18 @@ export class FakeSingleUserStateProvider implements SingleUserStateProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class FakeActiveUserStateProvider implements ActiveUserStateProvider {
|
export class FakeActiveUserStateProvider implements ActiveUserStateProvider {
|
||||||
activeUserId$: Observable<UserId>;
|
activeUserId$: Observable<UserId | null>;
|
||||||
states: Map<string, FakeActiveUserState<unknown>> = new Map();
|
states: Map<string, FakeActiveUserState<unknown>> = new Map();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public accountService: FakeAccountService,
|
public accountServiceAccessor: MinimalAccountService,
|
||||||
readonly updateSyncCallback?: (
|
readonly updateSyncCallback?: (
|
||||||
key: UserKeyDefinition<unknown>,
|
key: UserKeyDefinition<unknown>,
|
||||||
userId: UserId,
|
userId: UserId,
|
||||||
newValue: unknown,
|
newValue: unknown,
|
||||||
) => Promise<void>,
|
) => Promise<void>,
|
||||||
) {
|
) {
|
||||||
this.activeUserId$ = accountService.activeAccountSubject.asObservable().pipe(map((a) => a?.id));
|
this.activeUserId$ = accountServiceAccessor.activeAccount$.pipe(map((a) => a?.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
get<T>(userKeyDefinition: UserKeyDefinition<T>): ActiveUserState<T> {
|
get<T>(userKeyDefinition: UserKeyDefinition<T>): ActiveUserState<T> {
|
||||||
@@ -182,9 +211,13 @@ export class FakeActiveUserStateProvider implements ActiveUserStateProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private buildFakeState<T>(userKeyDefinition: UserKeyDefinition<T>, initialValue?: T) {
|
private buildFakeState<T>(userKeyDefinition: UserKeyDefinition<T>, initialValue?: T) {
|
||||||
const state = new FakeActiveUserState<T>(this.accountService, initialValue, async (...args) => {
|
const state = new FakeActiveUserState<T>(
|
||||||
await this.updateSyncCallback?.(userKeyDefinition, ...args);
|
this.accountServiceAccessor,
|
||||||
});
|
initialValue,
|
||||||
|
async (...args) => {
|
||||||
|
await this.updateSyncCallback?.(userKeyDefinition, ...args);
|
||||||
|
},
|
||||||
|
);
|
||||||
state.keyDefinition = userKeyDefinition;
|
state.keyDefinition = userKeyDefinition;
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
@@ -256,14 +289,14 @@ export class FakeStateProvider implements StateProvider {
|
|||||||
return this.derived.get(parentState$, deriveDefinition, dependencies);
|
return this.derived.get(parentState$, deriveDefinition, dependencies);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(public accountService: FakeAccountService) {}
|
constructor(private activeAccountAccessor: MinimalAccountService) {}
|
||||||
|
|
||||||
private distributeSingleUserUpdate(
|
private distributeSingleUserUpdate(
|
||||||
key: UserKeyDefinition<unknown>,
|
key: UserKeyDefinition<unknown>,
|
||||||
userId: UserId,
|
userId: UserId,
|
||||||
newState: unknown,
|
newState: unknown,
|
||||||
) {
|
) {
|
||||||
if (this.activeUser.accountService.activeUserId === userId) {
|
if (this.activeUser.accountServiceAccessor.activeUserId === userId) {
|
||||||
const state = this.activeUser.getFake(key, { allowInit: false });
|
const state = this.activeUser.getFake(key, { allowInit: false });
|
||||||
state?.nextState(newState, { syncValue: false });
|
state?.nextState(newState, { syncValue: false });
|
||||||
}
|
}
|
||||||
@@ -284,7 +317,7 @@ export class FakeStateProvider implements StateProvider {
|
|||||||
this.distributeSingleUserUpdate.bind(this),
|
this.distributeSingleUserUpdate.bind(this),
|
||||||
);
|
);
|
||||||
activeUser: FakeActiveUserStateProvider = new FakeActiveUserStateProvider(
|
activeUser: FakeActiveUserStateProvider = new FakeActiveUserStateProvider(
|
||||||
this.accountService,
|
this.activeAccountAccessor,
|
||||||
this.distributeActiveUserUpdate.bind(this),
|
this.distributeActiveUserUpdate.bind(this),
|
||||||
);
|
);
|
||||||
derived: FakeDerivedStateProvider = new FakeDerivedStateProvider();
|
derived: FakeDerivedStateProvider = new FakeDerivedStateProvider();
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import { CombinedState, activeMarker } from "../src/platform/state/user-state";
|
|||||||
import { UserId } from "../src/types/guid";
|
import { UserId } from "../src/types/guid";
|
||||||
import { DerivedStateDependencies } from "../src/types/state";
|
import { DerivedStateDependencies } from "../src/types/state";
|
||||||
|
|
||||||
import { FakeAccountService } from "./fake-account-service";
|
import { MinimalAccountService } from "./fake-state-provider";
|
||||||
|
|
||||||
const DEFAULT_TEST_OPTIONS: StateUpdateOptions<any, any> = {
|
const DEFAULT_TEST_OPTIONS: StateUpdateOptions<any, any> = {
|
||||||
shouldUpdate: () => true,
|
shouldUpdate: () => true,
|
||||||
@@ -177,7 +177,7 @@ export class FakeActiveUserState<T> implements ActiveUserState<T> {
|
|||||||
combinedState$: Observable<CombinedState<T>>;
|
combinedState$: Observable<CombinedState<T>>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private accountService: FakeAccountService,
|
private activeAccountAccessor: MinimalAccountService,
|
||||||
initialValue?: T,
|
initialValue?: T,
|
||||||
updateSyncCallback?: (userId: UserId, newValue: T) => Promise<void>,
|
updateSyncCallback?: (userId: UserId, newValue: T) => Promise<void>,
|
||||||
) {
|
) {
|
||||||
@@ -194,14 +194,10 @@ export class FakeActiveUserState<T> implements ActiveUserState<T> {
|
|||||||
this.state$ = this.combinedState$.pipe(map(([_userId, state]) => state));
|
this.state$ = this.combinedState$.pipe(map(([_userId, state]) => state));
|
||||||
}
|
}
|
||||||
|
|
||||||
get userId() {
|
|
||||||
return this.accountService.activeUserId;
|
|
||||||
}
|
|
||||||
|
|
||||||
nextState(state: T | null, { syncValue }: { syncValue: boolean } = { syncValue: true }) {
|
nextState(state: T | null, { syncValue }: { syncValue: boolean } = { syncValue: true }) {
|
||||||
this.stateSubject.next({
|
this.stateSubject.next({
|
||||||
syncValue,
|
syncValue,
|
||||||
combinedState: [this.userId, state],
|
combinedState: [this.activeAccountAccessor.activeUserId, state],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,12 +212,12 @@ export class FakeActiveUserState<T> implements ActiveUserState<T> {
|
|||||||
? await firstValueFrom(options.combineLatestWith.pipe(timeout(options.msTimeout)))
|
? await firstValueFrom(options.combineLatestWith.pipe(timeout(options.msTimeout)))
|
||||||
: null;
|
: null;
|
||||||
if (!options.shouldUpdate(current, combinedDependencies)) {
|
if (!options.shouldUpdate(current, combinedDependencies)) {
|
||||||
return [this.userId, current];
|
return [this.activeAccountAccessor.activeUserId, current];
|
||||||
}
|
}
|
||||||
const newState = configureState(current, combinedDependencies);
|
const newState = configureState(current, combinedDependencies);
|
||||||
this.nextState(newState);
|
this.nextState(newState);
|
||||||
this.nextMock([this.userId, newState]);
|
this.nextMock([this.activeAccountAccessor.activeUserId, newState]);
|
||||||
return [this.userId, newState];
|
return [this.activeAccountAccessor.activeUserId, newState];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Tracks update values resolved by `FakeState.update` */
|
/** Tracks update values resolved by `FakeState.update` */
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { map, Observable } from "rxjs";
|
||||||
|
|
||||||
|
import { UserId } from "@bitwarden/user-core";
|
||||||
|
|
||||||
|
import { ActiveUserAccessor } from "../../platform/state";
|
||||||
|
import { AccountService } from "../abstractions/account.service";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation for Platform so they can avoid a direct dependency on AccountService. Not for general consumption.
|
||||||
|
*/
|
||||||
|
export class DefaultActiveUserAccessor implements ActiveUserAccessor {
|
||||||
|
constructor(private readonly accountService: AccountService) {
|
||||||
|
this.activeUserId$ = this.accountService.activeAccount$.pipe(
|
||||||
|
map((a) => (a != null ? a.id : null)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
activeUserId$: Observable<UserId | null>;
|
||||||
|
}
|
||||||
11
libs/common/src/platform/state/active-user.accessor.ts
Normal file
11
libs/common/src/platform/state/active-user.accessor.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { Observable } from "rxjs";
|
||||||
|
|
||||||
|
import { UserId } from "@bitwarden/user-core";
|
||||||
|
|
||||||
|
export abstract class ActiveUserAccessor {
|
||||||
|
/**
|
||||||
|
* Returns a stream of the current active user for the application. The stream either emits the user id for that account
|
||||||
|
* or returns null if there is no current active user.
|
||||||
|
*/
|
||||||
|
abstract activeUserId$: Observable<UserId | null>;
|
||||||
|
}
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
import { mock } from "jest-mock-extended";
|
|
||||||
|
|
||||||
import { mockAccountServiceWith, trackEmissions } from "../../../../spec";
|
|
||||||
import { UserId } from "../../../types/guid";
|
|
||||||
import { SingleUserStateProvider } from "../user-state.provider";
|
|
||||||
|
|
||||||
import { DefaultActiveUserStateProvider } from "./default-active-user-state.provider";
|
|
||||||
|
|
||||||
describe("DefaultActiveUserStateProvider", () => {
|
|
||||||
const singleUserStateProvider = mock<SingleUserStateProvider>();
|
|
||||||
const userId = "userId" as UserId;
|
|
||||||
const accountInfo = {
|
|
||||||
id: userId,
|
|
||||||
name: "name",
|
|
||||||
email: "email",
|
|
||||||
emailVerified: false,
|
|
||||||
};
|
|
||||||
const accountService = mockAccountServiceWith(userId, accountInfo);
|
|
||||||
let sut: DefaultActiveUserStateProvider;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
sut = new DefaultActiveUserStateProvider(accountService, singleUserStateProvider);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
jest.resetAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should track the active User id from account service", () => {
|
|
||||||
const emissions = trackEmissions(sut.activeUserId$);
|
|
||||||
|
|
||||||
accountService.activeAccountSubject.next(undefined);
|
|
||||||
accountService.activeAccountSubject.next(accountInfo);
|
|
||||||
|
|
||||||
expect(emissions).toEqual([userId, undefined, userId]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
// FIXME: Update this file to be type safe and remove this and next line
|
||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { Observable, distinctUntilChanged, map } from "rxjs";
|
import { Observable, distinctUntilChanged } from "rxjs";
|
||||||
|
|
||||||
import { AccountService } from "../../../auth/abstractions/account.service";
|
|
||||||
import { UserId } from "../../../types/guid";
|
import { UserId } from "../../../types/guid";
|
||||||
|
import { ActiveUserAccessor } from "../active-user.accessor";
|
||||||
import { UserKeyDefinition } from "../user-key-definition";
|
import { UserKeyDefinition } from "../user-key-definition";
|
||||||
import { ActiveUserState } from "../user-state";
|
import { ActiveUserState } from "../user-state";
|
||||||
import { ActiveUserStateProvider, SingleUserStateProvider } from "../user-state.provider";
|
import { ActiveUserStateProvider, SingleUserStateProvider } from "../user-state.provider";
|
||||||
@@ -14,11 +14,10 @@ export class DefaultActiveUserStateProvider implements ActiveUserStateProvider {
|
|||||||
activeUserId$: Observable<UserId | undefined>;
|
activeUserId$: Observable<UserId | undefined>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly accountService: AccountService,
|
private readonly activeAccountAccessor: ActiveUserAccessor,
|
||||||
private readonly singleUserStateProvider: SingleUserStateProvider,
|
private readonly singleUserStateProvider: SingleUserStateProvider,
|
||||||
) {
|
) {
|
||||||
this.activeUserId$ = this.accountService.activeAccount$.pipe(
|
this.activeUserId$ = this.activeAccountAccessor.activeUserId$.pipe(
|
||||||
map((account) => account?.id),
|
|
||||||
// To avoid going to storage when we don't need to, only get updates when there is a true change.
|
// To avoid going to storage when we don't need to, only get updates when there is a true change.
|
||||||
distinctUntilChanged((a, b) => (a == null || b == null ? a == b : a === b)), // Treat null and undefined as equal
|
distinctUntilChanged((a, b) => (a == null || b == null ? a == b : a === b)), // Treat null and undefined as equal
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,14 +3,13 @@
|
|||||||
* @jest-environment ../shared/test.environment.ts
|
* @jest-environment ../shared/test.environment.ts
|
||||||
*/
|
*/
|
||||||
import { any, mock } from "jest-mock-extended";
|
import { any, mock } from "jest-mock-extended";
|
||||||
import { BehaviorSubject, firstValueFrom, map, of, timeout } from "rxjs";
|
import { BehaviorSubject, firstValueFrom, of, timeout } from "rxjs";
|
||||||
import { Jsonify } from "type-fest";
|
import { Jsonify } from "type-fest";
|
||||||
|
|
||||||
import { StorageServiceProvider } from "@bitwarden/storage-core";
|
import { StorageServiceProvider } from "@bitwarden/storage-core";
|
||||||
|
|
||||||
import { awaitAsync, trackEmissions } from "../../../../spec";
|
import { awaitAsync, trackEmissions } from "../../../../spec";
|
||||||
import { FakeStorageService } from "../../../../spec/fake-storage.service";
|
import { FakeStorageService } from "../../../../spec/fake-storage.service";
|
||||||
import { Account } from "../../../auth/abstractions/account.service";
|
|
||||||
import { UserId } from "../../../types/guid";
|
import { UserId } from "../../../types/guid";
|
||||||
import { LogService } from "../../abstractions/log.service";
|
import { LogService } from "../../abstractions/log.service";
|
||||||
import { StateDefinition } from "../state-definition";
|
import { StateDefinition } from "../state-definition";
|
||||||
@@ -48,7 +47,7 @@ describe("DefaultActiveUserState", () => {
|
|||||||
const storageServiceProvider = mock<StorageServiceProvider>();
|
const storageServiceProvider = mock<StorageServiceProvider>();
|
||||||
const stateEventRegistrarService = mock<StateEventRegistrarService>();
|
const stateEventRegistrarService = mock<StateEventRegistrarService>();
|
||||||
const logService = mock<LogService>();
|
const logService = mock<LogService>();
|
||||||
let activeAccountSubject: BehaviorSubject<Account | null>;
|
let activeAccountSubject: BehaviorSubject<UserId | null>;
|
||||||
|
|
||||||
let singleUserStateProvider: DefaultSingleUserStateProvider;
|
let singleUserStateProvider: DefaultSingleUserStateProvider;
|
||||||
|
|
||||||
@@ -64,11 +63,11 @@ describe("DefaultActiveUserState", () => {
|
|||||||
logService,
|
logService,
|
||||||
);
|
);
|
||||||
|
|
||||||
activeAccountSubject = new BehaviorSubject<Account | null>(null);
|
activeAccountSubject = new BehaviorSubject<UserId | null>(null);
|
||||||
|
|
||||||
userState = new DefaultActiveUserState(
|
userState = new DefaultActiveUserState(
|
||||||
testKeyDefinition,
|
testKeyDefinition,
|
||||||
activeAccountSubject.asObservable().pipe(map((a) => a?.id)),
|
activeAccountSubject.asObservable(),
|
||||||
singleUserStateProvider,
|
singleUserStateProvider,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -83,12 +82,7 @@ describe("DefaultActiveUserState", () => {
|
|||||||
|
|
||||||
const changeActiveUser = async (id: string) => {
|
const changeActiveUser = async (id: string) => {
|
||||||
const userId = makeUserId(id);
|
const userId = makeUserId(id);
|
||||||
activeAccountSubject.next({
|
activeAccountSubject.next(userId);
|
||||||
id: userId,
|
|
||||||
email: `test${id}@example.com`,
|
|
||||||
emailVerified: false,
|
|
||||||
name: `Test User ${id}`,
|
|
||||||
});
|
|
||||||
await awaitAsync();
|
await awaitAsync();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -588,7 +582,7 @@ describe("DefaultActiveUserState", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("does not await updates if the active user changes", async () => {
|
it("does not await updates if the active user changes", async () => {
|
||||||
const initialUserId = (await firstValueFrom(activeAccountSubject)).id;
|
const initialUserId = activeAccountSubject.value;
|
||||||
expect(initialUserId).toBe(userId);
|
expect(initialUserId).toBe(userId);
|
||||||
trackEmissions(userState.state$);
|
trackEmissions(userState.state$);
|
||||||
await awaitAsync(); // storage updates are behind a promise
|
await awaitAsync(); // storage updates are behind a promise
|
||||||
|
|||||||
@@ -4,16 +4,16 @@
|
|||||||
*/
|
*/
|
||||||
import { Observable, of } from "rxjs";
|
import { Observable, of } from "rxjs";
|
||||||
|
|
||||||
|
import { UserId } from "@bitwarden/user-core";
|
||||||
|
|
||||||
import { awaitAsync, trackEmissions } from "../../../../spec";
|
import { awaitAsync, trackEmissions } from "../../../../spec";
|
||||||
import { FakeAccountService, mockAccountServiceWith } from "../../../../spec/fake-account-service";
|
|
||||||
import {
|
import {
|
||||||
|
FakeActiveUserAccessor,
|
||||||
FakeActiveUserStateProvider,
|
FakeActiveUserStateProvider,
|
||||||
FakeDerivedStateProvider,
|
FakeDerivedStateProvider,
|
||||||
FakeGlobalStateProvider,
|
FakeGlobalStateProvider,
|
||||||
FakeSingleUserStateProvider,
|
FakeSingleUserStateProvider,
|
||||||
} from "../../../../spec/fake-state-provider";
|
} from "../../../../spec/fake-state-provider";
|
||||||
import { AuthenticationStatus } from "../../../auth/enums/authentication-status";
|
|
||||||
import { UserId } from "../../../types/guid";
|
|
||||||
import { DeriveDefinition } from "../derive-definition";
|
import { DeriveDefinition } from "../derive-definition";
|
||||||
import { KeyDefinition } from "../key-definition";
|
import { KeyDefinition } from "../key-definition";
|
||||||
import { StateDefinition } from "../state-definition";
|
import { StateDefinition } from "../state-definition";
|
||||||
@@ -27,12 +27,12 @@ describe("DefaultStateProvider", () => {
|
|||||||
let singleUserStateProvider: FakeSingleUserStateProvider;
|
let singleUserStateProvider: FakeSingleUserStateProvider;
|
||||||
let globalStateProvider: FakeGlobalStateProvider;
|
let globalStateProvider: FakeGlobalStateProvider;
|
||||||
let derivedStateProvider: FakeDerivedStateProvider;
|
let derivedStateProvider: FakeDerivedStateProvider;
|
||||||
let accountService: FakeAccountService;
|
let activeAccountAccessor: FakeActiveUserAccessor;
|
||||||
const userId = "fakeUserId" as UserId;
|
const userId = "fakeUserId" as UserId;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
accountService = mockAccountServiceWith(userId);
|
activeAccountAccessor = new FakeActiveUserAccessor(userId);
|
||||||
activeUserStateProvider = new FakeActiveUserStateProvider(accountService);
|
activeUserStateProvider = new FakeActiveUserStateProvider(activeAccountAccessor);
|
||||||
singleUserStateProvider = new FakeSingleUserStateProvider();
|
singleUserStateProvider = new FakeSingleUserStateProvider();
|
||||||
globalStateProvider = new FakeGlobalStateProvider();
|
globalStateProvider = new FakeGlobalStateProvider();
|
||||||
derivedStateProvider = new FakeDerivedStateProvider();
|
derivedStateProvider = new FakeDerivedStateProvider();
|
||||||
@@ -70,12 +70,6 @@ describe("DefaultStateProvider", () => {
|
|||||||
userId?: UserId,
|
userId?: UserId,
|
||||||
) => Observable<string>,
|
) => Observable<string>,
|
||||||
) => {
|
) => {
|
||||||
const accountInfo = {
|
|
||||||
email: "email",
|
|
||||||
emailVerified: false,
|
|
||||||
name: "name",
|
|
||||||
status: AuthenticationStatus.LoggedOut,
|
|
||||||
};
|
|
||||||
const keyDefinition = new UserKeyDefinition<string>(
|
const keyDefinition = new UserKeyDefinition<string>(
|
||||||
new StateDefinition("test", "disk"),
|
new StateDefinition("test", "disk"),
|
||||||
"test",
|
"test",
|
||||||
@@ -97,7 +91,7 @@ describe("DefaultStateProvider", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should follow the current active user if no userId is provided", async () => {
|
it("should follow the current active user if no userId is provided", async () => {
|
||||||
accountService.activeAccountSubject.next({ id: userId, ...accountInfo });
|
activeAccountAccessor.switch(userId);
|
||||||
const state = singleUserStateProvider.getFake(userId, keyDefinition);
|
const state = singleUserStateProvider.getFake(userId, keyDefinition);
|
||||||
state.nextState("value");
|
state.nextState("value");
|
||||||
const emissions = trackEmissions(methodUnderTest(keyDefinition));
|
const emissions = trackEmissions(methodUnderTest(keyDefinition));
|
||||||
@@ -113,7 +107,7 @@ describe("DefaultStateProvider", () => {
|
|||||||
state.nextState("value");
|
state.nextState("value");
|
||||||
const emissions = trackEmissions(methodUnderTest(keyDefinition));
|
const emissions = trackEmissions(methodUnderTest(keyDefinition));
|
||||||
|
|
||||||
accountService.activeAccountSubject.next({ id: "newUserId" as UserId, ...accountInfo });
|
activeAccountAccessor.switch("newUserId" as UserId);
|
||||||
const newUserEmissions = trackEmissions(sut.getUserState$(keyDefinition));
|
const newUserEmissions = trackEmissions(sut.getUserState$(keyDefinition));
|
||||||
state.nextState("value2");
|
state.nextState("value2");
|
||||||
state.nextState("value3");
|
state.nextState("value3");
|
||||||
@@ -125,12 +119,6 @@ describe("DefaultStateProvider", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
describe("getUserState$", () => {
|
describe("getUserState$", () => {
|
||||||
const accountInfo = {
|
|
||||||
email: "email",
|
|
||||||
emailVerified: false,
|
|
||||||
name: "name",
|
|
||||||
status: AuthenticationStatus.LoggedOut,
|
|
||||||
};
|
|
||||||
const keyDefinition = new UserKeyDefinition<string>(
|
const keyDefinition = new UserKeyDefinition<string>(
|
||||||
new StateDefinition("test", "disk"),
|
new StateDefinition("test", "disk"),
|
||||||
"test",
|
"test",
|
||||||
@@ -141,7 +129,7 @@ describe("DefaultStateProvider", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
it("should not emit any values until a truthy user id is supplied", async () => {
|
it("should not emit any values until a truthy user id is supplied", async () => {
|
||||||
accountService.activeAccountSubject.next(null);
|
activeAccountAccessor.switch(null);
|
||||||
const state = singleUserStateProvider.getFake(userId, keyDefinition);
|
const state = singleUserStateProvider.getFake(userId, keyDefinition);
|
||||||
state.nextState("value");
|
state.nextState("value");
|
||||||
|
|
||||||
@@ -151,7 +139,7 @@ describe("DefaultStateProvider", () => {
|
|||||||
|
|
||||||
expect(emissions).toHaveLength(0);
|
expect(emissions).toHaveLength(0);
|
||||||
|
|
||||||
accountService.activeAccountSubject.next({ id: userId, ...accountInfo });
|
activeAccountAccessor.switch(userId);
|
||||||
|
|
||||||
await awaitAsync();
|
await awaitAsync();
|
||||||
|
|
||||||
@@ -170,7 +158,7 @@ describe("DefaultStateProvider", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
it("should emit default value if no userId supplied and first active user id emission in falsy", async () => {
|
it("should emit default value if no userId supplied and first active user id emission in falsy", async () => {
|
||||||
accountService.activeAccountSubject.next(null);
|
activeAccountAccessor.switch(null);
|
||||||
|
|
||||||
const emissions = trackEmissions(
|
const emissions = trackEmissions(
|
||||||
sut.getUserStateOrDefault$(keyDefinition, {
|
sut.getUserStateOrDefault$(keyDefinition, {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { mock } from "jest-mock-extended";
|
|||||||
|
|
||||||
import { StorageServiceProvider } from "@bitwarden/storage-core";
|
import { StorageServiceProvider } from "@bitwarden/storage-core";
|
||||||
|
|
||||||
import { mockAccountServiceWith } from "../../../../spec/fake-account-service";
|
import { FakeActiveUserAccessor } from "../../../../spec";
|
||||||
import { FakeStorageService } from "../../../../spec/fake-storage.service";
|
import { FakeStorageService } from "../../../../spec/fake-storage.service";
|
||||||
import { UserId } from "../../../types/guid";
|
import { UserId } from "../../../types/guid";
|
||||||
import { LogService } from "../../abstractions/log.service";
|
import { LogService } from "../../abstractions/log.service";
|
||||||
@@ -39,7 +39,7 @@ describe("Specific State Providers", () => {
|
|||||||
stateEventRegistrarService,
|
stateEventRegistrarService,
|
||||||
logService,
|
logService,
|
||||||
);
|
);
|
||||||
activeSut = new DefaultActiveUserStateProvider(mockAccountServiceWith(null), singleSut);
|
activeSut = new DefaultActiveUserStateProvider(new FakeActiveUserAccessor(null), singleSut);
|
||||||
globalSut = new DefaultGlobalStateProvider(storageServiceProvider, logService);
|
globalSut = new DefaultGlobalStateProvider(storageServiceProvider, logService);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -10,5 +10,6 @@ export { KeyDefinition, KeyDefinitionOptions } from "./key-definition";
|
|||||||
export { StateUpdateOptions } from "./state-update-options";
|
export { StateUpdateOptions } from "./state-update-options";
|
||||||
export { UserKeyDefinitionOptions, UserKeyDefinition } from "./user-key-definition";
|
export { UserKeyDefinitionOptions, UserKeyDefinition } from "./user-key-definition";
|
||||||
export { StateEventRunnerService } from "./state-event-runner.service";
|
export { StateEventRunnerService } from "./state-event-runner.service";
|
||||||
|
export { ActiveUserAccessor } from "./active-user.accessor";
|
||||||
|
|
||||||
export * from "./state-definitions";
|
export * from "./state-definitions";
|
||||||
|
|||||||
Reference in New Issue
Block a user