1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-10 05:30:01 +00:00

Move service to live alongside component

This commit is contained in:
Alec Rippberger
2025-04-15 16:42:10 -05:00
parent 6ee84f502c
commit f6a4e30fbb
3 changed files with 15 additions and 15 deletions

View File

@@ -0,0 +1,191 @@
import { TestBed } from "@angular/core/testing";
import { mock, MockProxy } from "jest-mock-extended";
import { BehaviorSubject } from "rxjs";
import { ViewCacheService } from "@bitwarden/angular/platform/abstractions/view-cache.service";
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import {
TwoFactorAuthComponentCache,
TwoFactorAuthComponentCacheService,
TwoFactorAuthComponentData,
} from "./two-factor-auth-component-cache.service";
describe("TwoFactorAuthCache", () => {
describe("fromJSON", () => {
it("returns null when input is null", () => {
const result = TwoFactorAuthComponentCache.fromJSON(null as any);
expect(result).toBeNull();
});
it("creates a TwoFactorAuthCache instance from valid JSON", () => {
const jsonData = {
token: "123456",
remember: true,
selectedProviderType: TwoFactorProviderType.Email,
};
const result = TwoFactorAuthComponentCache.fromJSON(jsonData as any);
expect(result).not.toBeNull();
expect(result).toBeInstanceOf(TwoFactorAuthComponentCache);
expect(result?.token).toBe("123456");
expect(result?.remember).toBe(true);
expect(result?.selectedProviderType).toBe(TwoFactorProviderType.Email);
});
});
});
describe("TwoFactorAuthComponentCacheService", () => {
let service: TwoFactorAuthComponentCacheService;
let mockViewCacheService: MockProxy<ViewCacheService>;
let mockConfigService: MockProxy<ConfigService>;
let cacheData: BehaviorSubject<TwoFactorAuthComponentCache | null>;
let mockSignal: any;
beforeEach(() => {
mockViewCacheService = mock<ViewCacheService>();
mockConfigService = mock<ConfigService>();
cacheData = new BehaviorSubject<TwoFactorAuthComponentCache | null>(null);
mockSignal = jest.fn(() => cacheData.getValue());
mockSignal.set = jest.fn((value: TwoFactorAuthComponentCache | null) => cacheData.next(value));
mockViewCacheService.signal.mockReturnValue(mockSignal);
TestBed.configureTestingModule({
providers: [
TwoFactorAuthComponentCacheService,
{ provide: ViewCacheService, useValue: mockViewCacheService },
{ provide: ConfigService, useValue: mockConfigService },
],
});
service = TestBed.inject(TwoFactorAuthComponentCacheService);
});
it("creates the service", () => {
expect(service).toBeTruthy();
});
describe("init", () => {
it("sets featureEnabled to true when flag is enabled", async () => {
mockConfigService.getFeatureFlag.mockResolvedValue(true);
await service.init();
expect(mockConfigService.getFeatureFlag).toHaveBeenCalledWith(
FeatureFlag.PM9115_TwoFactorExtensionDataPersistence,
);
service.cacheData({ token: "123456" });
expect(mockSignal.set).toHaveBeenCalled();
});
it("sets featureEnabled to false when flag is disabled", async () => {
mockConfigService.getFeatureFlag.mockResolvedValue(false);
await service.init();
expect(mockConfigService.getFeatureFlag).toHaveBeenCalledWith(
FeatureFlag.PM9115_TwoFactorExtensionDataPersistence,
);
service.cacheData({ token: "123456" });
expect(mockSignal.set).not.toHaveBeenCalled();
});
});
describe("cacheData", () => {
beforeEach(async () => {
mockConfigService.getFeatureFlag.mockResolvedValue(true);
await service.init();
});
it("caches complete data when feature is enabled", () => {
const testData: TwoFactorAuthComponentData = {
token: "123456",
remember: true,
selectedProviderType: TwoFactorProviderType.Email,
};
service.cacheData(testData);
expect(mockSignal.set).toHaveBeenCalledWith({
token: "123456",
remember: true,
selectedProviderType: TwoFactorProviderType.Email,
});
});
it("caches partial data when feature is enabled", () => {
service.cacheData({ token: "123456" });
expect(mockSignal.set).toHaveBeenCalledWith({
token: "123456",
remember: undefined,
selectedProviderType: undefined,
});
});
it("does not cache data when feature is disabled", async () => {
mockConfigService.getFeatureFlag.mockResolvedValue(false);
await service.init();
service.cacheData({ token: "123456" });
expect(mockSignal.set).not.toHaveBeenCalled();
});
});
describe("clearCachedData", () => {
beforeEach(async () => {
mockConfigService.getFeatureFlag.mockResolvedValue(true);
await service.init();
});
it("clears cached data when feature is enabled", () => {
service.clearCachedData();
expect(mockSignal.set).toHaveBeenCalledWith(null);
});
it("does not clear cached data when feature is disabled", async () => {
mockConfigService.getFeatureFlag.mockResolvedValue(false);
await service.init();
service.clearCachedData();
expect(mockSignal.set).not.toHaveBeenCalled();
});
});
describe("getCachedData", () => {
beforeEach(async () => {
mockConfigService.getFeatureFlag.mockResolvedValue(true);
await service.init();
});
it("returns cached data when feature is enabled", () => {
const testData = new TwoFactorAuthComponentCache();
testData.token = "123456";
testData.remember = true;
testData.selectedProviderType = TwoFactorProviderType.Email;
cacheData.next(testData);
const result = service.getCachedData();
expect(result).toEqual(testData);
expect(mockSignal).toHaveBeenCalled();
});
it("returns null when feature is disabled", async () => {
mockConfigService.getFeatureFlag.mockResolvedValue(false);
await service.init();
const result = service.getCachedData();
expect(result).toBeNull();
expect(mockSignal).not.toHaveBeenCalled();
});
});
});

View File

@@ -0,0 +1,108 @@
import { inject, Injectable, WritableSignal } from "@angular/core";
import { Jsonify } from "type-fest";
import { ViewCacheService } from "@bitwarden/angular/platform/abstractions/view-cache.service";
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
const TWO_FACTOR_AUTH_COMPONENT_CACHE_KEY = "two-factor-auth-component-cache";
/**
* This is a cache model for the two factor authentication data.
*/
export class TwoFactorAuthComponentCache {
token: string | undefined = undefined;
remember: boolean | undefined = undefined;
selectedProviderType: TwoFactorProviderType | undefined = undefined;
static fromJSON(
obj: Partial<Jsonify<TwoFactorAuthComponentCache>>,
): TwoFactorAuthComponentCache | null {
// Return null if the cache is empty
if (obj == null) {
return null;
}
return Object.assign(new TwoFactorAuthComponentCache(), obj);
}
}
export interface TwoFactorAuthComponentData {
token?: string;
remember?: boolean;
selectedProviderType?: TwoFactorProviderType;
}
/**
* This is a cache service used for the two factor auth component.
*
* There is sensitive information stored temporarily here. Cache will be cleared
* after 2 minutes.
*/
@Injectable()
export class TwoFactorAuthComponentCacheService {
private viewCacheService: ViewCacheService = inject(ViewCacheService);
private configService: ConfigService = inject(ConfigService);
/** True when the `PM9115_TwoFactorExtensionDataPersistence` flag is enabled */
private featureEnabled: boolean = false;
/**
* Signal for the cached TwoFactorAuthData.
*/
private twoFactorAuthComponentCache: WritableSignal<TwoFactorAuthComponentCache | null> =
this.viewCacheService.signal<TwoFactorAuthComponentCache | null>({
key: TWO_FACTOR_AUTH_COMPONENT_CACHE_KEY,
initialValue: null,
deserializer: TwoFactorAuthComponentCache.fromJSON,
});
constructor() {}
/**
* Must be called once before interacting with the cached data, otherwise methods will be noop.
*/
async init() {
this.featureEnabled = await this.configService.getFeatureFlag(
FeatureFlag.PM9115_TwoFactorExtensionDataPersistence,
);
}
/**
* Update the cache with the new TwoFactorAuthData.
*/
cacheData(data: TwoFactorAuthComponentData): void {
if (!this.featureEnabled) {
return;
}
this.twoFactorAuthComponentCache.set({
token: data.token,
remember: data.remember,
selectedProviderType: data.selectedProviderType,
} as TwoFactorAuthComponentCache);
}
/**
* Clears the cached TwoFactorAuthData.
*/
clearCachedData(): void {
if (!this.featureEnabled) {
return;
}
this.twoFactorAuthComponentCache.set(null);
}
/**
* Returns the cached TwoFactorAuthData when available.
*/
getCachedData(): TwoFactorAuthComponentCache | null {
if (!this.featureEnabled) {
return null;
}
return this.twoFactorAuthComponentCache();
}
}

View File

@@ -46,10 +46,6 @@ import {
ToastService,
} from "@bitwarden/components";
import {
TwoFactorAuthComponentCacheService,
TwoFactorAuthComponentData,
} from "../../common/services/auth-request/two-factor-auth-component-cache.service";
import { AnonLayoutWrapperDataService } from "../anon-layout/anon-layout-wrapper-data.service";
import {
TwoFactorAuthAuthenticatorIcon,
@@ -64,6 +60,10 @@ import { TwoFactorAuthDuoComponent } from "./child-components/two-factor-auth-du
import { TwoFactorAuthEmailComponent } from "./child-components/two-factor-auth-email/two-factor-auth-email.component";
import { TwoFactorAuthWebAuthnComponent } from "./child-components/two-factor-auth-webauthn/two-factor-auth-webauthn.component";
import { TwoFactorAuthYubikeyComponent } from "./child-components/two-factor-auth-yubikey.component";
import {
TwoFactorAuthComponentCacheService,
TwoFactorAuthComponentData,
} from "./two-factor-auth-component-cache.service";
import {
DuoLaunchAction,
LegacyKeyMigrationAction,