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

unit tests

This commit is contained in:
✨ Audrey ✨
2025-02-28 11:31:01 -05:00
parent 86324e5744
commit 29459d967f
8 changed files with 206 additions and 18 deletions

View File

@@ -0,0 +1,136 @@
import { mock } from "jest-mock-extended";
import { BehaviorSubject, firstValueFrom } from "rxjs";
import { FakeAccountService, FakeStateProvider, awaitAsync } from "../../../spec";
import { Account } from "../../auth/abstractions/account.service";
import { EXTENSION_DISK, UserKeyDefinition } from "../../platform/state";
import { UserId } from "../../types/guid";
import { LegacyEncryptorProvider } from "../cryptography/legacy-encryptor-provider";
import { UserEncryptor } from "../cryptography/user-encryptor.abstraction";
import { disabledSemanticLoggerProvider } from "../log";
import { UserStateSubjectDependencyProvider } from "../state/user-state-subject-dependency-provider";
import { Site } from "./data";
import { ExtensionRegistry } from "./extension-registry.abstraction";
import { ExtensionSite } from "./extension-site";
import { ExtensionService } from "./extension.service";
import { ExtensionMetadata, ExtensionProfileMetadata } from "./type";
import { Vendor } from "./vendor/data";
import { SimpleLogin } from "./vendor/simplelogin";
const SomeUser = "some user" as UserId;
const SomeAccount = {
id: SomeUser,
email: "someone@example.com",
emailVerified: true,
name: "Someone",
};
const SomeAccount$ = new BehaviorSubject<Account>(SomeAccount);
type TestType = { foo: string };
const SomeEncryptor: UserEncryptor = {
userId: SomeUser,
encrypt(secret) {
const tmp: any = secret;
return Promise.resolve({ foo: `encrypt(${tmp.foo})` } as any);
},
decrypt(secret) {
const tmp: any = JSON.parse(secret.encryptedString!);
return Promise.resolve({ foo: `decrypt(${tmp.foo})` } as any);
},
};
const SomeAccountService = new FakeAccountService({
[SomeUser]: SomeAccount,
});
const SomeStateProvider = new FakeStateProvider(SomeAccountService);
const SomeProvider = {
encryptor: {
userEncryptor$: () => {
return new BehaviorSubject({ encryptor: SomeEncryptor, userId: SomeUser }).asObservable();
},
organizationEncryptor$() {
throw new Error("`organizationEncryptor$` should never be invoked.");
},
} as LegacyEncryptorProvider,
state: SomeStateProvider,
log: disabledSemanticLoggerProvider,
} as UserStateSubjectDependencyProvider;
const SomeExtension: ExtensionMetadata = {
site: { id: "forwarder", availableFields: [] },
product: { vendor: SimpleLogin },
host: {
selfHost: "maybe",
baseUrl: "https://www.example.com/",
authentication: true,
},
requestedFields: [],
};
const SomeRegistry = mock<ExtensionRegistry>();
const SomeProfileMetadata = {
type: "extension",
site: Site.forwarder,
storage: {
key: "someProfile",
options: {
deserializer: (value) => value as TestType,
clearOn: [],
},
},
} satisfies ExtensionProfileMetadata<TestType, "forwarder">;
describe("ExtensionService", () => {
beforeEach(() => {
jest.resetAllMocks();
});
describe("settings", () => {
it("writes to the user's state", async () => {
const extension = new ExtensionService(SomeRegistry, SomeProvider);
SomeRegistry.extension.mockReturnValue(SomeExtension);
const subject = extension.settings(SomeProfileMetadata, Vendor.simplelogin, {
account$: SomeAccount$,
});
subject.next({ foo: "next value" });
await awaitAsync();
// if the write succeeded, then the storage location should contain an object;
// the precise value isn't tested to avoid coupling the test to the storage format
const expectedKey = new UserKeyDefinition(
EXTENSION_DISK,
"forwarder.simplelogin.someProfile",
SomeProfileMetadata.storage.options,
);
const result = await firstValueFrom(SomeStateProvider.getUserState$(expectedKey, SomeUser));
expect(result).toBeTruthy();
});
it("panics when the extension metadata isn't available", async () => {
const extension = new ExtensionService(SomeRegistry, SomeProvider);
expect(() =>
extension.settings(SomeProfileMetadata, Vendor.bitwarden, { account$: SomeAccount$ }),
).toThrow("extension not defined");
});
});
describe("site", () => {
it("returns an extension site", () => {
const expected = new ExtensionSite(SomeExtension.site, new Map());
SomeRegistry.build.mockReturnValueOnce(expected);
const extension = new ExtensionService(SomeRegistry, SomeProvider);
const site = extension.site(Site.forwarder);
expect(site).toEqual(expected);
});
});
});

View File

@@ -14,33 +14,36 @@ describe("DefaultSemanticLogger", () => {
describe("debug", () => {
it("writes structural log messages to console.log", () => {
const log = new DefaultSemanticLogger(logger, {});
const log = new DefaultSemanticLogger(logger, {}, () => 0);
log.debug("this is a debug message");
expect(logger.write).toHaveBeenCalledWith(LogLevelType.Debug, {
"@timestamp": 0,
message: "this is a debug message",
level: "debug",
});
});
it("writes structural content to console.log", () => {
const log = new DefaultSemanticLogger(logger, {});
const log = new DefaultSemanticLogger(logger, {}, () => 0);
log.debug({ example: "this is content" });
expect(logger.write).toHaveBeenCalledWith(LogLevelType.Debug, {
"@timestamp": 0,
content: { example: "this is content" },
level: "debug",
});
});
it("writes structural content to console.log with a message", () => {
const log = new DefaultSemanticLogger(logger, {});
const log = new DefaultSemanticLogger(logger, {}, () => 0);
log.info({ example: "this is content" }, "this is a message");
expect(logger.write).toHaveBeenCalledWith(LogLevelType.Info, {
"@timestamp": 0,
content: { example: "this is content" },
message: "this is a message",
level: "information",
@@ -50,33 +53,36 @@ describe("DefaultSemanticLogger", () => {
describe("info", () => {
it("writes structural log messages to console.log", () => {
const log = new DefaultSemanticLogger(logger, {});
const log = new DefaultSemanticLogger(logger, {}, () => 0);
log.info("this is an info message");
expect(logger.write).toHaveBeenCalledWith(LogLevelType.Info, {
"@timestamp": 0,
message: "this is an info message",
level: "information",
});
});
it("writes structural content to console.log", () => {
const log = new DefaultSemanticLogger(logger, {});
const log = new DefaultSemanticLogger(logger, {}, () => 0);
log.info({ example: "this is content" });
expect(logger.write).toHaveBeenCalledWith(LogLevelType.Info, {
"@timestamp": 0,
content: { example: "this is content" },
level: "information",
});
});
it("writes structural content to console.log with a message", () => {
const log = new DefaultSemanticLogger(logger, {});
const log = new DefaultSemanticLogger(logger, {}, () => 0);
log.info({ example: "this is content" }, "this is a message");
expect(logger.write).toHaveBeenCalledWith(LogLevelType.Info, {
"@timestamp": 0,
content: { example: "this is content" },
message: "this is a message",
level: "information",
@@ -86,33 +92,36 @@ describe("DefaultSemanticLogger", () => {
describe("warn", () => {
it("writes structural log messages to console.warn", () => {
const log = new DefaultSemanticLogger(logger, {});
const log = new DefaultSemanticLogger(logger, {}, () => 0);
log.warn("this is a warning message");
expect(logger.write).toHaveBeenCalledWith(LogLevelType.Warning, {
"@timestamp": 0,
message: "this is a warning message",
level: "warning",
});
});
it("writes structural content to console.warn", () => {
const log = new DefaultSemanticLogger(logger, {});
const log = new DefaultSemanticLogger(logger, {}, () => 0);
log.warn({ example: "this is content" });
expect(logger.write).toHaveBeenCalledWith(LogLevelType.Warning, {
"@timestamp": 0,
content: { example: "this is content" },
level: "warning",
});
});
it("writes structural content to console.warn with a message", () => {
const log = new DefaultSemanticLogger(logger, {});
const log = new DefaultSemanticLogger(logger, {}, () => 0);
log.warn({ example: "this is content" }, "this is a message");
expect(logger.write).toHaveBeenCalledWith(LogLevelType.Warning, {
"@timestamp": 0,
content: { example: "this is content" },
message: "this is a message",
level: "warning",
@@ -122,33 +131,36 @@ describe("DefaultSemanticLogger", () => {
describe("error", () => {
it("writes structural log messages to console.error", () => {
const log = new DefaultSemanticLogger(logger, {});
const log = new DefaultSemanticLogger(logger, {}, () => 0);
log.error("this is an error message");
expect(logger.write).toHaveBeenCalledWith(LogLevelType.Error, {
"@timestamp": 0,
message: "this is an error message",
level: "error",
});
});
it("writes structural content to console.error", () => {
const log = new DefaultSemanticLogger(logger, {});
const log = new DefaultSemanticLogger(logger, {}, () => 0);
log.error({ example: "this is content" });
expect(logger.write).toHaveBeenCalledWith(LogLevelType.Error, {
"@timestamp": 0,
content: { example: "this is content" },
level: "error",
});
});
it("writes structural content to console.error with a message", () => {
const log = new DefaultSemanticLogger(logger, {});
const log = new DefaultSemanticLogger(logger, {}, () => 0);
log.error({ example: "this is content" }, "this is a message");
expect(logger.write).toHaveBeenCalledWith(LogLevelType.Error, {
"@timestamp": 0,
content: { example: "this is content" },
message: "this is a message",
level: "error",
@@ -158,24 +170,26 @@ describe("DefaultSemanticLogger", () => {
describe("panic", () => {
it("writes structural log messages to console.error before throwing the message", () => {
const log = new DefaultSemanticLogger(logger, {});
const log = new DefaultSemanticLogger(logger, {}, () => 0);
expect(() => log.panic("this is an error message")).toThrow("this is an error message");
expect(logger.write).toHaveBeenCalledWith(LogLevelType.Error, {
"@timestamp": 0,
message: "this is an error message",
level: "error",
});
});
it("writes structural log messages to console.error with a message before throwing the message", () => {
const log = new DefaultSemanticLogger(logger, {});
const log = new DefaultSemanticLogger(logger, {}, () => 0);
expect(() => log.panic({ example: "this is content" }, "this is an error message")).toThrow(
"this is an error message",
);
expect(logger.write).toHaveBeenCalledWith(LogLevelType.Error, {
"@timestamp": 0,
content: { example: "this is content" },
message: "this is an error message",
level: "error",
@@ -183,13 +197,14 @@ describe("DefaultSemanticLogger", () => {
});
it("writes structural log messages to console.error with a content before throwing the message", () => {
const log = new DefaultSemanticLogger(logger, {});
const log = new DefaultSemanticLogger(logger, {}, () => 0);
expect(() => log.panic("this is content", "this is an error message")).toThrow(
"this is an error message",
);
expect(logger.write).toHaveBeenCalledWith(LogLevelType.Error, {
"@timestamp": 0,
content: "this is content",
message: "this is an error message",
level: "error",

View File

@@ -18,6 +18,7 @@ export class DefaultSemanticLogger<Context extends object> implements SemanticLo
constructor(
private logger: LogService,
context: Jsonify<Context>,
private now = () => Date.now(),
) {
this.context = context && typeof context === "object" ? context : {};
}
@@ -53,6 +54,7 @@ export class DefaultSemanticLogger<Context extends object> implements SemanticLo
message,
content: content ?? undefined,
level: stringifyLevel(level),
"@timestamp": this.now(),
};
if (typeof content === "string" && !message) {

View File

@@ -17,7 +17,7 @@ export class PrivateClassifier<Data> implements Classifier<Data, Record<string,
}
const secret = picked as Jsonify<Data>;
return { disclosed: null, secret };
return { disclosed: {}, secret };
}
declassify(_disclosed: Jsonify<Record<keyof Data, never>>, secret: Jsonify<Data>) {

View File

@@ -16,7 +16,7 @@ export class PublicClassifier<Data> implements Classifier<Data, Data, Record<str
}
const disclosed = picked as Jsonify<Data>;
return { disclosed, secret: null };
return { disclosed, secret: "" };
}
declassify(disclosed: Jsonify<Data>, _secret: Jsonify<Record<keyof Data, never>>) {

View File

@@ -0,0 +1,27 @@
import { isClassifiedFormat } from "./classified-format";
describe("isClassifiedFormat", () => {
it("returns `false` when the argument is `null`", () => {
expect(isClassifiedFormat(null)).toEqual(false);
});
it.each([
[{ id: true, secret: "" }],
[{ secret: "", disclosed: {} }],
[{ id: true, disclosed: {} }],
])("returns `false` when the argument is missing a required member (=%p).", (value) => {
expect(isClassifiedFormat(value)).toEqual(false);
});
it("returns `false` when 'secret' is not a string", () => {
expect(isClassifiedFormat({ id: true, secret: false, disclosed: {} })).toEqual(false);
});
it("returns `false` when 'disclosed' is not an object", () => {
expect(isClassifiedFormat({ id: true, secret: "", disclosed: false })).toEqual(false);
});
it("returns `true` when the argument has a `secret`, `disclosed`, and `id`.", () => {
expect(isClassifiedFormat({ id: true, secret: "", disclosed: {} })).toEqual(true);
});
});

View File

@@ -21,5 +21,12 @@ export type ClassifiedFormat<Id, Disclosed> = {
export function isClassifiedFormat<Id, Disclosed>(
value: any,
): value is ClassifiedFormat<Id, Disclosed> {
return "id" in value && "secret" in value && "disclosed" in value;
return (
!!value &&
"id" in value &&
"secret" in value &&
"disclosed" in value &&
typeof value.secret === "string" &&
typeof value.disclosed === "object"
);
}

View File

@@ -523,6 +523,7 @@ export class UserStateSubject<
private onError(value: any) {
if (!this.isDisposed) {
this.log.debug(value, "forwarding error to subscribers");
this.output.error(value);
}