mirror of
https://github.com/bitwarden/browser.git
synced 2024-10-09 05:57:40 +02:00
1ff7bdd014
* Create MigrationRunner - Create MigrationRunner Service for running migrations in StateService - Create web override so that migrations also run against `localStorage` * Fix Web StateService * Fix WebMigrationRunner * Fix CLI * Fix ElectronStateService * Update Comment * More Common Scenarios
253 lines
8.1 KiB
TypeScript
253 lines
8.1 KiB
TypeScript
import { MockProxy, mock } from "jest-mock-extended";
|
|
|
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
|
import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
|
|
import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service";
|
|
import { MigrationBuilder } from "@bitwarden/common/state-migrations/migration-builder";
|
|
import { MigrationHelper } from "@bitwarden/common/state-migrations/migration-helper";
|
|
|
|
import { WebMigrationRunner } from "./web-migration-runner";
|
|
import { WindowStorageService } from "./window-storage.service";
|
|
|
|
describe("WebMigrationRunner", () => {
|
|
let logService: MockProxy<LogService>;
|
|
let sessionStorageService: MockProxy<AbstractStorageService>;
|
|
let localStorageService: MockProxy<WindowStorageService>;
|
|
let migrationBuilderService: MockProxy<MigrationBuilderService>;
|
|
|
|
let sut: WebMigrationRunner;
|
|
|
|
beforeEach(() => {
|
|
logService = mock();
|
|
sessionStorageService = mock();
|
|
localStorageService = mock();
|
|
migrationBuilderService = mock();
|
|
|
|
sut = new WebMigrationRunner(
|
|
sessionStorageService,
|
|
logService,
|
|
migrationBuilderService,
|
|
localStorageService,
|
|
);
|
|
});
|
|
|
|
const mockMigrationBuilder = (migration: (helper: MigrationHelper) => Promise<void>) => {
|
|
migrationBuilderService.build.mockReturnValue({
|
|
migrate: async (helper: MigrationHelper) => {
|
|
await migration(helper);
|
|
},
|
|
with: () => {
|
|
throw new Error("Don't use this in tests.");
|
|
},
|
|
rollback: () => {
|
|
throw new Error("Don't use this in tests.");
|
|
},
|
|
} as unknown as MigrationBuilder);
|
|
};
|
|
|
|
const mockGet = (
|
|
mockStorage: MockProxy<AbstractStorageService>,
|
|
data: Record<string, unknown>,
|
|
) => {
|
|
mockStorage.get.mockImplementation((key) => {
|
|
return Promise.resolve(data[key]);
|
|
});
|
|
};
|
|
|
|
it("should run migration for both storage locations", async () => {
|
|
mockGet(sessionStorageService, {
|
|
stateVersion: 4,
|
|
});
|
|
mockGet(localStorageService, {});
|
|
|
|
mockMigrationBuilder(async (helper) => {
|
|
await helper.set("something", "something");
|
|
});
|
|
|
|
await sut.run();
|
|
|
|
expect(sessionStorageService.save).toHaveBeenCalledWith("something", "something");
|
|
expect(localStorageService.save).toHaveBeenCalledWith("something", "something");
|
|
});
|
|
|
|
it("should only migrate data in one migration if written defensively", async () => {
|
|
mockGet(sessionStorageService, {
|
|
stateVersion: 4,
|
|
});
|
|
mockGet(localStorageService, {
|
|
user1: {
|
|
settings: {
|
|
myData: "value",
|
|
},
|
|
},
|
|
});
|
|
|
|
mockMigrationBuilder(async (helper) => {
|
|
const account = await helper.get<{ settings?: { myData?: string } }>("user1");
|
|
const value = account?.settings?.myData;
|
|
if (value) {
|
|
await helper.setToUser("user1", { key: "key", stateDefinition: { name: "state" } }, value);
|
|
}
|
|
});
|
|
|
|
await sut.run();
|
|
|
|
expect(sessionStorageService.save).not.toHaveBeenCalled();
|
|
expect(localStorageService.save).toHaveBeenCalledWith("user_user1_state_key", "value");
|
|
});
|
|
|
|
it("should gather accounts differently", async () => {
|
|
mockGet(sessionStorageService, {
|
|
stateVersion: 10,
|
|
authenticatedAccounts: ["sessionUser1", "sessionUser2"],
|
|
sessionUser1: {
|
|
data: 1,
|
|
},
|
|
sessionUser2: {
|
|
data: null,
|
|
},
|
|
sessionUser3: {
|
|
// User does NOT have authenticated accounts entry
|
|
data: 3,
|
|
},
|
|
});
|
|
|
|
const localStorageObject = {
|
|
"8118af89-a621-4b0f-8dd2-4449569e5067": {
|
|
data: 4,
|
|
},
|
|
"cc202dba-55f8-4cbe-8c66-de37e48e7827": {
|
|
data: <number>null,
|
|
},
|
|
otherThing: {
|
|
data: 6,
|
|
},
|
|
"badd2aff-a380-468f-855a-e476557055d5": <object>null,
|
|
"01f81ccd-fb18-460c-9a6b-811ef5300d4b": 3,
|
|
};
|
|
|
|
mockGet(localStorageService, localStorageObject);
|
|
localStorageService.getKeys.mockReturnValue(Object.keys(localStorageObject));
|
|
|
|
mockMigrationBuilder(async (helper) => {
|
|
type ExpectedAccountType = {
|
|
data?: number;
|
|
};
|
|
async function migrateAccount(userId: string, account: ExpectedAccountType) {
|
|
const value = account?.data;
|
|
if (value != null) {
|
|
await helper.setToUser(userId, { key: "key", stateDefinition: { name: "state" } }, value);
|
|
delete account.data;
|
|
await helper.set(userId, account);
|
|
}
|
|
}
|
|
|
|
const accounts = await helper.getAccounts();
|
|
await Promise.all(accounts.map(({ userId, account }) => migrateAccount(userId, account)));
|
|
});
|
|
|
|
await sut.run();
|
|
|
|
// Session storage has two users but only one with data
|
|
expect(sessionStorageService.save).toHaveBeenCalledTimes(2);
|
|
// Should move the data to the new location first
|
|
expect(sessionStorageService.save).toHaveBeenNthCalledWith(1, "user_sessionUser1_state_key", 1);
|
|
// Should then delete the migrated data and resave object
|
|
expect(sessionStorageService.save).toHaveBeenNthCalledWith(2, "sessionUser1", {});
|
|
|
|
expect(sessionStorageService.get).toHaveBeenCalledTimes(4);
|
|
// Should first get the state version so it knowns which migrations to run (not really used in this test)
|
|
expect(sessionStorageService.get).toHaveBeenNthCalledWith(1, "stateVersion");
|
|
// "base" migration runner should trust the authenticatedAccounts stored value for knowing which accounts to migrate
|
|
expect(sessionStorageService.get).toHaveBeenNthCalledWith(2, "authenticatedAccounts");
|
|
// Should get the data for each user
|
|
expect(sessionStorageService.get).toHaveBeenNthCalledWith(3, "sessionUser1");
|
|
expect(sessionStorageService.get).toHaveBeenNthCalledWith(4, "sessionUser2");
|
|
|
|
expect(localStorageService.save).toHaveBeenCalledTimes(2);
|
|
// Should migrate data for a user in local storage
|
|
expect(localStorageService.save).toHaveBeenNthCalledWith(
|
|
1,
|
|
"user_8118af89-a621-4b0f-8dd2-4449569e5067_state_key",
|
|
4,
|
|
);
|
|
// Should update object with migrated data deleted
|
|
expect(localStorageService.save).toHaveBeenNthCalledWith(
|
|
2,
|
|
"8118af89-a621-4b0f-8dd2-4449569e5067",
|
|
{},
|
|
);
|
|
|
|
expect(localStorageService.get).toHaveBeenCalledTimes(5);
|
|
expect(localStorageService.get).toHaveBeenNthCalledWith(1, "stateVersion");
|
|
expect(localStorageService.get).toHaveBeenNthCalledWith(
|
|
2,
|
|
"8118af89-a621-4b0f-8dd2-4449569e5067",
|
|
);
|
|
expect(localStorageService.get).toHaveBeenNthCalledWith(
|
|
3,
|
|
"cc202dba-55f8-4cbe-8c66-de37e48e7827",
|
|
);
|
|
expect(localStorageService.get).toHaveBeenNthCalledWith(
|
|
4,
|
|
"badd2aff-a380-468f-855a-e476557055d5",
|
|
);
|
|
expect(localStorageService.get).toHaveBeenNthCalledWith(
|
|
5,
|
|
"01f81ccd-fb18-460c-9a6b-811ef5300d4b",
|
|
);
|
|
});
|
|
|
|
it("should default currentVersion to 12 if no stateVersion exists", async () => {
|
|
mockGet(sessionStorageService, {
|
|
stateVersion: 14,
|
|
});
|
|
mockGet(localStorageService, {});
|
|
|
|
let runCount = 0;
|
|
|
|
mockMigrationBuilder(async (helper) => {
|
|
if (runCount === 0) {
|
|
// This should be the session storage run
|
|
expect(helper.currentVersion).toBe(14);
|
|
} else if (runCount === 1) {
|
|
// This should be the local storage run, and it should be the default version
|
|
expect(helper.currentVersion).toBe(12);
|
|
} else {
|
|
throw new Error("Should not have been called more than twice");
|
|
}
|
|
|
|
runCount++;
|
|
});
|
|
|
|
await sut.run();
|
|
});
|
|
|
|
it("should respect local storage stateVersion", async () => {
|
|
mockGet(sessionStorageService, {
|
|
stateVersion: 14,
|
|
});
|
|
mockGet(localStorageService, {
|
|
stateVersion: 18,
|
|
});
|
|
|
|
let runCount = 0;
|
|
|
|
mockMigrationBuilder(async (helper) => {
|
|
if (runCount === 0) {
|
|
// This should be the session storage run
|
|
expect(helper.currentVersion).toBe(14);
|
|
} else if (runCount === 1) {
|
|
// This should be the local storage run, and it should be the default version
|
|
expect(helper.currentVersion).toBe(18);
|
|
} else {
|
|
throw new Error("Should not have been called more than twice");
|
|
}
|
|
|
|
runCount++;
|
|
});
|
|
|
|
await sut.run();
|
|
});
|
|
});
|