mirror of
https://github.com/bitwarden/browser
synced 2026-02-09 05:00:10 +00:00
unit tests
This commit is contained in:
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>) {
|
||||
|
||||
@@ -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>>) {
|
||||
|
||||
27
libs/common/src/tools/state/classified-format.spec.ts
Normal file
27
libs/common/src/tools/state/classified-format.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user