1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-17 08:43:33 +00:00

getUserState$ Helper Improvements (#8267)

* Block Sending Null to `getUser`

* Update Comments & Tests

Co-authored-by: Matt Gibson <mgibson@bitwarden.com>

* Update Comment

* Update Fake

---------

Co-authored-by: Matt Gibson <mgibson@bitwarden.com>
This commit is contained in:
Justin Baur
2024-03-15 08:05:04 -05:00
committed by GitHub
parent c92cdb6e84
commit 1e921eb4f6
4 changed files with 174 additions and 31 deletions

View File

@@ -2,9 +2,9 @@
* need to update test environment so structuredClone works appropriately
* @jest-environment ../shared/test.environment.ts
*/
import { of } from "rxjs";
import { Observable, of } from "rxjs";
import { trackEmissions } from "../../../../spec";
import { awaitAsync, trackEmissions } from "../../../../spec";
import { FakeAccountService, mockAccountServiceWith } from "../../../../spec/fake-account-service";
import {
FakeActiveUserStateProvider,
@@ -49,47 +49,111 @@ describe("DefaultStateProvider", () => {
});
});
describe.each([
[
"getUserState$",
(keyDefinition: KeyDefinition<string>, userId?: UserId) =>
sut.getUserState$(keyDefinition, userId),
],
[
"getUserStateOrDefault$",
(keyDefinition: KeyDefinition<string>, userId?: UserId) =>
sut.getUserStateOrDefault$(keyDefinition, { userId: userId }),
],
])(
"Shared behavior for %s",
(
_testName: string,
methodUnderTest: (
keyDefinition: KeyDefinition<string>,
userId?: UserId,
) => Observable<string>,
) => {
const accountInfo = { email: "email", name: "name", status: AuthenticationStatus.LoggedOut };
const keyDefinition = new KeyDefinition<string>(new StateDefinition("test", "disk"), "test", {
deserializer: (s) => s,
});
it("should follow the specified user if userId is provided", async () => {
const state = singleUserStateProvider.getFake(userId, keyDefinition);
state.nextState("value");
const emissions = trackEmissions(methodUnderTest(keyDefinition, userId));
state.nextState("value2");
state.nextState("value3");
expect(emissions).toEqual(["value", "value2", "value3"]);
});
it("should follow the current active user if no userId is provided", async () => {
accountService.activeAccountSubject.next({ id: userId, ...accountInfo });
const state = singleUserStateProvider.getFake(userId, keyDefinition);
state.nextState("value");
const emissions = trackEmissions(methodUnderTest(keyDefinition));
state.nextState("value2");
state.nextState("value3");
expect(emissions).toEqual(["value", "value2", "value3"]);
});
it("should continue to follow the state of the user that was active when called, even if active user changes", async () => {
const state = singleUserStateProvider.getFake(userId, keyDefinition);
state.nextState("value");
const emissions = trackEmissions(methodUnderTest(keyDefinition));
accountService.activeAccountSubject.next({ id: "newUserId" as UserId, ...accountInfo });
const newUserEmissions = trackEmissions(sut.getUserState$(keyDefinition));
state.nextState("value2");
state.nextState("value3");
expect(emissions).toEqual(["value", "value2", "value3"]);
expect(newUserEmissions).toEqual([null]);
});
},
);
describe("getUserState$", () => {
const accountInfo = { email: "email", name: "name", status: AuthenticationStatus.LoggedOut };
const keyDefinition = new KeyDefinition<string>(new StateDefinition("test", "disk"), "test", {
deserializer: (s) => s,
});
it("should follow the specified user if userId is provided", async () => {
it("should not emit any values until a truthy user id is supplied", async () => {
accountService.activeAccountSubject.next(null);
const state = singleUserStateProvider.getFake(userId, keyDefinition);
state.nextState("value");
const emissions = trackEmissions(sut.getUserState$(keyDefinition, userId));
state.stateSubject.next([userId, "value"]);
state.nextState("value2");
state.nextState("value3");
const emissions = trackEmissions(sut.getUserState$(keyDefinition));
expect(emissions).toEqual(["value", "value2", "value3"]);
});
await awaitAsync();
expect(emissions).toHaveLength(0);
it("should follow the current active user if no userId is provided", async () => {
accountService.activeAccountSubject.next({ id: userId, ...accountInfo });
const state = singleUserStateProvider.getFake(userId, keyDefinition);
state.nextState("value");
const emissions = trackEmissions(sut.getUserState$(keyDefinition));
state.nextState("value2");
state.nextState("value3");
await awaitAsync();
expect(emissions).toEqual(["value", "value2", "value3"]);
expect(emissions).toEqual(["value"]);
});
});
describe("getUserStateOrDefault$", () => {
const keyDefinition = new KeyDefinition<string>(new StateDefinition("test", "disk"), "test", {
deserializer: (s) => s,
});
it("should continue to follow the state of the user that was active when called, even if active user changes", async () => {
const state = singleUserStateProvider.getFake(userId, keyDefinition);
state.nextState("value");
const emissions = trackEmissions(sut.getUserState$(keyDefinition));
it("should emit default value if no userId supplied and first active user id emission in falsy", async () => {
accountService.activeAccountSubject.next(null);
accountService.activeAccountSubject.next({ id: "newUserId" as UserId, ...accountInfo });
const newUserEmissions = trackEmissions(sut.getUserState$(keyDefinition));
state.nextState("value2");
state.nextState("value3");
const emissions = trackEmissions(
sut.getUserStateOrDefault$(keyDefinition, {
userId: undefined,
defaultValue: "I'm default!",
}),
);
expect(emissions).toEqual(["value", "value2", "value3"]);
expect(newUserEmissions).toEqual([null]);
expect(emissions).toEqual(["I'm default!"]);
});
});

View File

@@ -1,4 +1,4 @@
import { Observable, switchMap, take } from "rxjs";
import { Observable, filter, of, switchMap, take } from "rxjs";
import { UserId } from "../../../types/guid";
import { DerivedStateDependencies } from "../../../types/state";
@@ -30,12 +30,30 @@ export class DefaultStateProvider implements StateProvider {
return this.getUser<T>(userId, keyDefinition).state$;
} else {
return this.activeUserId$.pipe(
filter((userId) => userId != null), // Filter out null-ish user ids since we can't get state for a null user id
take(1),
switchMap((userId) => this.getUser<T>(userId, keyDefinition).state$),
);
}
}
getUserStateOrDefault$<T>(
keyDefinition: KeyDefinition<T> | UserKeyDefinition<T>,
config: { userId: UserId | undefined; defaultValue?: T },
): Observable<T> {
const { userId, defaultValue = null } = config;
if (userId) {
return this.getUser<T>(userId, keyDefinition).state$;
} else {
return this.activeUserId$.pipe(
take(1),
switchMap((userId) =>
userId != null ? this.getUser<T>(userId, keyDefinition).state$ : of(defaultValue),
),
);
}
}
async setUserState<T>(
keyDefinition: KeyDefinition<T> | UserKeyDefinition<T>,
value: T,