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

fix unit tests

This commit is contained in:
✨ Audrey ✨
2025-03-31 18:21:39 -04:00
parent c5e9340964
commit 93ff55c433
25 changed files with 556 additions and 135 deletions

View File

@@ -12,7 +12,7 @@ export function deepFreeze<T extends object>(value: T): Readonly<T> {
for (const key of keys) {
const own = value[key];
if ((own && typeof own === "object") || typeof own === "function") {
if (own && typeof own === "object") {
deepFreeze(own);
}
}

View File

@@ -45,6 +45,8 @@ import {
Algorithm,
AlgorithmMetadata,
Type,
GeneratorProfile,
Profile,
} from "@bitwarden/generator-core";
import { GeneratorHistoryService } from "@bitwarden/generator-history";
@@ -104,8 +106,12 @@ export class PasswordGeneratorComponent implements OnInit, OnChanges, OnDestroy
}
}
@Input()
profile: GeneratorProfile = Profile.account;
/** Removes bottom margin, passed to downstream components */
@Input({ transform: coerceBooleanProperty }) disableMargin = false;
@Input({ transform: coerceBooleanProperty })
disableMargin = false;
/** tracks the currently selected credential type */
protected credentialType$ = new BehaviorSubject<CredentialAlgorithm>(Algorithm.password);
@@ -125,7 +131,7 @@ export class PasswordGeneratorComponent implements OnInit, OnChanges, OnDestroy
*/
protected async generate(source: string) {
const algorithm = await firstValueFrom(this.algorithm$);
const request: GenerateRequest = { source, algorithm: algorithm.id };
const request: GenerateRequest = { source, algorithm: algorithm.id, profile: this.profile };
this.log.debug(request, "generation requested");
this.generate$.next(request);
@@ -176,10 +182,7 @@ export class PasswordGeneratorComponent implements OnInit, OnChanges, OnDestroy
// wire up the generator
this.generatorService
.generate$({
on$: this.generate$,
account$: this.account$,
})
.generate$({ on$: this.generate$, account$: this.account$ })
.pipe(
catchError((error: unknown, generator) => {
if (typeof error === "string") {

View File

@@ -2,6 +2,8 @@ import { mock } from "jest-mock-extended";
import { EFFLongWordList } from "@bitwarden/common/platform/misc/wordlist";
import { Algorithm, Type } from "../metadata";
import { Randomizer } from "./abstractions";
import { EmailRandomizer } from "./email-randomizer";
@@ -41,7 +43,8 @@ describe("EmailRandomizer", () => {
async (email) => {
const emailRandomizer = new EmailRandomizer(randomizer);
const result = await emailRandomizer.randomAsciiSubaddress(email);
// this tests what happens when the type system is subverted
const result = await emailRandomizer.randomAsciiSubaddress(email!);
expect(result).toEqual("");
},
@@ -100,7 +103,8 @@ describe("EmailRandomizer", () => {
it.each([[null], [undefined], [""]])("returns null if the domain is %p", async (domain) => {
const emailRandomizer = new EmailRandomizer(randomizer);
const result = await emailRandomizer.randomAsciiCatchall(domain);
// this tests what happens when the type system is subverted
const result = await emailRandomizer.randomAsciiCatchall(domain!);
expect(result).toBeNull();
});
@@ -150,7 +154,8 @@ describe("EmailRandomizer", () => {
it.each([[null], [undefined], [""]])("returns null if the domain is %p", async (domain) => {
const emailRandomizer = new EmailRandomizer(randomizer);
const result = await emailRandomizer.randomWordsCatchall(domain);
// this tests what happens when the type system is subverted
const result = await emailRandomizer.randomWordsCatchall(domain!);
expect(result).toBeNull();
});
@@ -214,32 +219,32 @@ describe("EmailRandomizer", () => {
const email = new EmailRandomizer(randomizer);
const result = await email.generate(
{},
{ algorithm: Algorithm.catchall },
{
catchallDomain: "example.com",
},
);
expect(result.category).toEqual("catchall");
expect(result.category).toEqual(Type.email);
});
it("processes subaddress generation options", async () => {
const email = new EmailRandomizer(randomizer);
const result = await email.generate(
{},
{ algorithm: Algorithm.plusAddress },
{
subaddressEmail: "foo@example.com",
},
);
expect(result.category).toEqual("subaddress");
expect(result.category).toEqual(Type.email);
});
it("throws when it cannot recognize the options type", async () => {
const email = new EmailRandomizer(randomizer);
const result = email.generate({}, {});
const result = email.generate({ algorithm: Algorithm.password }, {});
await expect(result).rejects.toBeInstanceOf(Error);
});

View File

@@ -3,6 +3,7 @@ import { mock } from "jest-mock-extended";
import { EFFLongWordList } from "@bitwarden/common/platform/misc/wordlist";
import { Randomizer } from "../abstractions";
import { Algorithm, Type } from "../metadata";
import { Ascii } from "./data";
import { PasswordRandomizer } from "./password-randomizer";
@@ -341,32 +342,32 @@ describe("PasswordRandomizer", () => {
const password = new PasswordRandomizer(randomizer);
const result = await password.generate(
{},
{ algorithm: Algorithm.password },
{
length: 10,
},
);
expect(result.category).toEqual("password");
expect(result.category).toEqual(Type.password);
});
it("processes passphrase generation options", async () => {
const password = new PasswordRandomizer(randomizer);
const result = await password.generate(
{},
{ algorithm: Algorithm.passphrase },
{
numWords: 10,
},
);
expect(result.category).toEqual("passphrase");
expect(result.category).toEqual(Type.password);
});
it("throws when it cannot recognize the options type", async () => {
const password = new PasswordRandomizer(randomizer);
const result = password.generate({}, {});
const result = password.generate({ algorithm: Algorithm.username }, {});
await expect(result).rejects.toBeInstanceOf(Error);
});

View File

@@ -2,6 +2,8 @@ import { mock } from "jest-mock-extended";
import { EFFLongWordList } from "@bitwarden/common/platform/misc/wordlist";
import { Algorithm, Type } from "../metadata";
import { Randomizer } from "./abstractions";
import { UsernameRandomizer } from "./username-randomizer";
@@ -108,19 +110,19 @@ describe("UsernameRandomizer", () => {
const username = new UsernameRandomizer(randomizer);
const result = await username.generate(
{},
{ algorithm: Algorithm.username },
{
wordIncludeNumber: true,
},
);
expect(result.category).toEqual("username");
expect(result.category).toEqual(Type.username);
});
it("throws when it cannot recognize the options type", async () => {
const username = new UsernameRandomizer(randomizer);
const result = username.generate({}, {});
const result = username.generate({ algorithm: Algorithm.passphrase }, {});
await expect(result).rejects.toBeInstanceOf(Error);
});

View File

@@ -13,6 +13,7 @@ export {
Type,
Profile,
GeneratorMetadata,
GeneratorProfile,
AlgorithmMetadata,
AlgorithmsByType,
} from "./metadata";

View File

@@ -2,7 +2,8 @@ import { mock } from "jest-mock-extended";
import { EmailRandomizer } from "../../engine";
import { CatchallConstraints } from "../../policies/catchall-constraints";
import { CatchallGenerationOptions, GeneratorDependencyProvider } from "../../types";
import { GeneratorDependencyProvider } from "../../providers";
import { CatchallGenerationOptions } from "../../types";
import { Profile } from "../data";
import { CoreProfileMetadata } from "../profile-metadata";
import { isCoreProfile } from "../util";

View File

@@ -2,7 +2,8 @@ import { mock } from "jest-mock-extended";
import { EmailRandomizer } from "../../engine";
import { SubaddressConstraints } from "../../policies/subaddress-constraints";
import { SubaddressGenerationOptions, GeneratorDependencyProvider } from "../../types";
import { GeneratorDependencyProvider } from "../../providers";
import { SubaddressGenerationOptions } from "../../types";
import { Profile } from "../data";
import { CoreProfileMetadata } from "../profile-metadata";
import { isCoreProfile } from "../util";

View File

@@ -35,5 +35,11 @@ export { toForwarderMetadata } from "./email/forwarder";
export { AlgorithmMetadata } from "./algorithm-metadata";
export { GeneratorMetadata } from "./generator-metadata";
export { ProfileContext, CoreProfileMetadata, ProfileMetadata } from "./profile-metadata";
export { GeneratorProfile, CredentialAlgorithm, PasswordAlgorithm, CredentialType } from "./type";
export {
GeneratorProfile,
CredentialAlgorithm,
PasswordAlgorithm,
CredentialType,
ForwarderExtensionId,
} from "./type";
export { isForwarderProfile, toVendorId, isForwarderExtensionId } from "./util";

View File

@@ -5,7 +5,8 @@ import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
import { PasswordRandomizer } from "../../engine";
import { PassphrasePolicyConstraints } from "../../policies";
import { PassphraseGenerationOptions, GeneratorDependencyProvider } from "../../types";
import { GeneratorDependencyProvider } from "../../providers";
import { PassphraseGenerationOptions } from "../../types";
import { Profile } from "../data";
import { CoreProfileMetadata } from "../profile-metadata";
import { isCoreProfile } from "../util";

View File

@@ -5,7 +5,8 @@ import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
import { PasswordRandomizer } from "../../engine";
import { DynamicPasswordPolicyConstraints } from "../../policies";
import { PasswordGenerationOptions, GeneratorDependencyProvider } from "../../types";
import { GeneratorDependencyProvider } from "../../providers";
import { PasswordGenerationOptions } from "../../types";
import { Profile } from "../data";
import { CoreProfileMetadata } from "../profile-metadata";
import { isCoreProfile } from "../util";

View File

@@ -3,7 +3,8 @@ import { mock } from "jest-mock-extended";
import { IdentityConstraint } from "@bitwarden/common/tools/state/identity-state-constraint";
import { UsernameRandomizer } from "../../engine";
import { EffUsernameGenerationOptions, GeneratorDependencyProvider } from "../../types";
import { GeneratorDependencyProvider } from "../../providers";
import { EffUsernameGenerationOptions } from "../../types";
import { Profile } from "../data";
import { CoreProfileMetadata } from "../profile-metadata";
import { isCoreProfile } from "../util";

View File

@@ -14,16 +14,16 @@ import { passphraseLeastPrivilege } from "./passphrase-least-privilege";
const Passphrase: PolicyConfiguration<PassphraseGeneratorPolicy, PassphraseGenerationOptions> =
deepFreeze({
type: PolicyType.PasswordGenerator,
disabledValue: Object.freeze({
disabledValue: {
minNumberWords: 0,
capitalize: false,
includeNumber: false,
}),
},
combine: passphraseLeastPrivilege,
createEvaluator: (policy) => new PassphraseGeneratorOptionsEvaluator(policy),
});
describe("Password generator options builder", () => {
describe("Passphrase generator options builder", () => {
describe("constructor()", () => {
it("should set the policy object to a copy of the input policy", () => {
const policy: any = Object.assign({}, Passphrase.disabledValue);

View File

@@ -24,34 +24,56 @@ describe("PREFERENCES", () => {
// this case tests what happens when the type system is bypassed
const result = PREFERENCES.deserializer(value!);
expect(result).toEqual(SomeCredentialPreferences);
expect(result).toMatchObject({
email: {
algorithm: AlgorithmsByType[Type.email][0],
},
password: {
algorithm: AlgorithmsByType[Type.password][0],
},
username: {
algorithm: AlgorithmsByType[Type.username][0],
},
});
});
it("fills missing password preferences", () => {
const input: any = { ...SomeCredentialPreferences };
const input: any = structuredClone(SomeCredentialPreferences);
delete input.password;
const result = PREFERENCES.deserializer(input);
expect(result).toEqual(SomeCredentialPreferences);
expect(result).toMatchObject({
password: {
algorithm: AlgorithmsByType[Type.password][0],
},
});
});
it("fills missing email preferences", () => {
const input: any = { ...SomeCredentialPreferences };
const input: any = structuredClone(SomeCredentialPreferences);
delete input.email;
const result = PREFERENCES.deserializer(input);
expect(result).toEqual(SomeCredentialPreferences);
expect(result).toMatchObject({
email: {
algorithm: AlgorithmsByType[Type.email][0],
},
});
});
it("fills missing username preferences", () => {
const input: any = { ...SomeCredentialPreferences };
const input: any = structuredClone(SomeCredentialPreferences);
delete input.username;
const result = PREFERENCES.deserializer(input);
expect(result).toEqual(SomeCredentialPreferences);
expect(result).toMatchObject({
username: {
algorithm: AlgorithmsByType[Type.username][0],
},
});
});
it("converts string fields to Dates", () => {

View File

@@ -5,7 +5,6 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
import { FakeAccountService, FakeStateProvider } from "@bitwarden/common/spec";
import { LegacyEncryptorProvider } from "@bitwarden/common/tools/cryptography/legacy-encryptor-provider";
import { UserEncryptor } from "@bitwarden/common/tools/cryptography/user-encryptor.abstraction";
import {
@@ -24,6 +23,7 @@ import { UserStateSubjectDependencyProvider } from "@bitwarden/common/tools/stat
import { deepFreeze } from "@bitwarden/common/tools/util";
import { UserId } from "@bitwarden/common/types/guid";
import { FakeAccountService, FakeStateProvider } from "../../../../../common/spec";
import { Algorithm, AlgorithmsByType, CredentialAlgorithm, Type, Types } from "../metadata";
import catchall from "../metadata/email/catchall";
import plusAddress from "../metadata/email/plus-address";

View File

@@ -6,7 +6,6 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
import { GENERATOR_DISK, UserKeyDefinition } from "@bitwarden/common/platform/state";
import { FakeStateProvider, FakeAccountService, awaitAsync } from "@bitwarden/common/spec";
import { LegacyEncryptorProvider } from "@bitwarden/common/tools/cryptography/legacy-encryptor-provider";
import { UserEncryptor } from "@bitwarden/common/tools/cryptography/user-encryptor.abstraction";
import { disabledSemanticLoggerProvider } from "@bitwarden/common/tools/log";
@@ -16,6 +15,7 @@ import { UserStateSubjectDependencyProvider } from "@bitwarden/common/tools/stat
import { StateConstraints } from "@bitwarden/common/tools/types";
import { OrganizationId, PolicyId, UserId } from "@bitwarden/common/types/guid";
import { FakeStateProvider, FakeAccountService, awaitAsync } from "../../../../../common/spec";
import { CoreProfileMetadata, ProfileContext } from "../metadata/profile-metadata";
import { GeneratorConstraints } from "../types";

View File

@@ -1,5 +1,355 @@
describe("CredentialGeneratorService", () => {
describe("settings", () => {});
import { BehaviorSubject, firstValueFrom, of } from "rxjs";
describe("policy$", () => {});
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
import { Site, VendorId } from "@bitwarden/common/tools/extension";
import { Bitwarden } from "@bitwarden/common/tools/extension/vendor/bitwarden";
import { Vendor } from "@bitwarden/common/tools/extension/vendor/data";
import { UserId } from "@bitwarden/common/types/guid";
import {
Algorithm,
CredentialAlgorithm,
CredentialType,
ForwarderExtensionId,
GeneratorMetadata,
Profile,
} from "../metadata";
import { CredentialGeneratorProviders } from "../providers";
import { DefaultCredentialGeneratorService } from "./credential-generator.service";
// Custom type for jest.fn() mocks to preserve their type
type JestMockFunction<T extends (...args: any) => any> = jest.Mock<ReturnType<T>, Parameters<T>>;
// two-level partial that preserves jest.fn() mock types
type MockTwoLevelPartial<T> = {
[K in keyof T]?: T[K] extends object
? {
[P in keyof T[K]]?: T[K][P] extends (...args: any) => any
? JestMockFunction<T[K][P]>
: T[K][P];
}
: T[K];
};
describe("CredentialGeneratorService", () => {
let service: DefaultCredentialGeneratorService;
let providers: MockTwoLevelPartial<CredentialGeneratorProviders>;
let system: any;
let mockLogger: any;
let mockExtension: { settings: jest.Mock };
let account: Account;
let createService: (overrides?: any) => DefaultCredentialGeneratorService;
beforeEach(() => {
mockLogger = {
info: jest.fn(),
panic: jest.fn().mockImplementationOnce((c, m) => {
throw new Error(m ?? c);
}),
};
mockExtension = { settings: jest.fn() };
// Use a hard-coded value for mockAccount
account = {
id: "test-account-id" as UserId,
emailVerified: true,
email: "test@example.com",
name: "Test User",
};
system = {
log: jest.fn().mockReturnValue(mockLogger),
extension: mockExtension,
};
providers = {
metadata: {
metadata: jest.fn(),
preference$: jest.fn(),
algorithms$: jest.fn(),
algorithms: jest.fn(),
preferences: jest.fn(),
},
profile: {
settings: jest.fn(),
constraints$: jest.fn(),
},
generator: {},
};
// Creating the service instance with a cast to the expected type
createService = (overrides = {}) => {
// Force cast the incomplete providers to the required type
// similar to how the overrides are applied
const providersCast = providers as unknown as CredentialGeneratorProviders;
const instance = new DefaultCredentialGeneratorService(providersCast, system);
Object.assign(instance, overrides);
return instance;
};
service = createService();
});
describe("generate$", () => {
it("should generate credentials when provided a specific algorithm", async () => {
const mockEngine = { generate: jest.fn().mockReturnValue(of("generatedPassword")) };
const mockMetadata = {
id: "testAlgorithm",
engine: { create: jest.fn().mockReturnValue(mockEngine) },
} as unknown as GeneratorMetadata<any>;
const mockSettings = new BehaviorSubject({ length: 12 });
providers.metadata!.metadata = jest.fn().mockReturnValue(mockMetadata);
service = createService({
settings: () => mockSettings as any,
});
const dependencies = {
on$: of({ algorithm: "testAlgorithm" as CredentialAlgorithm }),
account$: of(account),
};
const result = await firstValueFrom(service.generate$(dependencies));
expect(result).toBe("generatedPassword");
expect(providers.metadata!.metadata).toHaveBeenCalledWith("testAlgorithm");
expect(mockMetadata.engine.create).toHaveBeenCalled();
expect(mockEngine.generate).toHaveBeenCalled();
});
it("should determine preferred algorithm from credential type and generate credentials", async () => {
const mockEngine = { generate: jest.fn().mockReturnValue(of("generatedPassword")) };
const mockMetadata = {
id: "testAlgorithm",
engine: { create: jest.fn().mockReturnValue(mockEngine) },
} as unknown as GeneratorMetadata<any>;
const mockSettings = new BehaviorSubject({ length: 12 });
providers.metadata!.preference$ = jest
.fn()
.mockReturnValue(of("testAlgorithm" as CredentialAlgorithm));
providers.metadata!.metadata = jest.fn().mockReturnValue(mockMetadata);
service = createService({
settings: () => mockSettings as any,
});
const dependencies = {
on$: of({ type: "password" as CredentialType }),
account$: of(account),
};
const result = await firstValueFrom(service.generate$(dependencies));
expect(result).toBe("generatedPassword");
expect(providers.metadata!.metadata).toHaveBeenCalledWith("testAlgorithm");
});
});
describe("algorithms$", () => {
it("should retrieve and map available algorithms for a credential type", async () => {
const mockAlgorithms = [Algorithm.password, Algorithm.passphrase] as CredentialAlgorithm[];
const mockMetadata1 = { id: Algorithm.password } as GeneratorMetadata<any>;
const mockMetadata2 = { id: Algorithm.passphrase } as GeneratorMetadata<any>;
providers.metadata!.algorithms$ = jest.fn().mockReturnValue(of(mockAlgorithms));
providers.metadata!.metadata = jest
.fn()
.mockReturnValueOnce(mockMetadata1)
.mockReturnValueOnce(mockMetadata2);
const result = await firstValueFrom(
service.algorithms$("password" as CredentialType, { account$: of(account) }),
);
expect(result).toEqual([mockMetadata1, mockMetadata2]);
});
});
describe("algorithms", () => {
it("should list algorithm metadata for a single credential type", () => {
providers.metadata!.algorithms = jest
.fn()
.mockReturnValue([Algorithm.password, Algorithm.passphrase] as CredentialAlgorithm[]);
service = createService({
algorithm: (id: CredentialAlgorithm) => ({ id }) as GeneratorMetadata<any>,
});
const result = service.algorithms("password" as CredentialType);
expect(result).toEqual([{ id: Algorithm.password }, { id: Algorithm.passphrase }]);
expect(providers.metadata!.algorithms).toHaveBeenCalledWith({ type: "password" });
});
it("should list combined algorithm metadata for multiple credential types", () => {
providers.metadata!.algorithms = jest
.fn()
.mockReturnValueOnce([Algorithm.password] as CredentialAlgorithm[])
.mockReturnValueOnce([Algorithm.username] as CredentialAlgorithm[]);
service = createService({
algorithm: (id: CredentialAlgorithm) => ({ id }) as GeneratorMetadata<any>,
});
const result = service.algorithms(["password", "username"] as CredentialType[]);
expect(result).toEqual([{ id: Algorithm.password }, { id: Algorithm.username }]);
expect(providers.metadata!.algorithms).toHaveBeenCalledWith({ type: "password" });
expect(providers.metadata!.algorithms).toHaveBeenCalledWith({ type: "username" });
});
});
describe("algorithm", () => {
it("should retrieve metadata for a specific generator algorithm", () => {
const mockMetadata = { id: Algorithm.password } as GeneratorMetadata<any>;
providers.metadata!.metadata = jest.fn().mockReturnValue(mockMetadata);
const result = service.algorithm(Algorithm.password);
expect(result).toBe(mockMetadata);
expect(providers.metadata!.metadata).toHaveBeenCalledWith(Algorithm.password);
});
it("should log a panic when algorithm ID is invalid", () => {
providers.metadata!.metadata = jest.fn().mockReturnValue(null);
expect(() => service.algorithm("invalidAlgo" as CredentialAlgorithm)).toThrow(
"invalid credential algorithm",
);
expect(mockLogger.panic).toHaveBeenCalledWith(
{ algorithm: "invalidAlgo" },
"invalid credential algorithm",
);
});
});
describe("forwarder", () => {
it("should retrieve forwarder metadata for a specific vendor", () => {
const vendorId = Vendor.bitwarden;
const forwarderExtensionId: ForwarderExtensionId = { forwarder: vendorId };
const mockMetadata = {
id: forwarderExtensionId,
type: "email" as CredentialType,
} as GeneratorMetadata<any>;
providers.metadata!.metadata = jest.fn().mockReturnValue(mockMetadata);
const result = service.forwarder(vendorId);
expect(result).toBe(mockMetadata);
expect(providers.metadata!.metadata).toHaveBeenCalledWith(forwarderExtensionId);
});
it("should log a panic when vendor ID is invalid", () => {
const invalidVendorId = "invalid-vendor" as VendorId;
providers.metadata!.metadata = jest.fn().mockReturnValue(null);
expect(() => service.forwarder(invalidVendorId)).toThrow("invalid vendor");
expect(mockLogger.panic).toHaveBeenCalledWith(
{ algorithm: invalidVendorId },
"invalid vendor",
);
});
});
describe("preferences", () => {
it("should retrieve credential preferences bound to the user's account", () => {
const mockPreferences = { defaultType: "password" };
providers.metadata!.preferences = jest.fn().mockReturnValue(mockPreferences);
const result = service.preferences({ account$: of(account) });
expect(result).toBe(mockPreferences);
});
});
describe("settings", () => {
it("should load user settings for account-bound profiles", () => {
const mockSettings = { value: { length: 12 } };
const mockMetadata = {
id: "test",
profiles: {
[Profile.account]: { id: "accountProfile" },
},
} as unknown as GeneratorMetadata<any>;
providers.profile!.settings = jest.fn().mockReturnValue(mockSettings);
const result = service.settings(mockMetadata, { account$: of(account) });
expect(result).toBe(mockSettings);
});
it("should load user settings for extension-bound profiles", () => {
const mockSettings = new BehaviorSubject({ value: { length: 12 } });
const vendorId = Vendor.bitwarden;
const forwarderProfile = {
id: { forwarder: Bitwarden.id },
site: Site.forwarder,
type: "extension",
};
const mockMetadata = {
id: { forwarder: vendorId } as ForwarderExtensionId,
profiles: {
[Profile.account]: forwarderProfile,
},
} as unknown as GeneratorMetadata<any>;
mockExtension.settings.mockReturnValue(mockSettings);
const result = service.settings(mockMetadata, { account$: of(account) });
expect(result).toBe(mockSettings);
});
it("should log a panic when profile metadata is not found", () => {
const mockMetadata = {
id: "test",
profiles: {},
} as unknown as GeneratorMetadata<any>;
expect(() => service.settings(mockMetadata, { account$: of(account) })).toThrow(
"failed to load settings; profile metadata not found",
);
expect(mockLogger.panic).toHaveBeenCalledWith(
{ algorithm: "test", profile: "account" },
"failed to load settings; profile metadata not found",
);
});
});
describe("policy$", () => {
it("should retrieve policy constraints for a specific profile", async () => {
const mockConstraints = { minLength: 8 };
const mockMetadata = {
id: "test",
profiles: {
[Profile.account]: { id: "accountProfile" },
},
} as unknown as GeneratorMetadata<any>;
providers.profile!.constraints$ = jest.fn().mockReturnValue(of(mockConstraints));
const result = await firstValueFrom(service.policy$(mockMetadata, { account$: of(account) }));
expect(result).toEqual(mockConstraints);
});
it("should log a panic when profile metadata is not found for policy retrieval", () => {
const mockMetadata = {
id: "test",
profiles: {},
} as unknown as GeneratorMetadata<any>;
expect(() => service.policy$(mockMetadata, { account$: of(account) })).toThrow(
"failed to load policy; profile metadata not found",
);
expect(mockLogger.panic).toHaveBeenCalledWith(
{ algorithm: "test", profile: "account" },
"failed to load policy; profile metadata not found",
);
});
});
});

View File

@@ -18,7 +18,7 @@ import { DefaultGeneratorService } from "./default-generator.service";
function mockPolicyService(config?: { state?: BehaviorSubject<Policy[]> }) {
const service = mock<PolicyService>();
const stateValue = config?.state ?? new BehaviorSubject<Policy[]>([null]);
const stateValue = config?.state ?? new BehaviorSubject<Policy[]>([]);
service.getAll$.mockReturnValue(stateValue);
return service;
@@ -119,22 +119,22 @@ describe("Password generator service", () => {
it("should update the evaluator when the password generator policy changes", async () => {
// set up dependencies
const state = new BehaviorSubject<Policy[]>([null]);
const state = new BehaviorSubject<Policy[]>([]);
const policy = mockPolicyService({ state });
const strategy = mockGeneratorStrategy();
const service = new DefaultGeneratorService(strategy, policy);
// model responses for the observable update. The map is called multiple times,
// and the array shift ensures reference equality is maintained.
const firstEvaluator = mock<PolicyEvaluator<any, any>>();
const secondEvaluator = mock<PolicyEvaluator<any, any>>();
const firstEvaluator: PolicyEvaluator<any, any> = mock<PolicyEvaluator<any, any>>();
const secondEvaluator: PolicyEvaluator<any, any> = mock<PolicyEvaluator<any, any>>();
const evaluators = [firstEvaluator, secondEvaluator];
strategy.toEvaluator.mockReturnValueOnce(pipe(map(() => evaluators.shift())));
strategy.toEvaluator.mockReturnValueOnce(pipe(map(() => evaluators.shift()!)));
// act
const evaluator$ = service.evaluator$(SomeUser);
const firstResult = await firstValueFrom(evaluator$);
state.next([null]);
state.next([]);
const secondResult = await firstValueFrom(evaluator$);
// assert

View File

@@ -13,7 +13,7 @@ describe("GeneratedCredential", () => {
it("assigns category", () => {
const result = new GeneratedCredential("example", Type.password, new Date(100));
expect(result.category).toEqual("passphrase");
expect(result.category).toEqual(Type.password);
});
it("passes through date parameters", () => {

View File

@@ -1,40 +1,42 @@
import { GeneratorCategory, GeneratedCredential } from ".";
import { Type } from "@bitwarden/generator-core";
import { GeneratedCredential } from ".";
describe("GeneratedCredential", () => {
describe("constructor", () => {
it("assigns credential", () => {
const result = new GeneratedCredential("example", "passphrase", new Date(100));
const result = new GeneratedCredential("example", Type.password, new Date(100));
expect(result.credential).toEqual("example");
});
it("assigns category", () => {
const result = new GeneratedCredential("example", "passphrase", new Date(100));
const result = new GeneratedCredential("example", Type.password, new Date(100));
expect(result.category).toEqual("passphrase");
expect(result.category).toEqual(Type.password);
});
it("passes through date parameters", () => {
const result = new GeneratedCredential("example", "password", new Date(100));
const result = new GeneratedCredential("example", Type.password, new Date(100));
expect(result.generationDate).toEqual(new Date(100));
});
it("converts numeric dates to Dates", () => {
const result = new GeneratedCredential("example", "password", 100);
const result = new GeneratedCredential("example", Type.password, 100);
expect(result.generationDate).toEqual(new Date(100));
});
});
it("toJSON converts from a credential into a JSON object", () => {
const credential = new GeneratedCredential("example", "password", new Date(100));
const credential = new GeneratedCredential("example", Type.password, new Date(100));
const result = credential.toJSON();
expect(result).toEqual({
credential: "example",
category: "password" as GeneratorCategory,
category: Type.password,
generationDate: 100,
});
});
@@ -42,7 +44,7 @@ describe("GeneratedCredential", () => {
it("fromJSON converts Json objects into credentials", () => {
const jsonValue = {
credential: "example",
category: "password" as GeneratorCategory,
category: Type.password,
generationDate: 100,
};
@@ -51,7 +53,7 @@ describe("GeneratedCredential", () => {
expect(result).toBeInstanceOf(GeneratedCredential);
expect(result).toEqual({
credential: "example",
category: "password",
category: Type.password,
generationDate: new Date(100),
});
});

View File

@@ -7,6 +7,7 @@ import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/sym
import { CsprngArray } from "@bitwarden/common/types/csprng";
import { UserId } from "@bitwarden/common/types/guid";
import { UserKey } from "@bitwarden/common/types/key";
import { Type } from "@bitwarden/generator-core";
import { KeyService } from "@bitwarden/key-management";
import { FakeStateProvider, awaitAsync, mockAccountServiceWith } from "../../../../../common/spec";
@@ -23,7 +24,8 @@ describe("LocalGeneratorHistoryService", () => {
beforeEach(() => {
encryptService.encrypt.mockImplementation((p) => Promise.resolve(p as unknown as EncString));
encryptService.decryptToUtf8.mockImplementation((c) => Promise.resolve(c.encryptedString));
// tests always provide a value for c.encryptedString
encryptService.decryptToUtf8.mockImplementation((c) => Promise.resolve(c.encryptedString!));
keyService.getUserKey.mockImplementation(() => Promise.resolve(userKey));
keyService.userKey$.mockImplementation(() => of(true as unknown as UserKey));
});
@@ -48,35 +50,35 @@ describe("LocalGeneratorHistoryService", () => {
const stateProvider = new FakeStateProvider(mockAccountServiceWith(SomeUser));
const history = new LocalGeneratorHistoryService(encryptService, keyService, stateProvider);
await history.track(SomeUser, "example", "password");
await history.track(SomeUser, "example", Type.password);
await awaitAsync();
const [result] = await firstValueFrom(history.credentials$(SomeUser));
expect(result).toMatchObject({ credential: "example", category: "password" });
expect(result).toMatchObject({ credential: "example", category: Type.password });
});
it("stores a passphrase", async () => {
const stateProvider = new FakeStateProvider(mockAccountServiceWith(SomeUser));
const history = new LocalGeneratorHistoryService(encryptService, keyService, stateProvider);
await history.track(SomeUser, "example", "passphrase");
await history.track(SomeUser, "example", Type.password);
await awaitAsync();
const [result] = await firstValueFrom(history.credentials$(SomeUser));
expect(result).toMatchObject({ credential: "example", category: "passphrase" });
expect(result).toMatchObject({ credential: "example", category: Type.password });
});
it("stores a specific date when one is provided", async () => {
const stateProvider = new FakeStateProvider(mockAccountServiceWith(SomeUser));
const history = new LocalGeneratorHistoryService(encryptService, keyService, stateProvider);
await history.track(SomeUser, "example", "password", new Date(100));
await history.track(SomeUser, "example", Type.password, new Date(100));
await awaitAsync();
const [result] = await firstValueFrom(history.credentials$(SomeUser));
expect(result).toEqual({
credential: "example",
category: "password",
category: Type.password,
generationDate: new Date(100),
});
});
@@ -85,13 +87,13 @@ describe("LocalGeneratorHistoryService", () => {
const stateProvider = new FakeStateProvider(mockAccountServiceWith(SomeUser));
const history = new LocalGeneratorHistoryService(encryptService, keyService, stateProvider);
await history.track(SomeUser, "example", "password");
await history.track(SomeUser, "example", "password");
await history.track(SomeUser, "example", "passphrase");
await history.track(SomeUser, "example", Type.password);
await history.track(SomeUser, "example", Type.password);
await history.track(SomeUser, "example", Type.password);
await awaitAsync();
const [firstResult, secondResult] = await firstValueFrom(history.credentials$(SomeUser));
expect(firstResult).toMatchObject({ credential: "example", category: "password" });
expect(firstResult).toMatchObject({ credential: "example", category: Type.password });
expect(secondResult).toBeUndefined();
});
@@ -99,13 +101,13 @@ describe("LocalGeneratorHistoryService", () => {
const stateProvider = new FakeStateProvider(mockAccountServiceWith(SomeUser));
const history = new LocalGeneratorHistoryService(encryptService, keyService, stateProvider);
await history.track(SomeUser, "secondResult", "password");
await history.track(SomeUser, "firstResult", "password");
await history.track(SomeUser, "secondResult", Type.password);
await history.track(SomeUser, "firstResult", Type.password);
await awaitAsync();
const [firstResult, secondResult] = await firstValueFrom(history.credentials$(SomeUser));
expect(firstResult).toMatchObject({ credential: "firstResult", category: "password" });
expect(secondResult).toMatchObject({ credential: "secondResult", category: "password" });
expect(firstResult).toMatchObject({ credential: "firstResult", category: Type.password });
expect(secondResult).toMatchObject({ credential: "secondResult", category: Type.password });
});
it("removes history items exceeding maxTotal configuration", async () => {
@@ -114,12 +116,12 @@ describe("LocalGeneratorHistoryService", () => {
maxTotal: 1,
});
await history.track(SomeUser, "removed result", "password");
await history.track(SomeUser, "example", "password");
await history.track(SomeUser, "removed result", Type.password);
await history.track(SomeUser, "example", Type.password);
await awaitAsync();
const [firstResult, secondResult] = await firstValueFrom(history.credentials$(SomeUser));
expect(firstResult).toMatchObject({ credential: "example", category: "password" });
expect(firstResult).toMatchObject({ credential: "example", category: Type.password });
expect(secondResult).toBeUndefined();
});
@@ -129,8 +131,8 @@ describe("LocalGeneratorHistoryService", () => {
maxTotal: 1,
});
await history.track(SomeUser, "some user example", "password");
await history.track(AnotherUser, "another user example", "password");
await history.track(SomeUser, "some user example", Type.password);
await history.track(AnotherUser, "another user example", Type.password);
await awaitAsync();
const [someFirstResult, someSecondResult] = await firstValueFrom(
history.credentials$(SomeUser),
@@ -141,12 +143,12 @@ describe("LocalGeneratorHistoryService", () => {
expect(someFirstResult).toMatchObject({
credential: "some user example",
category: "password",
category: Type.password,
});
expect(someSecondResult).toBeUndefined();
expect(anotherFirstResult).toMatchObject({
credential: "another user example",
category: "password",
category: Type.password,
});
expect(anotherSecondResult).toBeUndefined();
});
@@ -165,7 +167,7 @@ describe("LocalGeneratorHistoryService", () => {
it("returns null when the credential wasn't found", async () => {
const stateProvider = new FakeStateProvider(mockAccountServiceWith(SomeUser));
const history = new LocalGeneratorHistoryService(encryptService, keyService, stateProvider);
await history.track(SomeUser, "example", "password");
await history.track(SomeUser, "example", Type.password);
const result = await history.take(SomeUser, "not found");
@@ -175,20 +177,20 @@ describe("LocalGeneratorHistoryService", () => {
it("returns a matching credential", async () => {
const stateProvider = new FakeStateProvider(mockAccountServiceWith(SomeUser));
const history = new LocalGeneratorHistoryService(encryptService, keyService, stateProvider);
await history.track(SomeUser, "example", "password");
await history.track(SomeUser, "example", Type.password);
const result = await history.take(SomeUser, "example");
expect(result).toMatchObject({
credential: "example",
category: "password",
category: Type.password,
});
});
it("removes a matching credential", async () => {
const stateProvider = new FakeStateProvider(mockAccountServiceWith(SomeUser));
const history = new LocalGeneratorHistoryService(encryptService, keyService, stateProvider);
await history.track(SomeUser, "example", "password");
await history.track(SomeUser, "example", Type.password);
await history.take(SomeUser, "example");
await awaitAsync();

View File

@@ -1,7 +1,7 @@
import { mock } from "jest-mock-extended";
import { of } from "rxjs";
import { IntegrationId } from "@bitwarden/common/tools/integration";
import { VendorId } from "@bitwarden/common/tools/extension";
import { UserId } from "@bitwarden/common/types/guid";
import {
GeneratorService,
@@ -204,7 +204,7 @@ describe("LegacyPasswordGenerationService", () => {
const navigation = createNavigationGenerator({
type: "passphrase",
username: "word",
forwarder: "simplelogin" as IntegrationId,
forwarder: "simplelogin" as VendorId,
});
const accountService = mockAccountServiceWith(SomeUser);
const generator = new LegacyPasswordGenerationService(
@@ -515,7 +515,7 @@ describe("LegacyPasswordGenerationService", () => {
const navigation = createNavigationGenerator({
type: "password",
username: "forwarded",
forwarder: "firefoxrelay" as IntegrationId,
forwarder: "firefoxrelay" as VendorId,
});
const accountService = mockAccountServiceWith(SomeUser);
const generator = new LegacyPasswordGenerationService(

View File

@@ -1,6 +1,15 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { mock } from "jest-mock-extended";
import { of } from "rxjs";
import { AddyIo } from "@bitwarden/common/tools/extension/vendor/addyio";
import { DuckDuckGo } from "@bitwarden/common/tools/extension/vendor/duckduckgo";
import { Fastmail } from "@bitwarden/common/tools/extension/vendor/fastmail";
import { ForwardEmail } from "@bitwarden/common/tools/extension/vendor/forwardemail";
import { Mozilla } from "@bitwarden/common/tools/extension/vendor/mozilla";
import { SimpleLogin } from "@bitwarden/common/tools/extension/vendor/simplelogin";
import { IntegrationId } from "@bitwarden/common/tools/integration";
import { UserId } from "@bitwarden/common/types/guid";
import {
ApiOptions,
@@ -13,13 +22,6 @@ import {
DefaultCatchallOptions,
DefaultEffUsernameOptions,
EffUsernameGenerationOptions,
DefaultAddyIoOptions,
DefaultDuckDuckGoOptions,
DefaultFastmailOptions,
DefaultFirefoxRelayOptions,
DefaultForwardEmailOptions,
DefaultSimpleLoginOptions,
Forwarders,
DefaultSubaddressOptions,
SubaddressGenerationOptions,
policies,
@@ -169,7 +171,7 @@ describe("LegacyUsernameGenerationService", () => {
// set up an arbitrary forwarder for the username test; all forwarders tested in their own tests
const options = {
type: "forwarded",
forwardedService: Forwarders.AddyIo.id,
forwardedService: AddyIo.id,
} as UsernameGeneratorOptions;
const addyIo = createGenerator<SelfHostedApiOptions & EmailDomainOptions>(null, null);
addyIo.generate.mockResolvedValue("addyio@example.com");
@@ -249,7 +251,7 @@ describe("LegacyUsernameGenerationService", () => {
describe("generateForwarded", () => {
it("should generate a AddyIo username", async () => {
const options = {
forwardedService: Forwarders.AddyIo.id,
forwardedService: AddyIo.id,
forwardedAnonAddyApiToken: "token",
forwardedAnonAddyBaseUrl: "https://example.com",
forwardedAnonAddyDomain: "example.com",
@@ -284,7 +286,7 @@ describe("LegacyUsernameGenerationService", () => {
it("should generate a DuckDuckGo username", async () => {
const options = {
forwardedService: Forwarders.DuckDuckGo.id,
forwardedService: DuckDuckGo.id,
forwardedDuckDuckGoToken: "token",
website: "example.com",
} as UsernameGeneratorOptions;
@@ -315,7 +317,7 @@ describe("LegacyUsernameGenerationService", () => {
it("should generate a Fastmail username", async () => {
const options = {
forwardedService: Forwarders.Fastmail.id,
forwardedService: Fastmail.id,
forwardedFastmailApiToken: "token",
website: "example.com",
} as UsernameGeneratorOptions;
@@ -346,7 +348,7 @@ describe("LegacyUsernameGenerationService", () => {
it("should generate a FirefoxRelay username", async () => {
const options = {
forwardedService: Forwarders.FirefoxRelay.id,
forwardedService: Mozilla.id,
forwardedFirefoxApiToken: "token",
website: "example.com",
} as UsernameGeneratorOptions;
@@ -377,7 +379,7 @@ describe("LegacyUsernameGenerationService", () => {
it("should generate a ForwardEmail username", async () => {
const options = {
forwardedService: Forwarders.ForwardEmail.id,
forwardedService: ForwardEmail.id,
forwardedForwardEmailApiToken: "token",
forwardedForwardEmailDomain: "example.com",
website: "example.com",
@@ -410,7 +412,7 @@ describe("LegacyUsernameGenerationService", () => {
it("should generate a SimpleLogin username", async () => {
const options = {
forwardedService: Forwarders.SimpleLogin.id,
forwardedService: SimpleLogin.id,
forwardedSimpleLoginApiKey: "token",
forwardedSimpleLoginBaseUrl: "https://example.com",
website: "example.com",
@@ -449,7 +451,7 @@ describe("LegacyUsernameGenerationService", () => {
const navigation = createNavigationGenerator({
type: "username",
username: "catchall",
forwarder: Forwarders.AddyIo.id,
forwarder: AddyIo.id,
});
const catchall = createGenerator<CatchallGenerationOptions>(
@@ -557,7 +559,7 @@ describe("LegacyUsernameGenerationService", () => {
subaddressEmail: "foo@example.com",
catchallType: "random",
catchallDomain: "example.com",
forwardedService: Forwarders.AddyIo.id,
forwardedService: AddyIo.id,
forwardedAnonAddyApiToken: "addyIoToken",
forwardedAnonAddyDomain: "addyio.example.com",
forwardedAnonAddyBaseUrl: "https://addyio.api.example.com",
@@ -583,21 +585,36 @@ describe("LegacyUsernameGenerationService", () => {
null,
DefaultSubaddressOptions,
);
const addyIo = createGenerator<SelfHostedApiOptions & EmailDomainOptions>(
null,
DefaultAddyIoOptions,
);
const duckDuckGo = createGenerator<ApiOptions>(null, DefaultDuckDuckGoOptions);
const fastmail = createGenerator<ApiOptions & EmailPrefixOptions>(
null,
DefaultFastmailOptions,
);
const firefoxRelay = createGenerator<ApiOptions>(null, DefaultFirefoxRelayOptions);
const forwardEmail = createGenerator<ApiOptions & EmailDomainOptions>(
null,
DefaultForwardEmailOptions,
);
const simpleLogin = createGenerator<SelfHostedApiOptions>(null, DefaultSimpleLoginOptions);
const addyIo = createGenerator<SelfHostedApiOptions & EmailDomainOptions>(null, {
website: null,
baseUrl: "https://app.addy.io",
token: "",
domain: "",
});
const duckDuckGo = createGenerator<ApiOptions>(null, {
website: null,
token: "",
});
const fastmail = createGenerator<ApiOptions & EmailPrefixOptions>(null, {
website: "",
domain: "",
prefix: "",
token: "",
});
const firefoxRelay = createGenerator<ApiOptions>(null, {
website: null,
token: "",
});
const forwardEmail = createGenerator<ApiOptions & EmailDomainOptions>(null, {
website: null,
token: "",
domain: "",
});
const simpleLogin = createGenerator<SelfHostedApiOptions>(null, {
website: null,
baseUrl: "https://app.simplelogin.io",
token: "",
});
const generator = new LegacyUsernameGenerationService(
account,
@@ -624,16 +641,16 @@ describe("LegacyUsernameGenerationService", () => {
subaddressType: DefaultSubaddressOptions.subaddressType,
subaddressEmail: DefaultSubaddressOptions.subaddressEmail,
forwardedService: DefaultGeneratorNavigation.forwarder,
forwardedAnonAddyApiToken: DefaultAddyIoOptions.token,
forwardedAnonAddyDomain: DefaultAddyIoOptions.domain,
forwardedAnonAddyBaseUrl: DefaultAddyIoOptions.baseUrl,
forwardedDuckDuckGoToken: DefaultDuckDuckGoOptions.token,
forwardedFastmailApiToken: DefaultFastmailOptions.token,
forwardedFirefoxApiToken: DefaultFirefoxRelayOptions.token,
forwardedForwardEmailApiToken: DefaultForwardEmailOptions.token,
forwardedForwardEmailDomain: DefaultForwardEmailOptions.domain,
forwardedSimpleLoginApiKey: DefaultSimpleLoginOptions.token,
forwardedSimpleLoginBaseUrl: DefaultSimpleLoginOptions.baseUrl,
forwardedAnonAddyApiToken: "",
forwardedAnonAddyDomain: "",
forwardedAnonAddyBaseUrl: "https://app.addy.io",
forwardedDuckDuckGoToken: "",
forwardedFastmailApiToken: "",
forwardedFirefoxApiToken: "",
forwardedForwardEmailApiToken: "",
forwardedForwardEmailDomain: "",
forwardedSimpleLoginApiKey: "",
forwardedSimpleLoginBaseUrl: "https://app.simplelogin.io",
});
});
});
@@ -678,7 +695,7 @@ describe("LegacyUsernameGenerationService", () => {
subaddressEmail: "foo@example.com",
catchallType: "random",
catchallDomain: "example.com",
forwardedService: Forwarders.AddyIo.id,
forwardedService: AddyIo.id as IntegrationId,
forwardedAnonAddyApiToken: "addyIoToken",
forwardedAnonAddyDomain: "addyio.example.com",
forwardedAnonAddyBaseUrl: "https://addyio.api.example.com",
@@ -697,7 +714,7 @@ describe("LegacyUsernameGenerationService", () => {
expect(navigation.saveOptions).toHaveBeenCalledWith(SomeUser, {
type: "password",
username: "catchall",
forwarder: Forwarders.AddyIo.id,
forwarder: AddyIo.id,
});
expect(catchall.saveOptions).toHaveBeenCalledWith(SomeUser, {

View File

@@ -3,6 +3,7 @@
import { zip, firstValueFrom, map, concatMap, combineLatest } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { Vendor } from "@bitwarden/common/tools/extension/vendor/data";
import { IntegrationRequest } from "@bitwarden/common/tools/integration/rpc";
import { UserId } from "@bitwarden/common/types/guid";
import {
@@ -89,12 +90,14 @@ export class LegacyUsernameGenerationService implements UsernameGenerationServic
const stored = this.toStoredOptions(options);
switch (options.forwardedService) {
case Forwarders.AddyIo.id:
case Vendor.addyio:
return this.addyIo.generate(stored.forwarders.addyIo);
case Forwarders.DuckDuckGo.id:
return this.duckDuckGo.generate(stored.forwarders.duckDuckGo);
case Forwarders.Fastmail.id:
return this.fastmail.generate(stored.forwarders.fastmail);
case Forwarders.FirefoxRelay.id:
case Vendor.mozilla:
return this.firefoxRelay.generate(stored.forwarders.firefoxRelay);
case Forwarders.ForwardEmail.id:
return this.forwardEmail.generate(stored.forwarders.forwardEmail);
@@ -233,6 +236,7 @@ export class LegacyUsernameGenerationService implements UsernameGenerationServic
) {
switch (forwarder) {
case "anonaddy":
case "addyio":
await this.addyIo.saveOptions(account, options.forwarders.addyIo);
return true;
case "duckduckgo":

View File

@@ -1,4 +1,5 @@
import { ForwarderId, UsernameGeneratorType, CredentialAlgorithm } from "@bitwarden/generator-core";
import { VendorId } from "@bitwarden/common/tools/extension";
import { UsernameGeneratorType, CredentialAlgorithm } from "@bitwarden/generator-core";
/** Stores credential generator UI state. */
export type GeneratorNavigation = {
@@ -12,5 +13,5 @@ export type GeneratorNavigation = {
username?: UsernameGeneratorType;
/** When `username === "forwarded"`, this stores the forwarder implementation. */
forwarder?: ForwarderId | "";
forwarder?: VendorId | "";
};