288 lines
8.8 KiB
TypeScript
288 lines
8.8 KiB
TypeScript
import { awaitAsync } from "@bitwarden/angular/../test-utils";
|
|
import { mock, MockProxy } from "jest-mock-extended";
|
|
import { BehaviorSubject, ReplaySubject } from "rxjs";
|
|
|
|
import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service";
|
|
|
|
import { BrowserApi } from "../../browser/browser-api";
|
|
|
|
import { SessionSyncer } from "./session-syncer";
|
|
import { SyncedItemMetadata } from "./sync-item-metadata";
|
|
|
|
describe("session syncer", () => {
|
|
const propertyKey = "behaviorSubject";
|
|
const sessionKey = "Test__" + propertyKey;
|
|
const metaData: SyncedItemMetadata = {
|
|
propertyKey,
|
|
sessionKey,
|
|
initializer: (s: string) => s,
|
|
initializeAs: "object",
|
|
};
|
|
let storageService: MockProxy<MemoryStorageService>;
|
|
let sut: SessionSyncer;
|
|
let behaviorSubject: BehaviorSubject<string>;
|
|
|
|
beforeEach(() => {
|
|
behaviorSubject = new BehaviorSubject<string>("");
|
|
jest.spyOn(chrome.runtime, "getManifest").mockReturnValue({
|
|
name: "bitwarden-test",
|
|
version: "0.0.0",
|
|
manifest_version: 3,
|
|
});
|
|
|
|
storageService = mock();
|
|
storageService.has.mockResolvedValue(false);
|
|
sut = new SessionSyncer(behaviorSubject, storageService, metaData);
|
|
});
|
|
|
|
afterEach(() => {
|
|
jest.resetAllMocks();
|
|
|
|
behaviorSubject.complete();
|
|
});
|
|
|
|
describe("constructor", () => {
|
|
it("should throw if subject is not an instance of Subject", () => {
|
|
expect(() => {
|
|
new SessionSyncer({} as any, storageService, null);
|
|
}).toThrowError("subject must inherit from Subject");
|
|
});
|
|
|
|
it("should create if either ctor or initializer is provided", () => {
|
|
expect(
|
|
new SessionSyncer(behaviorSubject, storageService, {
|
|
propertyKey,
|
|
sessionKey,
|
|
initializeAs: "object",
|
|
initializer: () => null,
|
|
})
|
|
).toBeDefined();
|
|
expect(
|
|
new SessionSyncer(behaviorSubject, storageService, {
|
|
propertyKey,
|
|
sessionKey,
|
|
initializer: (s: any) => s,
|
|
initializeAs: "object",
|
|
})
|
|
).toBeDefined();
|
|
});
|
|
it("should throw if neither ctor or initializer is provided", () => {
|
|
expect(() => {
|
|
new SessionSyncer(behaviorSubject, storageService, {
|
|
propertyKey,
|
|
sessionKey,
|
|
initializeAs: "object",
|
|
initializer: null,
|
|
});
|
|
}).toThrowError("initializer must be provided");
|
|
});
|
|
});
|
|
|
|
describe("init", () => {
|
|
it("should ignore all updates currently in a ReplaySubject's buffer", () => {
|
|
const replaySubject = new ReplaySubject<string>(Infinity);
|
|
replaySubject.next("1");
|
|
replaySubject.next("2");
|
|
replaySubject.next("3");
|
|
sut = new SessionSyncer(replaySubject, storageService, metaData);
|
|
// block observing the subject
|
|
jest.spyOn(sut as any, "observe").mockImplementation();
|
|
|
|
sut.init();
|
|
|
|
expect(sut["ignoreNUpdates"]).toBe(3);
|
|
});
|
|
|
|
it("should ignore BehaviorSubject's initial value", () => {
|
|
const behaviorSubject = new BehaviorSubject<string>("initial");
|
|
sut = new SessionSyncer(behaviorSubject, storageService, metaData);
|
|
// block observing the subject
|
|
jest.spyOn(sut as any, "observe").mockImplementation();
|
|
|
|
sut.init();
|
|
|
|
expect(sut["ignoreNUpdates"]).toBe(1);
|
|
});
|
|
|
|
it("should grab an initial value from storage if it exists", async () => {
|
|
storageService.has.mockResolvedValue(true);
|
|
//Block a call to update
|
|
const updateSpy = jest.spyOn(sut as any, "updateFromMemory").mockImplementation();
|
|
|
|
sut.init();
|
|
await awaitAsync();
|
|
|
|
expect(updateSpy).toHaveBeenCalledWith();
|
|
});
|
|
|
|
it("should not grab an initial value from storage if it does not exist", async () => {
|
|
storageService.has.mockResolvedValue(false);
|
|
//Block a call to update
|
|
const updateSpy = jest.spyOn(sut as any, "update").mockImplementation();
|
|
|
|
sut.init();
|
|
await awaitAsync();
|
|
|
|
expect(updateSpy).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe("a value is emitted on the observable", () => {
|
|
let sendMessageSpy: jest.SpyInstance;
|
|
const value = "test";
|
|
const serializedValue = JSON.stringify(value);
|
|
|
|
beforeEach(() => {
|
|
sendMessageSpy = jest.spyOn(BrowserApi, "sendMessage");
|
|
|
|
sut.init();
|
|
|
|
behaviorSubject.next(value);
|
|
});
|
|
|
|
it("should update sessionSyncers in other contexts", async () => {
|
|
// await finishing of fire-and-forget operation
|
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
|
|
expect(sendMessageSpy).toHaveBeenCalledTimes(1);
|
|
expect(sendMessageSpy).toHaveBeenCalledWith(`${sessionKey}_update`, {
|
|
id: sut.id,
|
|
serializedValue,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("A message is received", () => {
|
|
let nextSpy: jest.SpyInstance;
|
|
let sendMessageSpy: jest.SpyInstance;
|
|
|
|
beforeEach(() => {
|
|
nextSpy = jest.spyOn(behaviorSubject, "next");
|
|
sendMessageSpy = jest.spyOn(BrowserApi, "sendMessage");
|
|
|
|
sut.init();
|
|
});
|
|
|
|
afterEach(() => {
|
|
jest.resetAllMocks();
|
|
});
|
|
|
|
it("should ignore messages with the wrong command", async () => {
|
|
await sut.updateFromMessage({ command: "wrong_command", id: sut.id });
|
|
|
|
expect(storageService.getBypassCache).not.toHaveBeenCalled();
|
|
expect(nextSpy).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("should ignore messages from itself", async () => {
|
|
await sut.updateFromMessage({ command: `${sessionKey}_update`, id: sut.id });
|
|
|
|
expect(storageService.getBypassCache).not.toHaveBeenCalled();
|
|
expect(nextSpy).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("should update from message on emit from another instance", async () => {
|
|
const builder = jest.fn();
|
|
jest.spyOn(SyncedItemMetadata, "builder").mockReturnValue(builder);
|
|
const value = "test";
|
|
const serializedValue = JSON.stringify(value);
|
|
builder.mockReturnValue(value);
|
|
|
|
// Expect no circular messaging
|
|
await awaitAsync();
|
|
expect(sendMessageSpy).toHaveBeenCalledTimes(0);
|
|
|
|
await sut.updateFromMessage({
|
|
command: `${sessionKey}_update`,
|
|
id: "different_id",
|
|
serializedValue,
|
|
});
|
|
await awaitAsync();
|
|
|
|
expect(storageService.getBypassCache).toHaveBeenCalledTimes(0);
|
|
|
|
expect(nextSpy).toHaveBeenCalledTimes(1);
|
|
expect(nextSpy).toHaveBeenCalledWith(value);
|
|
expect(behaviorSubject.value).toBe(value);
|
|
|
|
// Expect no circular messaging
|
|
expect(sendMessageSpy).toHaveBeenCalledTimes(0);
|
|
});
|
|
});
|
|
|
|
describe("memory storage", () => {
|
|
const value = "test";
|
|
const serializedValue = JSON.stringify(value);
|
|
let saveSpy: jest.SpyInstance;
|
|
const builder = jest.fn().mockReturnValue(value);
|
|
const manifestVersionSpy = jest.spyOn(BrowserApi, "manifestVersion", "get");
|
|
const isBackgroundPageSpy = jest.spyOn(BrowserApi, "isBackgroundPage");
|
|
|
|
beforeEach(async () => {
|
|
jest.spyOn(SyncedItemMetadata, "builder").mockReturnValue(builder);
|
|
saveSpy = jest.spyOn(storageService, "save");
|
|
|
|
sut.init();
|
|
await awaitAsync();
|
|
});
|
|
|
|
afterEach(() => {
|
|
jest.resetAllMocks();
|
|
});
|
|
|
|
it("should always store on observed next for manifest version 3", async () => {
|
|
manifestVersionSpy.mockReturnValue(3);
|
|
isBackgroundPageSpy.mockReturnValueOnce(true).mockReturnValueOnce(false);
|
|
behaviorSubject.next(value);
|
|
await awaitAsync();
|
|
behaviorSubject.next(value);
|
|
await awaitAsync();
|
|
|
|
expect(saveSpy).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it("should not store on message receive for manifest version 3", async () => {
|
|
manifestVersionSpy.mockReturnValue(3);
|
|
isBackgroundPageSpy.mockReturnValueOnce(true).mockReturnValueOnce(false);
|
|
await sut.updateFromMessage({
|
|
command: `${sessionKey}_update`,
|
|
id: "different_id",
|
|
serializedValue,
|
|
});
|
|
await awaitAsync();
|
|
|
|
expect(saveSpy).toHaveBeenCalledTimes(0);
|
|
});
|
|
|
|
it("should store on message receive for manifest version 2 for background page only", async () => {
|
|
manifestVersionSpy.mockReturnValue(2);
|
|
isBackgroundPageSpy.mockReturnValueOnce(true).mockReturnValueOnce(false);
|
|
await sut.updateFromMessage({
|
|
command: `${sessionKey}_update`,
|
|
id: "different_id",
|
|
serializedValue,
|
|
});
|
|
await awaitAsync();
|
|
await sut.updateFromMessage({
|
|
command: `${sessionKey}_update`,
|
|
id: "different_id",
|
|
serializedValue,
|
|
});
|
|
await awaitAsync();
|
|
|
|
expect(saveSpy).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("should store on observed next for manifest version 2 for background page only", async () => {
|
|
manifestVersionSpy.mockReturnValue(2);
|
|
isBackgroundPageSpy.mockReturnValueOnce(true).mockReturnValueOnce(false);
|
|
behaviorSubject.next(value);
|
|
await awaitAsync();
|
|
behaviorSubject.next(value);
|
|
await awaitAsync();
|
|
|
|
expect(saveSpy).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|
|
});
|