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

Add and fix tests

This commit is contained in:
Alec Rippberger
2025-03-04 22:18:25 -06:00
parent 5823f4c849
commit a205892dcb
5 changed files with 253 additions and 32 deletions

View File

@@ -1,5 +1,6 @@
import { MockProxy, mock } from "jest-mock-extended";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { DialogService } from "@bitwarden/components";
// Must mock modules before importing
@@ -26,22 +27,26 @@ describe("ExtensionTwoFactorAuthEmailComponentService", () => {
let dialogService: MockProxy<DialogService>;
let window: MockProxy<Window>;
let configService: MockProxy<ConfigService>;
beforeEach(() => {
jest.clearAllMocks();
dialogService = mock<DialogService>();
window = mock<Window>();
configService = mock<ConfigService>();
extensionTwoFactorAuthEmailComponentService = new ExtensionTwoFactorAuthEmailComponentService(
dialogService,
window,
configService,
);
});
describe("openPopoutIfApprovedForEmail2fa", () => {
it("should open a popout if the user confirms the warning to popout the extension when in the popup", async () => {
// Arrange
configService.getFeatureFlag.mockResolvedValue(false);
dialogService.openSimpleDialog.mockResolvedValue(true);
jest.spyOn(BrowserPopupUtils, "inPopup").mockReturnValue(true);
@@ -61,6 +66,7 @@ describe("ExtensionTwoFactorAuthEmailComponentService", () => {
it("should not open a popout if the user cancels the warning to popout the extension when in the popup", async () => {
// Arrange
configService.getFeatureFlag.mockResolvedValue(false);
dialogService.openSimpleDialog.mockResolvedValue(false);
jest.spyOn(BrowserPopupUtils, "inPopup").mockReturnValue(true);
@@ -80,6 +86,7 @@ describe("ExtensionTwoFactorAuthEmailComponentService", () => {
it("should not open a popout if not in the popup", async () => {
// Arrange
configService.getFeatureFlag.mockResolvedValue(false);
jest.spyOn(BrowserPopupUtils, "inPopup").mockReturnValue(false);
// Act
@@ -89,5 +96,18 @@ describe("ExtensionTwoFactorAuthEmailComponentService", () => {
expect(dialogService.openSimpleDialog).not.toHaveBeenCalled();
expect(openTwoFactorAuthEmailPopout).not.toHaveBeenCalled();
});
it("does not prompt or open a popout if the feature flag is enabled", async () => {
// Arrange
configService.getFeatureFlag.mockResolvedValue(true);
jest.spyOn(BrowserPopupUtils, "inPopup").mockReturnValue(true);
// Act
await extensionTwoFactorAuthEmailComponentService.openPopoutIfApprovedForEmail2fa();
// Assert
expect(dialogService.openSimpleDialog).not.toHaveBeenCalled();
expect(openTwoFactorAuthEmailPopout).not.toHaveBeenCalled();
});
});
});

View File

@@ -0,0 +1,199 @@
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_TwoFactorFormPersistence) {
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_TwoFactorFormPersistence);
});
});
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_TwoFactorFormPersistence);
});
});
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_TwoFactorFormPersistence) {
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_TwoFactorFormPersistence);
});
});
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_TwoFactorFormPersistence);
});
});
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);
});
});
});
});

View File

@@ -1,5 +1,5 @@
import { Injectable, WritableSignal } from "@angular/core";
import { Observable, from, of, switchMap } from "rxjs";
import { Observable, of, switchMap, from } from "rxjs";
import { ViewCacheService } from "@bitwarden/angular/platform/abstractions/view-cache.service";
import { TwoFactorFormCacheService, TwoFactorFormData } from "@bitwarden/auth/angular";
@@ -49,14 +49,16 @@ export class ExtensionTwoFactorFormCacheService extends TwoFactorFormCacheServic
});
}
/**
* Observable that emits the current enabled state
*/
isEnabled$(): Observable<boolean> {
return from(this.configService.getFeatureFlag(FeatureFlag.PM9115_TwoFactorFormPersistence));
}
async isEnabled(): Promise<boolean> {
return await this.configService.getFeatureFlag(FeatureFlag.PM9115_TwoFactorFormPersistence);
}
/**
* Observable that emits the current form data
*/
formData$(): Observable<TwoFactorFormData | null> {
return this.isEnabled$().pipe(
switchMap((enabled) => {
@@ -80,17 +82,6 @@ export class ExtensionTwoFactorFormCacheService extends TwoFactorFormCacheServic
this.formDataCache.set({ ...data });
}
/**
* Retrieve form data from cache
*/
async getFormData(): Promise<TwoFactorFormData | null> {
if (!(await this.isEnabled())) {
return null;
}
return this.formDataCache();
}
/**
* Clear form data from cache
*/

View File

@@ -1,4 +1,4 @@
import { Observable } from "rxjs";
import { Observable, firstValueFrom } from "rxjs";
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
@@ -17,30 +17,36 @@ export interface TwoFactorFormData {
*/
export abstract class TwoFactorFormCacheService {
/**
* Check if the form persistence feature is enabled
*/
abstract isEnabled(): Promise<boolean>;
/**
* Observable that emits the current enabled state
* Observable that emits the current enabled state of the feature flag
*/
abstract isEnabled$(): Observable<boolean>;
/**
* Helper method that returns whether the feature is enabled
* @returns A promise that resolves to true if the feature is enabled
*/
async isEnabled(): Promise<boolean> {
return firstValueFrom(this.isEnabled$());
}
/**
* Save form data to persistent storage
*/
abstract saveFormData(data: TwoFactorFormData): Promise<void>;
/**
* Retrieve form data from persistent storage
*/
abstract getFormData(): Promise<TwoFactorFormData | null>;
/**
* Observable that emits the current form data
*/
abstract formData$(): Observable<TwoFactorFormData | null>;
/**
* Helper method to retrieve form data
* @returns A promise that resolves to the form data
*/
async getFormData(): Promise<TwoFactorFormData | null> {
return firstValueFrom(this.formData$());
}
/**
* Clear form data from persistent storage
*/

View File

@@ -1,10 +1,8 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component } from "@angular/core";
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { ActivatedRoute, convertToParamMap, Router } from "@angular/router";
import { ActivatedRoute, Router, convertToParamMap } from "@angular/router";
import { mock, MockProxy } from "jest-mock-extended";
import { BehaviorSubject } from "rxjs";
import { BehaviorSubject, of } from "rxjs";
// eslint-disable-next-line no-restricted-imports
import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
@@ -39,6 +37,7 @@ import { DialogService, ToastService } from "@bitwarden/components";
import { AnonLayoutWrapperDataService } from "../anon-layout/anon-layout-wrapper-data.service";
import { TwoFactorFormCacheService } from "./abstractions/two-factor-form-cache.service.abstraction";
import { TwoFactorAuthComponentService } from "./two-factor-auth-component.service";
import { TwoFactorAuthComponent } from "./two-factor-auth.component";
@@ -73,6 +72,7 @@ describe("TwoFactorAuthComponent", () => {
let anonLayoutWrapperDataService: MockProxy<AnonLayoutWrapperDataService>;
let mockEnvService: MockProxy<EnvironmentService>;
let mockLoginSuccessHandlerService: MockProxy<LoginSuccessHandlerService>;
let mockTwoFactorFormCacheService: MockProxy<TwoFactorFormCacheService>;
let mockUserDecryptionOpts: {
noMasterPassword: UserDecryptionOptions;
@@ -113,6 +113,10 @@ describe("TwoFactorAuthComponent", () => {
anonLayoutWrapperDataService = mock<AnonLayoutWrapperDataService>();
mockTwoFactorFormCacheService = mock<TwoFactorFormCacheService>();
mockTwoFactorFormCacheService.isEnabled$.mockReturnValue(of(false));
mockTwoFactorFormCacheService.formData$.mockReturnValue(of(null));
mockUserDecryptionOpts = {
noMasterPassword: new UserDecryptionOptions({
hasMasterPassword: false,
@@ -195,6 +199,7 @@ describe("TwoFactorAuthComponent", () => {
{ provide: EnvironmentService, useValue: mockEnvService },
{ provide: AnonLayoutWrapperDataService, useValue: anonLayoutWrapperDataService },
{ provide: LoginSuccessHandlerService, useValue: mockLoginSuccessHandlerService },
{ provide: TwoFactorFormCacheService, useValue: mockTwoFactorFormCacheService },
],
});