mirror of
https://github.com/bitwarden/browser
synced 2026-02-26 01:23:24 +00:00
Refactor TwoFactorFormCacheService
This commit is contained in:
@@ -1,207 +0,0 @@
|
||||
import { signal } from "@angular/core";
|
||||
import { TestBed } from "@angular/core/testing";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { ViewCacheService } from "@bitwarden/angular/platform/abstractions/view-cache.service";
|
||||
import { TwoFactorFormData } from "@bitwarden/auth/angular";
|
||||
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 { ExtensionTwoFactorFormCacheService } from "./extension-two-factor-form-cache.service";
|
||||
|
||||
describe("ExtensionTwoFactorFormCacheService", () => {
|
||||
let service: ExtensionTwoFactorFormCacheService;
|
||||
let testBed: TestBed;
|
||||
const formDataSignal = signal<TwoFactorFormData | null>(null);
|
||||
const getFormDataSignal = jest.fn().mockReturnValue(formDataSignal);
|
||||
const getFeatureFlag = jest.fn().mockResolvedValue(false);
|
||||
const formDataSetMock = jest.spyOn(formDataSignal, "set");
|
||||
|
||||
const mockFormData: TwoFactorFormData = {
|
||||
token: "123456",
|
||||
remember: true,
|
||||
selectedProviderType: TwoFactorProviderType.Authenticator,
|
||||
emailSent: false,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
getFormDataSignal.mockClear();
|
||||
getFeatureFlag.mockClear();
|
||||
formDataSetMock.mockClear();
|
||||
|
||||
testBed = TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{ provide: ViewCacheService, useValue: { signal: getFormDataSignal } },
|
||||
{ provide: ConfigService, useValue: { getFeatureFlag } },
|
||||
ExtensionTwoFactorFormCacheService,
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
describe("feature enabled", () => {
|
||||
beforeEach(async () => {
|
||||
getFeatureFlag.mockImplementation((featureFlag: FeatureFlag) => {
|
||||
if (featureFlag === FeatureFlag.PM9115_TwoFactorExtensionDataPersistence) {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
return Promise.resolve(false);
|
||||
});
|
||||
|
||||
service = testBed.inject(ExtensionTwoFactorFormCacheService);
|
||||
});
|
||||
|
||||
describe("isEnabled$", () => {
|
||||
it("emits true when feature flag is on", async () => {
|
||||
const result = await firstValueFrom(service.isEnabled$());
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(getFeatureFlag).toHaveBeenCalledWith(
|
||||
FeatureFlag.PM9115_TwoFactorExtensionDataPersistence,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isEnabled", () => {
|
||||
it("returns true when feature flag is on", async () => {
|
||||
const result = await service.isEnabled();
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(getFeatureFlag).toHaveBeenCalledWith(
|
||||
FeatureFlag.PM9115_TwoFactorExtensionDataPersistence,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getFormData", () => {
|
||||
it("returns cached form data", async () => {
|
||||
formDataSignal.set(mockFormData);
|
||||
|
||||
const result = await service.getFormData();
|
||||
|
||||
expect(result).toEqual(mockFormData);
|
||||
});
|
||||
|
||||
it("returns null when cache is empty", async () => {
|
||||
formDataSignal.set(null);
|
||||
|
||||
const result = await service.getFormData();
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("formData$", () => {
|
||||
it("emits cached form data", async () => {
|
||||
formDataSignal.set(mockFormData);
|
||||
|
||||
const result = await firstValueFrom(service.formData$());
|
||||
|
||||
expect(result).toEqual(mockFormData);
|
||||
});
|
||||
|
||||
it("emits null when cache is empty", async () => {
|
||||
formDataSignal.set(null);
|
||||
|
||||
const result = await firstValueFrom(service.formData$());
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("saveFormData", () => {
|
||||
it("updates the cached form data", async () => {
|
||||
await service.saveFormData(mockFormData);
|
||||
|
||||
expect(formDataSetMock).toHaveBeenCalledWith({ ...mockFormData });
|
||||
});
|
||||
|
||||
it("creates a shallow copy of the data", async () => {
|
||||
const data = { ...mockFormData };
|
||||
|
||||
await service.saveFormData(data);
|
||||
|
||||
expect(formDataSetMock).toHaveBeenCalledWith(data);
|
||||
// Should be a new object, not the same reference
|
||||
expect(formDataSetMock.mock.calls[0][0]).not.toBe(data);
|
||||
});
|
||||
});
|
||||
|
||||
describe("clearFormData", () => {
|
||||
it("sets the cache to null", async () => {
|
||||
await service.clearFormData();
|
||||
|
||||
expect(formDataSetMock).toHaveBeenCalledWith(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("feature disabled", () => {
|
||||
beforeEach(async () => {
|
||||
formDataSignal.set(mockFormData);
|
||||
getFeatureFlag.mockImplementation((featureFlag: FeatureFlag) => {
|
||||
if (featureFlag === FeatureFlag.PM9115_TwoFactorExtensionDataPersistence) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
return Promise.resolve(false);
|
||||
});
|
||||
|
||||
service = testBed.inject(ExtensionTwoFactorFormCacheService);
|
||||
formDataSetMock.mockClear();
|
||||
});
|
||||
|
||||
describe("isEnabled$", () => {
|
||||
it("emits false when feature flag is off", async () => {
|
||||
const result = await firstValueFrom(service.isEnabled$());
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(getFeatureFlag).toHaveBeenCalledWith(
|
||||
FeatureFlag.PM9115_TwoFactorExtensionDataPersistence,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isEnabled", () => {
|
||||
it("returns false when feature flag is off", async () => {
|
||||
const result = await service.isEnabled();
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(getFeatureFlag).toHaveBeenCalledWith(
|
||||
FeatureFlag.PM9115_TwoFactorExtensionDataPersistence,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("formData$", () => {
|
||||
it("emits null when feature is disabled", async () => {
|
||||
const result = await firstValueFrom(service.formData$());
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getFormData", () => {
|
||||
it("returns null when feature is disabled", async () => {
|
||||
const result = await service.getFormData();
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("saveFormData", () => {
|
||||
it("does not update cache when feature is disabled", async () => {
|
||||
await service.saveFormData(mockFormData);
|
||||
|
||||
expect(formDataSetMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("clearFormData", () => {
|
||||
it("still works when feature is disabled", async () => {
|
||||
await service.clearFormData();
|
||||
|
||||
expect(formDataSetMock).toHaveBeenCalledWith(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,93 +0,0 @@
|
||||
import { Injectable, WritableSignal } from "@angular/core";
|
||||
import { Observable, of, switchMap, from } from "rxjs";
|
||||
|
||||
import { ViewCacheService } from "@bitwarden/angular/platform/abstractions/view-cache.service";
|
||||
import { TwoFactorFormCacheService, TwoFactorFormData } from "@bitwarden/auth/angular";
|
||||
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_FORM_CACHE_KEY = "two-factor-form-cache";
|
||||
|
||||
// Utilize function overloading to create a type-safe deserializer to match the exact expected signature
|
||||
function deserializeFormData(jsonValue: null): null;
|
||||
function deserializeFormData(jsonValue: {
|
||||
token?: string;
|
||||
remember?: boolean;
|
||||
selectedProviderType?: TwoFactorProviderType;
|
||||
emailSent?: boolean;
|
||||
}): TwoFactorFormData;
|
||||
function deserializeFormData(jsonValue: any): TwoFactorFormData | null {
|
||||
if (!jsonValue) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
token: jsonValue.token,
|
||||
remember: jsonValue.remember,
|
||||
selectedProviderType: jsonValue.selectedProviderType,
|
||||
emailSent: jsonValue.emailSent,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Service for caching two-factor form data
|
||||
*/
|
||||
@Injectable()
|
||||
export class ExtensionTwoFactorFormCacheService extends TwoFactorFormCacheService {
|
||||
private formDataCache: WritableSignal<TwoFactorFormData | null>;
|
||||
|
||||
constructor(
|
||||
private viewCacheService: ViewCacheService,
|
||||
private configService: ConfigService,
|
||||
) {
|
||||
super();
|
||||
this.formDataCache = this.viewCacheService.signal<TwoFactorFormData | null>({
|
||||
key: TWO_FACTOR_FORM_CACHE_KEY,
|
||||
initialValue: null,
|
||||
deserializer: deserializeFormData,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Observable that emits the current enabled state
|
||||
*/
|
||||
isEnabled$(): Observable<boolean> {
|
||||
return from(
|
||||
this.configService.getFeatureFlag(FeatureFlag.PM9115_TwoFactorExtensionDataPersistence),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Observable that emits the current form data
|
||||
*/
|
||||
formData$(): Observable<TwoFactorFormData | null> {
|
||||
return this.isEnabled$().pipe(
|
||||
switchMap((enabled) => {
|
||||
if (!enabled) {
|
||||
return of(null);
|
||||
}
|
||||
return of(this.formDataCache());
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save form data to cache
|
||||
*/
|
||||
async saveFormData(data: TwoFactorFormData): Promise<void> {
|
||||
if (!(await this.isEnabled())) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set the new form data in the cache
|
||||
this.formDataCache.set({ ...data });
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear form data from cache
|
||||
*/
|
||||
async clearFormData(): Promise<void> {
|
||||
this.formDataCache.set(null);
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,6 @@ import {
|
||||
TwoFactorAuthDuoComponentService,
|
||||
TwoFactorAuthWebAuthnComponentService,
|
||||
SsoComponentService,
|
||||
TwoFactorFormCacheService,
|
||||
} from "@bitwarden/auth/angular";
|
||||
import {
|
||||
LockService,
|
||||
@@ -147,7 +146,6 @@ import { ExtensionTwoFactorAuthComponentService } from "../../auth/services/exte
|
||||
import { ExtensionTwoFactorAuthDuoComponentService } from "../../auth/services/extension-two-factor-auth-duo-component.service";
|
||||
import { ExtensionTwoFactorAuthEmailComponentService } from "../../auth/services/extension-two-factor-auth-email-component.service";
|
||||
import { ExtensionTwoFactorAuthWebAuthnComponentService } from "../../auth/services/extension-two-factor-auth-webauthn-component.service";
|
||||
import { ExtensionTwoFactorFormCacheService } from "../../auth/services/extension-two-factor-form-cache.service";
|
||||
import { AutofillService as AutofillServiceAbstraction } from "../../autofill/services/abstractions/autofill.service";
|
||||
import AutofillService from "../../autofill/services/autofill.service";
|
||||
import { InlineMenuFieldQualificationService } from "../../autofill/services/inline-menu-field-qualification.service";
|
||||
@@ -566,11 +564,6 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: ExtensionTwoFactorAuthWebAuthnComponentService,
|
||||
deps: [],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: TwoFactorFormCacheService,
|
||||
useClass: ExtensionTwoFactorFormCacheService,
|
||||
deps: [PopupViewCacheService, ConfigService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: TwoFactorAuthDuoComponentService,
|
||||
useClass: ExtensionTwoFactorAuthDuoComponentService,
|
||||
|
||||
@@ -28,7 +28,6 @@ import {
|
||||
SsoComponentService,
|
||||
DefaultSsoComponentService,
|
||||
TwoFactorAuthDuoComponentService,
|
||||
TwoFactorFormCacheService,
|
||||
} from "@bitwarden/auth/angular";
|
||||
import {
|
||||
InternalUserDecryptionOptionsServiceAbstraction,
|
||||
@@ -110,7 +109,6 @@ import { DefaultSshImportPromptService, SshImportPromptService } from "@bitwarde
|
||||
import { DesktopLoginApprovalComponentService } from "../../auth/login/desktop-login-approval-component.service";
|
||||
import { DesktopLoginComponentService } from "../../auth/login/desktop-login-component.service";
|
||||
import { DesktopTwoFactorAuthDuoComponentService } from "../../auth/services/desktop-two-factor-auth-duo-component.service";
|
||||
import { DesktopTwoFactorFormCacheService } from "../../auth/services/desktop-two-factor-form-cache.service";
|
||||
import { DesktopAutofillSettingsService } from "../../autofill/services/desktop-autofill-settings.service";
|
||||
import { DesktopAutofillService } from "../../autofill/services/desktop-autofill.service";
|
||||
import { DesktopFido2UserInterfaceService } from "../../autofill/services/desktop-fido2-user-interface.service";
|
||||
@@ -423,11 +421,6 @@ const safeProviders: SafeProvider[] = [
|
||||
PlatformUtilsServiceAbstraction,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: TwoFactorFormCacheService,
|
||||
useClass: DesktopTwoFactorFormCacheService,
|
||||
deps: [],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: SdkClientFactory,
|
||||
useClass: flagEnabled("sdk") ? DefaultSdkClientFactory : NoopSdkClientFactory,
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import { Observable, of } from "rxjs";
|
||||
|
||||
import { TwoFactorFormCacheService, TwoFactorFormData } from "@bitwarden/auth/angular";
|
||||
|
||||
/**
|
||||
* No-op implementation of TwoFactorFormCacheService for desktop
|
||||
*/
|
||||
@Injectable()
|
||||
export class DesktopTwoFactorFormCacheService extends TwoFactorFormCacheService {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
isEnabled$(): Observable<boolean> {
|
||||
return of(false);
|
||||
}
|
||||
|
||||
formData$(): Observable<TwoFactorFormData | null> {
|
||||
return of(null);
|
||||
}
|
||||
|
||||
async saveFormData(): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async clearFormData(): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,2 @@
|
||||
export * from "./web-two-factor-auth-component.service";
|
||||
export * from "./web-two-factor-auth-duo-component.service";
|
||||
export * from "./web-two-factor-form-cache.service";
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import { Observable, of } from "rxjs";
|
||||
|
||||
import { TwoFactorFormCacheService, TwoFactorFormData } from "@bitwarden/auth/angular";
|
||||
|
||||
/**
|
||||
* No-op implementation of TwoFactorFormCacheService for web app
|
||||
*/
|
||||
@Injectable()
|
||||
export class WebTwoFactorFormCacheService extends TwoFactorFormCacheService {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
isEnabled$(): Observable<boolean> {
|
||||
return of(false);
|
||||
}
|
||||
|
||||
formData$(): Observable<TwoFactorFormData | null> {
|
||||
return of(null);
|
||||
}
|
||||
|
||||
async saveFormData(): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async clearFormData(): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,6 @@ import {
|
||||
LoginDecryptionOptionsService,
|
||||
TwoFactorAuthComponentService,
|
||||
TwoFactorAuthDuoComponentService,
|
||||
TwoFactorFormCacheService,
|
||||
} from "@bitwarden/auth/angular";
|
||||
import {
|
||||
InternalUserDecryptionOptionsServiceAbstraction,
|
||||
@@ -120,7 +119,6 @@ import {
|
||||
LinkSsoService,
|
||||
} from "../auth";
|
||||
import { WebSsoComponentService } from "../auth/core/services/login/web-sso-component.service";
|
||||
import { WebTwoFactorFormCacheService } from "../auth/core/services/two-factor-auth/web-two-factor-form-cache.service";
|
||||
import { AcceptOrganizationInviteService } from "../auth/organization-invite/accept-organization.service";
|
||||
import { HtmlStorageService } from "../core/html-storage.service";
|
||||
import { I18nService } from "../core/i18n.service";
|
||||
@@ -279,11 +277,6 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: WebTwoFactorAuthComponentService,
|
||||
deps: [],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: TwoFactorFormCacheService,
|
||||
useClass: WebTwoFactorFormCacheService,
|
||||
deps: [],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: SetPasswordJitService,
|
||||
useClass: WebSetPasswordJitService,
|
||||
|
||||
Reference in New Issue
Block a user