mirror of
https://github.com/bitwarden/browser
synced 2025-12-11 05:43:41 +00:00
567 lines
16 KiB
TypeScript
567 lines
16 KiB
TypeScript
import { mock } from "jest-mock-extended";
|
|
import { of } from "rxjs";
|
|
|
|
import { IntegrationId } from "@bitwarden/common/tools/integration";
|
|
import { UserId } from "@bitwarden/common/types/guid";
|
|
import {
|
|
GeneratorService,
|
|
DefaultPassphraseGenerationOptions,
|
|
DefaultPasswordGenerationOptions,
|
|
Policies,
|
|
PassphraseGenerationOptions,
|
|
PassphraseGeneratorPolicy,
|
|
PasswordGenerationOptions,
|
|
PasswordGeneratorPolicy,
|
|
policies,
|
|
} from "@bitwarden/generator-core";
|
|
import {
|
|
GeneratedCredential,
|
|
GeneratorHistoryService,
|
|
GeneratedPasswordHistory,
|
|
} from "@bitwarden/generator-history";
|
|
import {
|
|
GeneratorNavigationService,
|
|
DefaultGeneratorNavigation,
|
|
GeneratorNavigation,
|
|
GeneratorNavigationEvaluator,
|
|
GeneratorNavigationPolicy,
|
|
} from "@bitwarden/generator-navigation";
|
|
|
|
import { mockAccountServiceWith } from "../../../../../common/spec";
|
|
|
|
import { LegacyPasswordGenerationService } from "./legacy-password-generation.service";
|
|
import { PasswordGeneratorOptions } from "./password-generator-options";
|
|
|
|
const SomeUser = "some user" as UserId;
|
|
const PassphraseGeneratorOptionsEvaluator = policies.PassphraseGeneratorOptionsEvaluator;
|
|
const PasswordGeneratorOptionsEvaluator = policies.PasswordGeneratorOptionsEvaluator;
|
|
|
|
function createPassphraseGenerator(
|
|
options: PassphraseGenerationOptions = {},
|
|
policy: PassphraseGeneratorPolicy = Policies.Passphrase.disabledValue,
|
|
) {
|
|
let savedOptions = options;
|
|
const generator = mock<GeneratorService<PassphraseGenerationOptions, PassphraseGeneratorPolicy>>({
|
|
evaluator$(id: UserId) {
|
|
const evaluator = new PassphraseGeneratorOptionsEvaluator(policy);
|
|
return of(evaluator);
|
|
},
|
|
options$(id: UserId) {
|
|
return of(savedOptions);
|
|
},
|
|
defaults$(id: UserId) {
|
|
return of(DefaultPassphraseGenerationOptions);
|
|
},
|
|
saveOptions(userId, options) {
|
|
savedOptions = options;
|
|
return Promise.resolve();
|
|
},
|
|
});
|
|
|
|
return generator;
|
|
}
|
|
|
|
function createPasswordGenerator(
|
|
options: PasswordGenerationOptions = {},
|
|
policy: PasswordGeneratorPolicy = Policies.Password.disabledValue,
|
|
) {
|
|
let savedOptions = options;
|
|
const generator = mock<GeneratorService<PasswordGenerationOptions, PasswordGeneratorPolicy>>({
|
|
evaluator$(id: UserId) {
|
|
const evaluator = new PasswordGeneratorOptionsEvaluator(policy);
|
|
return of(evaluator);
|
|
},
|
|
options$(id: UserId) {
|
|
return of(savedOptions);
|
|
},
|
|
defaults$(id: UserId) {
|
|
return of(DefaultPasswordGenerationOptions);
|
|
},
|
|
saveOptions(userId, options) {
|
|
savedOptions = options;
|
|
return Promise.resolve();
|
|
},
|
|
});
|
|
|
|
return generator;
|
|
}
|
|
|
|
function createNavigationGenerator(
|
|
options: GeneratorNavigation = {},
|
|
policy: GeneratorNavigationPolicy = {},
|
|
) {
|
|
let savedOptions = options;
|
|
const generator = mock<GeneratorNavigationService>({
|
|
evaluator$(id: UserId) {
|
|
const evaluator = new GeneratorNavigationEvaluator(policy);
|
|
return of(evaluator);
|
|
},
|
|
options$(id: UserId) {
|
|
return of(savedOptions);
|
|
},
|
|
defaults$(id: UserId) {
|
|
return of(DefaultGeneratorNavigation);
|
|
},
|
|
saveOptions: jest.fn((userId, options) => {
|
|
savedOptions = options;
|
|
return Promise.resolve();
|
|
}),
|
|
});
|
|
|
|
return generator;
|
|
}
|
|
|
|
describe("LegacyPasswordGenerationService", () => {
|
|
// NOTE: in all tests, `null` constructor arguments are not used by the test.
|
|
// They're set to `null` to avoid setting up unnecessary mocks.
|
|
|
|
describe("generatePassword", () => {
|
|
it("invokes the inner password generator to generate passwords", async () => {
|
|
const innerPassword = createPasswordGenerator();
|
|
const generator = new LegacyPasswordGenerationService(null, null, innerPassword, null, null);
|
|
const options = { type: "password" } as PasswordGeneratorOptions;
|
|
|
|
await generator.generatePassword(options);
|
|
|
|
expect(innerPassword.generate).toHaveBeenCalledWith(options);
|
|
});
|
|
|
|
it("invokes the inner passphrase generator to generate passphrases", async () => {
|
|
const innerPassphrase = createPassphraseGenerator();
|
|
const generator = new LegacyPasswordGenerationService(
|
|
null,
|
|
null,
|
|
null,
|
|
innerPassphrase,
|
|
null,
|
|
);
|
|
const options = { type: "passphrase" } as PasswordGeneratorOptions;
|
|
|
|
await generator.generatePassword(options);
|
|
|
|
expect(innerPassphrase.generate).toHaveBeenCalledWith(options);
|
|
});
|
|
});
|
|
|
|
describe("generatePassphrase", () => {
|
|
it("invokes the inner passphrase generator", async () => {
|
|
const innerPassphrase = createPassphraseGenerator();
|
|
const generator = new LegacyPasswordGenerationService(
|
|
null,
|
|
null,
|
|
null,
|
|
innerPassphrase,
|
|
null,
|
|
);
|
|
const options = {} as PasswordGeneratorOptions;
|
|
|
|
await generator.generatePassphrase(options);
|
|
|
|
expect(innerPassphrase.generate).toHaveBeenCalledWith(options);
|
|
});
|
|
});
|
|
|
|
describe("getOptions", () => {
|
|
it("combines options from its inner services", async () => {
|
|
const innerPassword = createPasswordGenerator({
|
|
length: 29,
|
|
minLength: 20,
|
|
ambiguous: false,
|
|
uppercase: true,
|
|
minUppercase: 1,
|
|
lowercase: false,
|
|
minLowercase: 2,
|
|
number: true,
|
|
minNumber: 3,
|
|
special: false,
|
|
minSpecial: 0,
|
|
});
|
|
const innerPassphrase = createPassphraseGenerator({
|
|
numWords: 10,
|
|
wordSeparator: "-",
|
|
capitalize: true,
|
|
includeNumber: false,
|
|
});
|
|
const navigation = createNavigationGenerator({
|
|
type: "passphrase",
|
|
username: "word",
|
|
forwarder: "simplelogin" as IntegrationId,
|
|
});
|
|
const accountService = mockAccountServiceWith(SomeUser);
|
|
const generator = new LegacyPasswordGenerationService(
|
|
accountService,
|
|
navigation,
|
|
innerPassword,
|
|
innerPassphrase,
|
|
null,
|
|
);
|
|
|
|
const [result] = await generator.getOptions();
|
|
|
|
expect(result).toEqual({
|
|
type: "passphrase",
|
|
length: 29,
|
|
minLength: 5,
|
|
ambiguous: false,
|
|
uppercase: true,
|
|
minUppercase: 1,
|
|
lowercase: false,
|
|
minLowercase: 0,
|
|
number: true,
|
|
minNumber: 3,
|
|
special: false,
|
|
minSpecial: 0,
|
|
numWords: 10,
|
|
wordSeparator: "-",
|
|
capitalize: true,
|
|
includeNumber: false,
|
|
policyUpdated: true,
|
|
});
|
|
});
|
|
|
|
it("sets default options when an inner service lacks a value", async () => {
|
|
const innerPassword = createPasswordGenerator(null);
|
|
const innerPassphrase = createPassphraseGenerator(null);
|
|
const navigation = createNavigationGenerator(null);
|
|
const accountService = mockAccountServiceWith(SomeUser);
|
|
const generator = new LegacyPasswordGenerationService(
|
|
accountService,
|
|
navigation,
|
|
innerPassword,
|
|
innerPassphrase,
|
|
null,
|
|
);
|
|
|
|
const [result] = await generator.getOptions();
|
|
|
|
expect(result).toEqual({
|
|
type: DefaultGeneratorNavigation.type,
|
|
...DefaultPassphraseGenerationOptions,
|
|
...DefaultPasswordGenerationOptions,
|
|
minLowercase: 1,
|
|
minUppercase: 1,
|
|
policyUpdated: true,
|
|
});
|
|
});
|
|
|
|
it("combines policies from its inner services", async () => {
|
|
const innerPassword = createPasswordGenerator(
|
|
{},
|
|
{
|
|
minLength: 20,
|
|
numberCount: 10,
|
|
specialCount: 11,
|
|
useUppercase: true,
|
|
useLowercase: false,
|
|
useNumbers: true,
|
|
useSpecial: false,
|
|
},
|
|
);
|
|
const innerPassphrase = createPassphraseGenerator(
|
|
{},
|
|
{
|
|
minNumberWords: 5,
|
|
capitalize: true,
|
|
includeNumber: false,
|
|
},
|
|
);
|
|
const accountService = mockAccountServiceWith(SomeUser);
|
|
const navigation = createNavigationGenerator(
|
|
{},
|
|
{
|
|
overridePasswordType: "password",
|
|
},
|
|
);
|
|
const generator = new LegacyPasswordGenerationService(
|
|
accountService,
|
|
navigation,
|
|
innerPassword,
|
|
innerPassphrase,
|
|
null,
|
|
);
|
|
|
|
const [, policy] = await generator.getOptions();
|
|
|
|
expect(policy).toEqual({
|
|
overridePasswordType: "password",
|
|
minLength: 20,
|
|
numberCount: 10,
|
|
specialCount: 11,
|
|
useUppercase: true,
|
|
useLowercase: false,
|
|
useNumbers: true,
|
|
useSpecial: false,
|
|
minNumberWords: 5,
|
|
capitalize: true,
|
|
includeNumber: false,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("enforcePasswordGeneratorPoliciesOnOptions", () => {
|
|
it("returns its options parameter with password policy applied", async () => {
|
|
const innerPassword = createPasswordGenerator(
|
|
{},
|
|
{
|
|
minLength: 15,
|
|
numberCount: 5,
|
|
specialCount: 5,
|
|
useUppercase: true,
|
|
useLowercase: true,
|
|
useNumbers: true,
|
|
useSpecial: true,
|
|
},
|
|
);
|
|
const innerPassphrase = createPassphraseGenerator();
|
|
const accountService = mockAccountServiceWith(SomeUser);
|
|
const navigation = createNavigationGenerator();
|
|
const options = {
|
|
type: "password" as const,
|
|
};
|
|
const generator = new LegacyPasswordGenerationService(
|
|
accountService,
|
|
navigation,
|
|
innerPassword,
|
|
innerPassphrase,
|
|
null,
|
|
);
|
|
|
|
const [result] = await generator.enforcePasswordGeneratorPoliciesOnOptions(options);
|
|
|
|
expect(result).toBe(options);
|
|
expect(result).toMatchObject({
|
|
length: 15,
|
|
minLength: 15,
|
|
minLowercase: 1,
|
|
minNumber: 5,
|
|
minUppercase: 1,
|
|
minSpecial: 5,
|
|
uppercase: true,
|
|
lowercase: true,
|
|
number: true,
|
|
special: true,
|
|
});
|
|
});
|
|
|
|
it("returns its options parameter with passphrase policy applied", async () => {
|
|
const innerPassword = createPasswordGenerator();
|
|
const innerPassphrase = createPassphraseGenerator(
|
|
{},
|
|
{
|
|
minNumberWords: 5,
|
|
capitalize: true,
|
|
includeNumber: true,
|
|
},
|
|
);
|
|
const accountService = mockAccountServiceWith(SomeUser);
|
|
const navigation = createNavigationGenerator();
|
|
const options = {
|
|
type: "passphrase" as const,
|
|
};
|
|
const generator = new LegacyPasswordGenerationService(
|
|
accountService,
|
|
navigation,
|
|
innerPassword,
|
|
innerPassphrase,
|
|
null,
|
|
);
|
|
|
|
const [result] = await generator.enforcePasswordGeneratorPoliciesOnOptions(options);
|
|
|
|
expect(result).toBe(options);
|
|
expect(result).toMatchObject({
|
|
numWords: 5,
|
|
capitalize: true,
|
|
includeNumber: true,
|
|
});
|
|
});
|
|
|
|
it("returns the applied policy", async () => {
|
|
const innerPassword = createPasswordGenerator(
|
|
{},
|
|
{
|
|
minLength: 20,
|
|
numberCount: 10,
|
|
specialCount: 11,
|
|
useUppercase: true,
|
|
useLowercase: false,
|
|
useNumbers: true,
|
|
useSpecial: false,
|
|
},
|
|
);
|
|
const innerPassphrase = createPassphraseGenerator(
|
|
{},
|
|
{
|
|
minNumberWords: 5,
|
|
capitalize: true,
|
|
includeNumber: false,
|
|
},
|
|
);
|
|
const accountService = mockAccountServiceWith(SomeUser);
|
|
const navigation = createNavigationGenerator(
|
|
{},
|
|
{
|
|
overridePasswordType: "password",
|
|
},
|
|
);
|
|
const generator = new LegacyPasswordGenerationService(
|
|
accountService,
|
|
navigation,
|
|
innerPassword,
|
|
innerPassphrase,
|
|
null,
|
|
);
|
|
|
|
const [, policy] = await generator.enforcePasswordGeneratorPoliciesOnOptions({});
|
|
|
|
expect(policy).toEqual({
|
|
overridePasswordType: "password",
|
|
minLength: 20,
|
|
numberCount: 10,
|
|
specialCount: 11,
|
|
useUppercase: true,
|
|
useLowercase: false,
|
|
useNumbers: true,
|
|
useSpecial: false,
|
|
minNumberWords: 5,
|
|
capitalize: true,
|
|
includeNumber: false,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("saveOptions", () => {
|
|
it("loads saved password options", async () => {
|
|
const innerPassword = createPasswordGenerator();
|
|
const innerPassphrase = createPassphraseGenerator();
|
|
const navigation = createNavigationGenerator();
|
|
const accountService = mockAccountServiceWith(SomeUser);
|
|
const generator = new LegacyPasswordGenerationService(
|
|
accountService,
|
|
navigation,
|
|
innerPassword,
|
|
innerPassphrase,
|
|
null,
|
|
);
|
|
const options = {
|
|
type: "password" as const,
|
|
length: 29,
|
|
minLength: 5,
|
|
ambiguous: false,
|
|
uppercase: true,
|
|
minUppercase: 1,
|
|
lowercase: false,
|
|
minLowercase: 0,
|
|
number: true,
|
|
minNumber: 3,
|
|
special: false,
|
|
minSpecial: 0,
|
|
};
|
|
await generator.saveOptions(options);
|
|
|
|
const [result] = await generator.getOptions();
|
|
|
|
expect(result).toMatchObject(options);
|
|
});
|
|
|
|
it("loads saved passphrase options", async () => {
|
|
const innerPassword = createPasswordGenerator();
|
|
const innerPassphrase = createPassphraseGenerator();
|
|
const navigation = createNavigationGenerator();
|
|
const accountService = mockAccountServiceWith(SomeUser);
|
|
const generator = new LegacyPasswordGenerationService(
|
|
accountService,
|
|
navigation,
|
|
innerPassword,
|
|
innerPassphrase,
|
|
null,
|
|
);
|
|
const options = {
|
|
type: "passphrase" as const,
|
|
numWords: 10,
|
|
wordSeparator: "-",
|
|
capitalize: true,
|
|
includeNumber: false,
|
|
};
|
|
await generator.saveOptions(options);
|
|
|
|
const [result] = await generator.getOptions();
|
|
|
|
expect(result).toMatchObject(options);
|
|
});
|
|
|
|
it("preserves saved navigation options", async () => {
|
|
const innerPassword = createPasswordGenerator();
|
|
const innerPassphrase = createPassphraseGenerator();
|
|
const navigation = createNavigationGenerator({
|
|
type: "password",
|
|
username: "forwarded",
|
|
forwarder: "firefoxrelay" as IntegrationId,
|
|
});
|
|
const accountService = mockAccountServiceWith(SomeUser);
|
|
const generator = new LegacyPasswordGenerationService(
|
|
accountService,
|
|
navigation,
|
|
innerPassword,
|
|
innerPassphrase,
|
|
null,
|
|
);
|
|
const options = {
|
|
type: "passphrase" as const,
|
|
numWords: 10,
|
|
wordSeparator: "-",
|
|
capitalize: true,
|
|
includeNumber: false,
|
|
};
|
|
|
|
await generator.saveOptions(options);
|
|
|
|
expect(navigation.saveOptions).toHaveBeenCalledWith(SomeUser, {
|
|
type: "passphrase",
|
|
username: "forwarded",
|
|
forwarder: "firefoxrelay",
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("getHistory", () => {
|
|
it("gets the active user's history from the history service", async () => {
|
|
const history = mock<GeneratorHistoryService>();
|
|
history.credentials$.mockReturnValue(
|
|
of([new GeneratedCredential("foo", "password", new Date(100))]),
|
|
);
|
|
const accountService = mockAccountServiceWith(SomeUser);
|
|
const generator = new LegacyPasswordGenerationService(
|
|
accountService,
|
|
null,
|
|
null,
|
|
null,
|
|
history,
|
|
);
|
|
|
|
const result = await generator.getHistory();
|
|
|
|
expect(history.credentials$).toHaveBeenCalledWith(SomeUser);
|
|
expect(result).toEqual([new GeneratedPasswordHistory("foo", 100)]);
|
|
});
|
|
});
|
|
|
|
describe("addHistory", () => {
|
|
it("adds a history item as a password credential", async () => {
|
|
const history = mock<GeneratorHistoryService>();
|
|
const accountService = mockAccountServiceWith(SomeUser);
|
|
const generator = new LegacyPasswordGenerationService(
|
|
accountService,
|
|
null,
|
|
null,
|
|
null,
|
|
history,
|
|
);
|
|
|
|
await generator.addHistory("foo");
|
|
|
|
expect(history.track).toHaveBeenCalledWith(SomeUser, "foo", "password");
|
|
});
|
|
});
|
|
});
|