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