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:
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user