mirror of
https://github.com/bitwarden/browser
synced 2025-12-21 10:43:35 +00:00
refactor: introduce @bitwarden/state and other common libs (#15772)
* refactor: introduce @bitwarden/serialization * refactor: introduce @bitwarden/guid * refactor: introduce @bitwaren/client-type * refactor: introduce @bitwarden/core-test-utils * refactor: introduce @bitwarden/state and @bitwarden/state-test-utils Creates initial project structure for centralized application state management. Part of modularization effort to extract state code from common. * Added state provider documentation to README. * Changed callouts to Github format. * Fixed linting on file name. * Forced git to accept rename --------- Co-authored-by: Todd Martin <tmartin@bitwarden.com>
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
import { MockProxy, any } from "jest-mock-extended";
|
||||
|
||||
import { MigrationHelper } from "../migration-helper";
|
||||
import { mockMigrationHelper } from "../migration-helper.spec";
|
||||
|
||||
import { EverHadUserKeyMigrator } from "./10-move-ever-had-user-key-to-state-providers";
|
||||
|
||||
function exampleJSON() {
|
||||
return {
|
||||
global: {
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
authenticatedAccounts: [
|
||||
"c493ed01-4e08-4e88-abc7-332f380ca760",
|
||||
"23e61a5f-2ece-4f5e-b499-f0bc489482a9",
|
||||
"fd005ea6-a16a-45ef-ba4a-a194269bfd73",
|
||||
],
|
||||
"c493ed01-4e08-4e88-abc7-332f380ca760": {
|
||||
profile: {
|
||||
everHadUserKey: false,
|
||||
otherStuff: "overStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
},
|
||||
"23e61a5f-2ece-4f5e-b499-f0bc489482a9": {
|
||||
profile: {
|
||||
everHadUserKey: true,
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function rollbackJSON() {
|
||||
return {
|
||||
"user_c493ed01-4e08-4e88-abc7-332f380ca760_crypto_everHadUserKey": false,
|
||||
"user_23e61a5f-2ece-4f5e-b499-f0bc489482a9_crypto_everHadUserKey": true,
|
||||
"user_fd005ea6-a16a-45ef-ba4a-a194269bfd73_crypto_everHadUserKey": false,
|
||||
global: {
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
authenticatedAccounts: [
|
||||
"c493ed01-4e08-4e88-abc7-332f380ca760",
|
||||
"23e61a5f-2ece-4f5e-b499-f0bc489482a9",
|
||||
"fd005ea6-a16a-45ef-ba4a-a194269bfd73",
|
||||
],
|
||||
"c493ed01-4e08-4e88-abc7-332f380ca760": {
|
||||
profile: {
|
||||
everHadUserKey: false,
|
||||
otherStuff: "overStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
},
|
||||
"23e61a5f-2ece-4f5e-b499-f0bc489482a9": {
|
||||
profile: {
|
||||
everHadUserKey: true,
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("EverHadUserKeyMigrator", () => {
|
||||
let helper: MockProxy<MigrationHelper>;
|
||||
let sut: EverHadUserKeyMigrator;
|
||||
const keyDefinitionLike = {
|
||||
key: "everHadUserKey",
|
||||
stateDefinition: {
|
||||
name: "crypto",
|
||||
},
|
||||
};
|
||||
|
||||
describe("migrate", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(exampleJSON(), 9);
|
||||
sut = new EverHadUserKeyMigrator(9, 10);
|
||||
});
|
||||
|
||||
it("should remove everHadUserKey from all accounts", async () => {
|
||||
await sut.migrate(helper);
|
||||
expect(helper.set).toHaveBeenCalledWith("c493ed01-4e08-4e88-abc7-332f380ca760", {
|
||||
profile: {
|
||||
otherStuff: "overStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
});
|
||||
expect(helper.set).toHaveBeenCalledWith("23e61a5f-2ece-4f5e-b499-f0bc489482a9", {
|
||||
profile: {
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
});
|
||||
});
|
||||
|
||||
it("should set everHadUserKey provider value for each account", async () => {
|
||||
await sut.migrate(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(
|
||||
"c493ed01-4e08-4e88-abc7-332f380ca760",
|
||||
keyDefinitionLike,
|
||||
false,
|
||||
);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(
|
||||
"23e61a5f-2ece-4f5e-b499-f0bc489482a9",
|
||||
keyDefinitionLike,
|
||||
true,
|
||||
);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(
|
||||
"fd005ea6-a16a-45ef-ba4a-a194269bfd73",
|
||||
keyDefinitionLike,
|
||||
false,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("rollback", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(rollbackJSON(), 10);
|
||||
sut = new EverHadUserKeyMigrator(9, 10);
|
||||
});
|
||||
|
||||
it.each([
|
||||
"c493ed01-4e08-4e88-abc7-332f380ca760",
|
||||
"23e61a5f-2ece-4f5e-b499-f0bc489482a9",
|
||||
"fd005ea6-a16a-45ef-ba4a-a194269bfd73",
|
||||
])("should null out new values", async (userId) => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(userId, keyDefinitionLike, null);
|
||||
});
|
||||
|
||||
it("should add explicit value back to accounts", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.set).toHaveBeenCalledWith("c493ed01-4e08-4e88-abc7-332f380ca760", {
|
||||
profile: {
|
||||
everHadUserKey: false,
|
||||
otherStuff: "overStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
});
|
||||
expect(helper.set).toHaveBeenCalledWith("23e61a5f-2ece-4f5e-b499-f0bc489482a9", {
|
||||
profile: {
|
||||
everHadUserKey: true,
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
});
|
||||
});
|
||||
|
||||
it("should not try to restore values to missing accounts", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.set).not.toHaveBeenCalledWith("fd005ea6-a16a-45ef-ba4a-a194269bfd73", any());
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
|
||||
import { Migrator } from "../migrator";
|
||||
|
||||
type ExpectedAccountType = {
|
||||
profile?: {
|
||||
everHadUserKey?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
const USER_EVER_HAD_USER_KEY: KeyDefinitionLike = {
|
||||
key: "everHadUserKey",
|
||||
stateDefinition: {
|
||||
name: "crypto",
|
||||
},
|
||||
};
|
||||
|
||||
export class EverHadUserKeyMigrator extends Migrator<9, 10> {
|
||||
async migrate(helper: MigrationHelper): Promise<void> {
|
||||
const accounts = await helper.getAccounts<ExpectedAccountType>();
|
||||
async function migrateAccount(userId: string, account: ExpectedAccountType): Promise<void> {
|
||||
const value = account?.profile?.everHadUserKey;
|
||||
await helper.setToUser(userId, USER_EVER_HAD_USER_KEY, value ?? false);
|
||||
if (value != null) {
|
||||
delete account.profile.everHadUserKey;
|
||||
}
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
|
||||
await Promise.all([...accounts.map(({ userId, account }) => migrateAccount(userId, account))]);
|
||||
}
|
||||
async rollback(helper: MigrationHelper): Promise<void> {
|
||||
const accounts = await helper.getAccounts<ExpectedAccountType>();
|
||||
async function rollbackAccount(userId: string, account: ExpectedAccountType): Promise<void> {
|
||||
const value = await helper.getFromUser(userId, USER_EVER_HAD_USER_KEY);
|
||||
if (account) {
|
||||
account.profile = Object.assign(account.profile ?? {}, {
|
||||
everHadUserKey: value,
|
||||
});
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
await helper.setToUser(userId, USER_EVER_HAD_USER_KEY, null);
|
||||
}
|
||||
|
||||
await Promise.all([...accounts.map(({ userId, account }) => rollbackAccount(userId, account))]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
import { MockProxy, any } from "jest-mock-extended";
|
||||
|
||||
import { MigrationHelper } from "../migration-helper";
|
||||
import { mockMigrationHelper } from "../migration-helper.spec";
|
||||
|
||||
import { OrganizationKeyMigrator } from "./11-move-org-keys-to-state-providers";
|
||||
|
||||
function exampleJSON() {
|
||||
return {
|
||||
global: {
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
authenticatedAccounts: ["user-1", "user-2", "user-3"],
|
||||
"user-1": {
|
||||
keys: {
|
||||
organizationKeys: {
|
||||
encrypted: {
|
||||
"org-id-1": {
|
||||
type: "organization",
|
||||
key: "org-key-1",
|
||||
},
|
||||
"org-id-2": {
|
||||
type: "provider",
|
||||
key: "org-key-2",
|
||||
providerId: "provider-id-2",
|
||||
},
|
||||
},
|
||||
},
|
||||
otherStuff: "overStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
},
|
||||
"user-2": {
|
||||
keys: {
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function rollbackJSON() {
|
||||
return {
|
||||
"user_user-1_crypto_organizationKeys": {
|
||||
"org-id-1": {
|
||||
type: "organization",
|
||||
key: "org-key-1",
|
||||
},
|
||||
"org-id-2": {
|
||||
type: "provider",
|
||||
key: "org-key-2",
|
||||
providerId: "provider-id-2",
|
||||
},
|
||||
},
|
||||
"user_user-2_crypto_organizationKeys": null as any,
|
||||
global: {
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
authenticatedAccounts: ["user-1", "user-2", "user-3"],
|
||||
"user-1": {
|
||||
keys: {
|
||||
otherStuff: "overStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
},
|
||||
"user-2": {
|
||||
keys: {
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("OrganizationKeysMigrator", () => {
|
||||
let helper: MockProxy<MigrationHelper>;
|
||||
let sut: OrganizationKeyMigrator;
|
||||
const keyDefinitionLike = {
|
||||
key: "organizationKeys",
|
||||
stateDefinition: {
|
||||
name: "crypto",
|
||||
},
|
||||
};
|
||||
|
||||
describe("migrate", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(exampleJSON(), 10);
|
||||
sut = new OrganizationKeyMigrator(10, 11);
|
||||
});
|
||||
|
||||
it("should remove organizationKeys from all accounts", async () => {
|
||||
await sut.migrate(helper);
|
||||
expect(helper.set).toHaveBeenCalledTimes(1);
|
||||
expect(helper.set).toHaveBeenCalledWith("user-1", {
|
||||
keys: {
|
||||
otherStuff: "overStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
});
|
||||
});
|
||||
|
||||
it("should set organizationKeys value for each account", async () => {
|
||||
await sut.migrate(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledTimes(1);
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("user-1", keyDefinitionLike, {
|
||||
"org-id-1": {
|
||||
type: "organization",
|
||||
key: "org-key-1",
|
||||
},
|
||||
"org-id-2": {
|
||||
type: "provider",
|
||||
key: "org-key-2",
|
||||
providerId: "provider-id-2",
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("rollback", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(rollbackJSON(), 11);
|
||||
sut = new OrganizationKeyMigrator(10, 11);
|
||||
});
|
||||
|
||||
it.each(["user-1", "user-2", "user-3"])("should null out new values %s", async (userId) => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(userId, keyDefinitionLike, null);
|
||||
});
|
||||
|
||||
it("should add explicit value back to accounts", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.set).toHaveBeenCalledTimes(1);
|
||||
expect(helper.set).toHaveBeenCalledWith("user-1", {
|
||||
keys: {
|
||||
organizationKeys: {
|
||||
encrypted: {
|
||||
"org-id-1": {
|
||||
type: "organization",
|
||||
key: "org-key-1",
|
||||
},
|
||||
"org-id-2": {
|
||||
type: "provider",
|
||||
key: "org-key-2",
|
||||
providerId: "provider-id-2",
|
||||
},
|
||||
},
|
||||
},
|
||||
otherStuff: "overStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
});
|
||||
});
|
||||
|
||||
it("should not try to restore values to missing accounts", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.set).not.toHaveBeenCalledWith("user-3", any());
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
|
||||
import { Migrator } from "../migrator";
|
||||
|
||||
type OrgKeyDataType = {
|
||||
type: "organization" | "provider";
|
||||
key: string;
|
||||
providerId?: string;
|
||||
};
|
||||
|
||||
type ExpectedAccountType = {
|
||||
keys?: {
|
||||
organizationKeys?: {
|
||||
encrypted?: Record<string, OrgKeyDataType>;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
const USER_ENCRYPTED_ORGANIZATION_KEYS: KeyDefinitionLike = {
|
||||
key: "organizationKeys",
|
||||
stateDefinition: {
|
||||
name: "crypto",
|
||||
},
|
||||
};
|
||||
|
||||
export class OrganizationKeyMigrator extends Migrator<10, 11> {
|
||||
async migrate(helper: MigrationHelper): Promise<void> {
|
||||
const accounts = await helper.getAccounts<ExpectedAccountType>();
|
||||
async function migrateAccount(userId: string, account: ExpectedAccountType): Promise<void> {
|
||||
const value = account?.keys?.organizationKeys?.encrypted;
|
||||
if (value != null) {
|
||||
await helper.setToUser(userId, USER_ENCRYPTED_ORGANIZATION_KEYS, value);
|
||||
delete account.keys.organizationKeys;
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all([...accounts.map(({ userId, account }) => migrateAccount(userId, account))]);
|
||||
}
|
||||
async rollback(helper: MigrationHelper): Promise<void> {
|
||||
const accounts = await helper.getAccounts<ExpectedAccountType>();
|
||||
async function rollbackAccount(userId: string, account: ExpectedAccountType): Promise<void> {
|
||||
const value = await helper.getFromUser<Record<string, OrgKeyDataType>>(
|
||||
userId,
|
||||
USER_ENCRYPTED_ORGANIZATION_KEYS,
|
||||
);
|
||||
if (account && value) {
|
||||
account.keys = Object.assign(account.keys ?? {}, {
|
||||
organizationKeys: {
|
||||
encrypted: value,
|
||||
},
|
||||
});
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
await helper.setToUser(userId, USER_ENCRYPTED_ORGANIZATION_KEYS, null);
|
||||
}
|
||||
|
||||
await Promise.all([...accounts.map(({ userId, account }) => rollbackAccount(userId, account))]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
import { runMigrator } from "../migration-helper.spec";
|
||||
|
||||
import { MoveEnvironmentStateToProviders } from "./12-move-environment-state-to-providers";
|
||||
|
||||
describe("MoveEnvironmentStateToProviders", () => {
|
||||
const migrator = new MoveEnvironmentStateToProviders(11, 12);
|
||||
|
||||
it("can migrate all data", async () => {
|
||||
const output = await runMigrator(migrator, {
|
||||
authenticatedAccounts: ["user1", "user2"] as const,
|
||||
global: {
|
||||
region: "US",
|
||||
environmentUrls: {
|
||||
base: "example.com",
|
||||
},
|
||||
extra: "data",
|
||||
},
|
||||
user1: {
|
||||
extra: "data",
|
||||
settings: {
|
||||
extra: "data",
|
||||
region: "US",
|
||||
environmentUrls: {
|
||||
base: "example.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
user2: {
|
||||
extra: "data",
|
||||
settings: {
|
||||
region: "EU",
|
||||
environmentUrls: {
|
||||
base: "other.example.com",
|
||||
},
|
||||
extra: "data",
|
||||
},
|
||||
},
|
||||
extra: "data",
|
||||
});
|
||||
|
||||
expect(output).toEqual({
|
||||
authenticatedAccounts: ["user1", "user2"],
|
||||
global: {
|
||||
extra: "data",
|
||||
},
|
||||
global_environment_region: "US",
|
||||
global_environment_urls: {
|
||||
base: "example.com",
|
||||
},
|
||||
user1: {
|
||||
extra: "data",
|
||||
settings: {
|
||||
extra: "data",
|
||||
},
|
||||
},
|
||||
user2: {
|
||||
extra: "data",
|
||||
settings: {
|
||||
extra: "data",
|
||||
},
|
||||
},
|
||||
extra: "data",
|
||||
user_user1_environment_region: "US",
|
||||
user_user2_environment_region: "EU",
|
||||
user_user1_environment_urls: {
|
||||
base: "example.com",
|
||||
},
|
||||
user_user2_environment_urls: {
|
||||
base: "other.example.com",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("handles missing parts", async () => {
|
||||
const output = await runMigrator(migrator, {
|
||||
authenticatedAccounts: ["user1", "user2"],
|
||||
global: {
|
||||
extra: "data",
|
||||
},
|
||||
user1: {
|
||||
extra: "data",
|
||||
settings: {
|
||||
extra: "data",
|
||||
},
|
||||
},
|
||||
user2: null,
|
||||
});
|
||||
|
||||
expect(output).toEqual({
|
||||
authenticatedAccounts: ["user1", "user2"],
|
||||
global: {
|
||||
extra: "data",
|
||||
},
|
||||
user1: {
|
||||
extra: "data",
|
||||
settings: {
|
||||
extra: "data",
|
||||
},
|
||||
},
|
||||
user2: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("can migrate only global data", async () => {
|
||||
const output = await runMigrator(migrator, {
|
||||
authenticatedAccounts: [] as const,
|
||||
global: {
|
||||
region: "Self-Hosted",
|
||||
},
|
||||
});
|
||||
|
||||
expect(output).toEqual({
|
||||
authenticatedAccounts: [],
|
||||
global_environment_region: "Self-Hosted",
|
||||
global: {},
|
||||
});
|
||||
});
|
||||
|
||||
it("can migrate only user state", async () => {
|
||||
const output = await runMigrator(migrator, {
|
||||
authenticatedAccounts: ["user1"] as const,
|
||||
global: null,
|
||||
user1: {
|
||||
settings: {
|
||||
region: "Self-Hosted",
|
||||
environmentUrls: {
|
||||
base: "some-base-url",
|
||||
api: "some-api-url",
|
||||
identity: "some-identity-url",
|
||||
icons: "some-icons-url",
|
||||
notifications: "some-notifications-url",
|
||||
events: "some-events-url",
|
||||
webVault: "some-webVault-url",
|
||||
keyConnector: "some-keyConnector-url",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(output).toEqual({
|
||||
authenticatedAccounts: ["user1"] as const,
|
||||
global: null,
|
||||
user1: { settings: {} },
|
||||
user_user1_environment_region: "Self-Hosted",
|
||||
user_user1_environment_urls: {
|
||||
base: "some-base-url",
|
||||
api: "some-api-url",
|
||||
identity: "some-identity-url",
|
||||
icons: "some-icons-url",
|
||||
notifications: "some-notifications-url",
|
||||
events: "some-events-url",
|
||||
webVault: "some-webVault-url",
|
||||
keyConnector: "some-keyConnector-url",
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,132 @@
|
||||
import { KeyDefinitionLike, MigrationHelper, StateDefinitionLike } from "../migration-helper";
|
||||
import { Migrator } from "../migrator";
|
||||
|
||||
type EnvironmentUrls = Record<string, string>;
|
||||
|
||||
type ExpectedAccountType = {
|
||||
settings?: { region?: string; environmentUrls?: EnvironmentUrls };
|
||||
};
|
||||
|
||||
type ExpectedGlobalType = { region?: string; environmentUrls?: EnvironmentUrls };
|
||||
|
||||
const ENVIRONMENT_STATE: StateDefinitionLike = { name: "environment" };
|
||||
|
||||
const REGION_KEY: KeyDefinitionLike = { key: "region", stateDefinition: ENVIRONMENT_STATE };
|
||||
const URLS_KEY: KeyDefinitionLike = { key: "urls", stateDefinition: ENVIRONMENT_STATE };
|
||||
|
||||
export class MoveEnvironmentStateToProviders extends Migrator<11, 12> {
|
||||
async migrate(helper: MigrationHelper): Promise<void> {
|
||||
const legacyGlobal = await helper.get<ExpectedGlobalType>("global");
|
||||
|
||||
// Move global data
|
||||
if (legacyGlobal?.region != null) {
|
||||
await helper.setToGlobal(REGION_KEY, legacyGlobal.region);
|
||||
}
|
||||
|
||||
if (legacyGlobal?.environmentUrls != null) {
|
||||
await helper.setToGlobal(URLS_KEY, legacyGlobal.environmentUrls);
|
||||
}
|
||||
|
||||
const legacyAccounts = await helper.getAccounts<ExpectedAccountType>();
|
||||
|
||||
await Promise.all(
|
||||
legacyAccounts.map(async ({ userId, account }) => {
|
||||
// Move account data
|
||||
if (account?.settings?.region != null) {
|
||||
await helper.setToUser(userId, REGION_KEY, account.settings.region);
|
||||
}
|
||||
|
||||
if (account?.settings?.environmentUrls != null) {
|
||||
await helper.setToUser(userId, URLS_KEY, account.settings.environmentUrls);
|
||||
}
|
||||
|
||||
// Delete old account data
|
||||
delete account?.settings?.region;
|
||||
delete account?.settings?.environmentUrls;
|
||||
await helper.set(userId, account);
|
||||
}),
|
||||
);
|
||||
|
||||
// Delete legacy global data
|
||||
delete legacyGlobal?.region;
|
||||
delete legacyGlobal?.environmentUrls;
|
||||
await helper.set("global", legacyGlobal);
|
||||
}
|
||||
|
||||
async rollback(helper: MigrationHelper): Promise<void> {
|
||||
let legacyGlobal = await helper.get<ExpectedGlobalType>("global");
|
||||
|
||||
let updatedLegacyGlobal = false;
|
||||
|
||||
const globalRegion = await helper.getFromGlobal<string>(REGION_KEY);
|
||||
|
||||
if (globalRegion) {
|
||||
if (!legacyGlobal) {
|
||||
legacyGlobal = {};
|
||||
}
|
||||
|
||||
updatedLegacyGlobal = true;
|
||||
legacyGlobal.region = globalRegion;
|
||||
await helper.setToGlobal(REGION_KEY, null);
|
||||
}
|
||||
|
||||
const globalUrls = await helper.getFromGlobal<EnvironmentUrls>(URLS_KEY);
|
||||
|
||||
if (globalUrls) {
|
||||
if (!legacyGlobal) {
|
||||
legacyGlobal = {};
|
||||
}
|
||||
|
||||
updatedLegacyGlobal = true;
|
||||
legacyGlobal.environmentUrls = globalUrls;
|
||||
await helper.setToGlobal(URLS_KEY, null);
|
||||
}
|
||||
|
||||
if (updatedLegacyGlobal) {
|
||||
await helper.set("global", legacyGlobal);
|
||||
}
|
||||
|
||||
async function rollbackUser(userId: string, account: ExpectedAccountType) {
|
||||
let updatedAccount = false;
|
||||
const userRegion = await helper.getFromUser<string>(userId, REGION_KEY);
|
||||
|
||||
if (userRegion) {
|
||||
if (!account) {
|
||||
account = {};
|
||||
}
|
||||
|
||||
if (!account.settings) {
|
||||
account.settings = {};
|
||||
}
|
||||
|
||||
updatedAccount = true;
|
||||
account.settings.region = userRegion;
|
||||
await helper.setToUser(userId, REGION_KEY, null);
|
||||
}
|
||||
|
||||
const userUrls = await helper.getFromUser<EnvironmentUrls>(userId, URLS_KEY);
|
||||
|
||||
if (userUrls) {
|
||||
if (!account) {
|
||||
account = {};
|
||||
}
|
||||
|
||||
if (!account.settings) {
|
||||
account.settings = {};
|
||||
}
|
||||
|
||||
updatedAccount = true;
|
||||
account.settings.environmentUrls = userUrls;
|
||||
await helper.setToUser(userId, URLS_KEY, null);
|
||||
}
|
||||
|
||||
if (updatedAccount) {
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
}
|
||||
|
||||
const accounts = await helper.getAccounts<ExpectedAccountType>();
|
||||
|
||||
await Promise.all(accounts.map(({ userId, account }) => rollbackUser(userId, account)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
import { MockProxy, any } from "jest-mock-extended";
|
||||
|
||||
import { MigrationHelper } from "../migration-helper";
|
||||
import { mockMigrationHelper } from "../migration-helper.spec";
|
||||
|
||||
import { ProviderKeyMigrator } from "./13-move-provider-keys-to-state-providers";
|
||||
|
||||
function exampleJSON() {
|
||||
return {
|
||||
global: {
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
authenticatedAccounts: ["user-1", "user-2", "user-3"],
|
||||
"user-1": {
|
||||
keys: {
|
||||
providerKeys: {
|
||||
encrypted: {
|
||||
"provider-id-1": "provider-key-1",
|
||||
"provider-id-2": "provider-key-2",
|
||||
},
|
||||
},
|
||||
otherStuff: "overStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
},
|
||||
"user-2": {
|
||||
keys: {
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function rollbackJSON() {
|
||||
return {
|
||||
"user_user-1_crypto_providerKeys": {
|
||||
"provider-id-1": "provider-key-1",
|
||||
"provider-id-2": "provider-key-2",
|
||||
},
|
||||
"user_user-2_crypto_providerKeys": null as any,
|
||||
global: {
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
authenticatedAccounts: ["user-1", "user-2", "user-3"],
|
||||
"user-1": {
|
||||
keys: {
|
||||
otherStuff: "overStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
},
|
||||
"user-2": {
|
||||
keys: {
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("ProviderKeysMigrator", () => {
|
||||
let helper: MockProxy<MigrationHelper>;
|
||||
let sut: ProviderKeyMigrator;
|
||||
const keyDefinitionLike = {
|
||||
key: "providerKeys",
|
||||
stateDefinition: {
|
||||
name: "crypto",
|
||||
},
|
||||
};
|
||||
|
||||
describe("migrate", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(exampleJSON(), 12);
|
||||
sut = new ProviderKeyMigrator(12, 13);
|
||||
});
|
||||
|
||||
it("should remove providerKeys from all accounts", async () => {
|
||||
await sut.migrate(helper);
|
||||
expect(helper.set).toHaveBeenCalledTimes(1);
|
||||
expect(helper.set).toHaveBeenCalledWith("user-1", {
|
||||
keys: {
|
||||
otherStuff: "overStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
});
|
||||
});
|
||||
|
||||
it("should set providerKeys value for each account", async () => {
|
||||
await sut.migrate(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledTimes(1);
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("user-1", keyDefinitionLike, {
|
||||
"provider-id-1": "provider-key-1",
|
||||
"provider-id-2": "provider-key-2",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("rollback", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(rollbackJSON(), 13);
|
||||
sut = new ProviderKeyMigrator(12, 13);
|
||||
});
|
||||
|
||||
it.each(["user-1", "user-2", "user-3"])("should null out new values %s", async (userId) => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(userId, keyDefinitionLike, null);
|
||||
});
|
||||
|
||||
it("should add explicit value back to accounts", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.set).toHaveBeenCalledTimes(1);
|
||||
expect(helper.set).toHaveBeenCalledWith("user-1", {
|
||||
keys: {
|
||||
providerKeys: {
|
||||
encrypted: {
|
||||
"provider-id-1": "provider-key-1",
|
||||
"provider-id-2": "provider-key-2",
|
||||
},
|
||||
},
|
||||
otherStuff: "overStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
});
|
||||
});
|
||||
|
||||
it("should not try to restore values to missing accounts", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.set).not.toHaveBeenCalledWith("user-3", any());
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
|
||||
import { Migrator } from "../migrator";
|
||||
|
||||
type ExpectedAccountType = {
|
||||
keys?: {
|
||||
providerKeys?: {
|
||||
encrypted?: Record<string, string>; // Record<ProviderId, EncryptedString> where EncryptedString is the ProviderKey encrypted by the UserKey.
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
const USER_ENCRYPTED_PROVIDER_KEYS: KeyDefinitionLike = {
|
||||
key: "providerKeys",
|
||||
stateDefinition: {
|
||||
name: "crypto",
|
||||
},
|
||||
};
|
||||
|
||||
export class ProviderKeyMigrator extends Migrator<12, 13> {
|
||||
async migrate(helper: MigrationHelper): Promise<void> {
|
||||
const accounts = await helper.getAccounts<ExpectedAccountType>();
|
||||
async function migrateAccount(userId: string, account: ExpectedAccountType): Promise<void> {
|
||||
const value = account?.keys?.providerKeys?.encrypted;
|
||||
if (value != null) {
|
||||
await helper.setToUser(userId, USER_ENCRYPTED_PROVIDER_KEYS, value);
|
||||
delete account.keys.providerKeys;
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all([...accounts.map(({ userId, account }) => migrateAccount(userId, account))]);
|
||||
}
|
||||
async rollback(helper: MigrationHelper): Promise<void> {
|
||||
const accounts = await helper.getAccounts<ExpectedAccountType>();
|
||||
async function rollbackAccount(userId: string, account: ExpectedAccountType): Promise<void> {
|
||||
const value = await helper.getFromUser<Record<string, string>>(
|
||||
userId,
|
||||
USER_ENCRYPTED_PROVIDER_KEYS,
|
||||
);
|
||||
if (account && value) {
|
||||
account.keys = Object.assign(account.keys ?? {}, {
|
||||
providerKeys: {
|
||||
encrypted: value,
|
||||
},
|
||||
});
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
await helper.setToUser(userId, USER_ENCRYPTED_PROVIDER_KEYS, null);
|
||||
}
|
||||
|
||||
await Promise.all([...accounts.map(({ userId, account }) => rollbackAccount(userId, account))]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
import { MockProxy, any } from "jest-mock-extended";
|
||||
|
||||
import { MigrationHelper } from "../migration-helper";
|
||||
import { mockMigrationHelper } from "../migration-helper.spec";
|
||||
|
||||
import {
|
||||
MoveBiometricClientKeyHalfToStateProviders,
|
||||
CLIENT_KEY_HALF,
|
||||
} from "./14-move-biometric-client-key-half-state-to-providers";
|
||||
|
||||
function exampleJSON() {
|
||||
return {
|
||||
global: {
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
authenticatedAccounts: ["user-1", "user-2", "user-3"],
|
||||
"user-1": {
|
||||
keys: {
|
||||
biometricEncryptionClientKeyHalf: "user1-key-half",
|
||||
otherStuff: "overStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
},
|
||||
"user-2": {
|
||||
keys: {
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function rollbackJSON() {
|
||||
return {
|
||||
"user_user-1_biometricSettings_clientKeyHalf": "user1-key-half",
|
||||
global: {
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
authenticatedAccounts: ["user-1", "user-2", "user-3"],
|
||||
"user-1": {
|
||||
keys: {
|
||||
otherStuff: "overStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
},
|
||||
"user-2": {
|
||||
keys: {
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("DesktopBiometricState migrator", () => {
|
||||
let helper: MockProxy<MigrationHelper>;
|
||||
let sut: MoveBiometricClientKeyHalfToStateProviders;
|
||||
|
||||
describe("migrate", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(exampleJSON(), 13);
|
||||
sut = new MoveBiometricClientKeyHalfToStateProviders(13, 14);
|
||||
});
|
||||
|
||||
it("should remove biometricEncryptionClientKeyHalf from all accounts", async () => {
|
||||
await sut.migrate(helper);
|
||||
expect(helper.set).toHaveBeenCalledTimes(1);
|
||||
expect(helper.set).toHaveBeenCalledWith("user-1", {
|
||||
keys: {
|
||||
otherStuff: "overStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
});
|
||||
});
|
||||
|
||||
it("should set biometricEncryptionClientKeyHalf value for account that have it", async () => {
|
||||
await sut.migrate(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("user-1", CLIENT_KEY_HALF, "user1-key-half");
|
||||
});
|
||||
|
||||
it("should not call extra setToUser", async () => {
|
||||
await sut.migrate(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("rollback", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(rollbackJSON(), 14);
|
||||
sut = new MoveBiometricClientKeyHalfToStateProviders(13, 14);
|
||||
});
|
||||
|
||||
it("should null out new values", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("user-1", CLIENT_KEY_HALF, null);
|
||||
});
|
||||
|
||||
it("should add explicit value back to accounts", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.set).toHaveBeenCalledTimes(1);
|
||||
expect(helper.set).toHaveBeenCalledWith("user-1", {
|
||||
keys: {
|
||||
biometricEncryptionClientKeyHalf: "user1-key-half",
|
||||
otherStuff: "overStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
});
|
||||
});
|
||||
|
||||
it.each(["user-2", "user-3"])(
|
||||
"should not try to restore values to missing accounts",
|
||||
async (userId) => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.set).not.toHaveBeenCalledWith(userId, any());
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
|
||||
import { Migrator } from "../migrator";
|
||||
|
||||
type ExpectedAccountType = {
|
||||
settings?: {
|
||||
disableAutoBiometricsPrompt?: boolean;
|
||||
biometricUnlock?: boolean;
|
||||
dismissedBiometricRequirePasswordOnStartCallout?: boolean;
|
||||
};
|
||||
keys?: { biometricEncryptionClientKeyHalf?: string };
|
||||
};
|
||||
|
||||
// Biometric text, no auto prompt text, fingerprint validated, and prompt cancelled are refreshed on every app start, so we don't need to migrate them
|
||||
export const CLIENT_KEY_HALF: KeyDefinitionLike = {
|
||||
key: "clientKeyHalf",
|
||||
stateDefinition: { name: "biometricSettings" },
|
||||
};
|
||||
|
||||
export class MoveBiometricClientKeyHalfToStateProviders extends Migrator<13, 14> {
|
||||
async migrate(helper: MigrationHelper): Promise<void> {
|
||||
const legacyAccounts = await helper.getAccounts<ExpectedAccountType>();
|
||||
|
||||
await Promise.all(
|
||||
legacyAccounts.map(async ({ userId, account }) => {
|
||||
// Move account data
|
||||
if (account?.keys?.biometricEncryptionClientKeyHalf != null) {
|
||||
await helper.setToUser(
|
||||
userId,
|
||||
CLIENT_KEY_HALF,
|
||||
account.keys.biometricEncryptionClientKeyHalf,
|
||||
);
|
||||
|
||||
// Delete old account data
|
||||
delete account?.keys?.biometricEncryptionClientKeyHalf;
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async rollback(helper: MigrationHelper): Promise<void> {
|
||||
async function rollbackUser(userId: string, account: ExpectedAccountType) {
|
||||
let updatedAccount = false;
|
||||
|
||||
const userKeyHalf = await helper.getFromUser<string>(userId, CLIENT_KEY_HALF);
|
||||
|
||||
if (userKeyHalf) {
|
||||
account ??= {};
|
||||
account.keys ??= {};
|
||||
|
||||
updatedAccount = true;
|
||||
account.keys.biometricEncryptionClientKeyHalf = userKeyHalf;
|
||||
await helper.setToUser(userId, CLIENT_KEY_HALF, null);
|
||||
}
|
||||
|
||||
if (updatedAccount) {
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
}
|
||||
|
||||
const accounts = await helper.getAccounts<ExpectedAccountType>();
|
||||
|
||||
await Promise.all(accounts.map(({ userId, account }) => rollbackUser(userId, account)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
import { MockProxy, any } from "jest-mock-extended";
|
||||
|
||||
import { MigrationHelper } from "../migration-helper";
|
||||
import { mockMigrationHelper } from "../migration-helper.spec";
|
||||
|
||||
import { FolderMigrator } from "./15-move-folder-state-to-state-provider";
|
||||
|
||||
function exampleJSON() {
|
||||
return {
|
||||
global: {
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
authenticatedAccounts: ["user-1", "user-2"],
|
||||
"user-1": {
|
||||
data: {
|
||||
folders: {
|
||||
encrypted: {
|
||||
"folder-id-1": {
|
||||
id: "folder-id-1",
|
||||
name: "folder-name-1",
|
||||
revisionDate: "folder-revision-date-1",
|
||||
},
|
||||
"folder-id-2": {
|
||||
id: "folder-id-2",
|
||||
name: "folder-name-2",
|
||||
revisionDate: "folder-revision-date-2",
|
||||
},
|
||||
},
|
||||
},
|
||||
otherStuff: "overStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
},
|
||||
"user-2": {
|
||||
data: {
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function rollbackJSON() {
|
||||
return {
|
||||
"user_user-1_folder_folders": {
|
||||
"folder-id-1": {
|
||||
id: "folder-id-1",
|
||||
name: "folder-name-1",
|
||||
revisionDate: "folder-revision-date-1",
|
||||
},
|
||||
"folder-id-2": {
|
||||
id: "folder-id-2",
|
||||
name: "folder-name-2",
|
||||
revisionDate: "folder-revision-date-2",
|
||||
},
|
||||
},
|
||||
"user_user-2_folder_folders": null as any,
|
||||
global: {
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
authenticatedAccounts: ["user-1", "user-2"],
|
||||
"user-1": {
|
||||
data: {
|
||||
otherStuff: "overStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
},
|
||||
"user-2": {
|
||||
data: {
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("FolderMigrator", () => {
|
||||
let helper: MockProxy<MigrationHelper>;
|
||||
let sut: FolderMigrator;
|
||||
const keyDefinitionLike = {
|
||||
key: "folders",
|
||||
stateDefinition: {
|
||||
name: "folder",
|
||||
},
|
||||
};
|
||||
|
||||
describe("migrate", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(exampleJSON(), 14);
|
||||
sut = new FolderMigrator(14, 15);
|
||||
});
|
||||
|
||||
it("should remove folders from all accounts", async () => {
|
||||
await sut.migrate(helper);
|
||||
expect(helper.set).toHaveBeenCalledWith("user-1", {
|
||||
data: {
|
||||
otherStuff: "overStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
});
|
||||
});
|
||||
|
||||
it("should set folders value for each account", async () => {
|
||||
await sut.migrate(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("user-1", keyDefinitionLike, {
|
||||
"folder-id-1": {
|
||||
id: "folder-id-1",
|
||||
name: "folder-name-1",
|
||||
revisionDate: "folder-revision-date-1",
|
||||
},
|
||||
"folder-id-2": {
|
||||
id: "folder-id-2",
|
||||
name: "folder-name-2",
|
||||
revisionDate: "folder-revision-date-2",
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("rollback", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(rollbackJSON(), 15);
|
||||
sut = new FolderMigrator(14, 15);
|
||||
});
|
||||
|
||||
it.each(["user-1", "user-2"])("should null out new values", async (userId) => {
|
||||
await sut.rollback(helper);
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(userId, keyDefinitionLike, null);
|
||||
});
|
||||
|
||||
it("should add explicit value back to accounts", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.set).toHaveBeenCalledWith("user-1", {
|
||||
data: {
|
||||
folders: {
|
||||
encrypted: {
|
||||
"folder-id-1": {
|
||||
id: "folder-id-1",
|
||||
name: "folder-name-1",
|
||||
revisionDate: "folder-revision-date-1",
|
||||
},
|
||||
"folder-id-2": {
|
||||
id: "folder-id-2",
|
||||
name: "folder-name-2",
|
||||
revisionDate: "folder-revision-date-2",
|
||||
},
|
||||
},
|
||||
},
|
||||
otherStuff: "overStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
});
|
||||
});
|
||||
|
||||
it("should not try to restore values to missing accounts", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.set).not.toHaveBeenCalledWith("user-3", any());
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
|
||||
import { Migrator } from "../migrator";
|
||||
|
||||
type FolderDataType = {
|
||||
id: string;
|
||||
name: string;
|
||||
revisionDate: string;
|
||||
};
|
||||
|
||||
type ExpectedAccountType = {
|
||||
data?: {
|
||||
folders?: {
|
||||
encrypted?: Record<string, FolderDataType>;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
const USER_ENCRYPTED_FOLDERS: KeyDefinitionLike = {
|
||||
key: "folders",
|
||||
stateDefinition: {
|
||||
name: "folder",
|
||||
},
|
||||
};
|
||||
|
||||
export class FolderMigrator extends Migrator<14, 15> {
|
||||
async migrate(helper: MigrationHelper): Promise<void> {
|
||||
const accounts = await helper.getAccounts<ExpectedAccountType>();
|
||||
async function migrateAccount(userId: string, account: ExpectedAccountType): Promise<void> {
|
||||
const value = account?.data?.folders?.encrypted;
|
||||
if (value != null) {
|
||||
await helper.setToUser(userId, USER_ENCRYPTED_FOLDERS, value);
|
||||
delete account.data.folders;
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all([...accounts.map(({ userId, account }) => migrateAccount(userId, account))]);
|
||||
}
|
||||
|
||||
async rollback(helper: MigrationHelper): Promise<void> {
|
||||
const accounts = await helper.getAccounts<ExpectedAccountType>();
|
||||
async function rollbackAccount(userId: string, account: ExpectedAccountType): Promise<void> {
|
||||
const value = await helper.getFromUser(userId, USER_ENCRYPTED_FOLDERS);
|
||||
if (account) {
|
||||
account.data = Object.assign(account.data ?? {}, {
|
||||
folders: {
|
||||
encrypted: value,
|
||||
},
|
||||
});
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
await helper.setToUser(userId, USER_ENCRYPTED_FOLDERS, null);
|
||||
}
|
||||
|
||||
await Promise.all([...accounts.map(({ userId, account }) => rollbackAccount(userId, account))]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import { any, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { MigrationHelper } from "../migration-helper";
|
||||
import { mockMigrationHelper } from "../migration-helper.spec";
|
||||
|
||||
import { LastSyncMigrator } from "./16-move-last-sync-to-state-provider";
|
||||
|
||||
function exampleJSON() {
|
||||
return {
|
||||
global: {
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
authenticatedAccounts: ["user-1", "user-2"],
|
||||
"user-1": {
|
||||
profile: {
|
||||
lastSync: "2024-01-24T00:00:00.000Z",
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function rollbackJSON() {
|
||||
return {
|
||||
"user_user-1_sync_lastSync": "2024-01-24T00:00:00.000Z",
|
||||
"user_user-2_sync_lastSync": null as any,
|
||||
global: {
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
authenticatedAccounts: ["user-1", "user-2"],
|
||||
"user-1": {
|
||||
profile: {
|
||||
lastSync: "2024-01-24T00:00:00.000Z",
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("LastSyncMigrator", () => {
|
||||
let helper: MockProxy<MigrationHelper>;
|
||||
let sut: LastSyncMigrator;
|
||||
|
||||
const keyDefinitionLike = {
|
||||
key: "lastSync",
|
||||
stateDefinition: {
|
||||
name: "sync",
|
||||
},
|
||||
};
|
||||
|
||||
describe("migrate", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(exampleJSON(), 15);
|
||||
sut = new LastSyncMigrator(15, 16);
|
||||
});
|
||||
|
||||
it("should remove lastSync from all accounts", async () => {
|
||||
await sut.migrate(helper);
|
||||
expect(helper.set).toHaveBeenCalledWith("user-1", {
|
||||
profile: {
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
});
|
||||
});
|
||||
|
||||
it("should set lastSync provider value for each account", async () => {
|
||||
await sut.migrate(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(
|
||||
"user-1",
|
||||
keyDefinitionLike,
|
||||
"2024-01-24T00:00:00.000Z",
|
||||
);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("user-2", keyDefinitionLike, null);
|
||||
});
|
||||
});
|
||||
|
||||
describe("rollback", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(rollbackJSON(), 16);
|
||||
sut = new LastSyncMigrator(15, 16);
|
||||
});
|
||||
|
||||
it.each(["user-1", "user-2"])("should null out new values", async (userId) => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(userId, keyDefinitionLike, null);
|
||||
});
|
||||
|
||||
it("should add lastSync back to accounts", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.set).toHaveBeenCalledWith("user-1", {
|
||||
profile: {
|
||||
lastSync: "2024-01-24T00:00:00.000Z",
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
});
|
||||
});
|
||||
|
||||
it("should not try to restore values to missing accounts", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.set).not.toHaveBeenCalledWith("user-2", any());
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
|
||||
import { Migrator } from "../migrator";
|
||||
|
||||
type ExpectedAccountType = {
|
||||
profile?: {
|
||||
lastSync?: string;
|
||||
};
|
||||
};
|
||||
|
||||
const LAST_SYNC_KEY: KeyDefinitionLike = {
|
||||
key: "lastSync",
|
||||
stateDefinition: {
|
||||
name: "sync",
|
||||
},
|
||||
};
|
||||
|
||||
export class LastSyncMigrator extends Migrator<15, 16> {
|
||||
async migrate(helper: MigrationHelper): Promise<void> {
|
||||
const accounts = await helper.getAccounts<ExpectedAccountType>();
|
||||
async function migrateAccount(userId: string, account: ExpectedAccountType): Promise<void> {
|
||||
const value = account?.profile?.lastSync;
|
||||
await helper.setToUser(userId, LAST_SYNC_KEY, value ?? null);
|
||||
if (value != null) {
|
||||
delete account.profile.lastSync;
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all([...accounts.map(({ userId, account }) => migrateAccount(userId, account))]);
|
||||
}
|
||||
async rollback(helper: MigrationHelper): Promise<void> {
|
||||
const accounts = await helper.getAccounts<ExpectedAccountType>();
|
||||
|
||||
async function rollbackAccount(userId: string, account: ExpectedAccountType): Promise<void> {
|
||||
const value = await helper.getFromUser(userId, LAST_SYNC_KEY);
|
||||
if (account) {
|
||||
account.profile = Object.assign(account.profile ?? {}, {
|
||||
lastSync: value,
|
||||
});
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
await helper.setToUser(userId, LAST_SYNC_KEY, null);
|
||||
}
|
||||
|
||||
await Promise.all([...accounts.map(({ userId, account }) => rollbackAccount(userId, account))]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import { MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { MigrationHelper } from "../migration-helper";
|
||||
import { mockMigrationHelper } from "../migration-helper.spec";
|
||||
|
||||
import { EnablePasskeysMigrator } from "./17-move-enable-passkeys-to-state-providers";
|
||||
|
||||
function exampleJSON() {
|
||||
return {
|
||||
global: {
|
||||
enablePasskeys: true,
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
authenticatedAccounts: ["user-1", "user-2"],
|
||||
"user-1": {
|
||||
settings: {
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
},
|
||||
"user-2": {
|
||||
settings: {
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function rollbackJSON() {
|
||||
return {
|
||||
global_vaultSettings_enablePasskeys: true,
|
||||
global: {
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
authenticatedAccounts: ["user-1", "user-2"],
|
||||
"user-1": {
|
||||
settings: {
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
},
|
||||
"user-2": {
|
||||
settings: {
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("EnablePasskeysMigrator", () => {
|
||||
let helper: MockProxy<MigrationHelper>;
|
||||
let sut: EnablePasskeysMigrator;
|
||||
|
||||
describe("migrate", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(exampleJSON(), 16);
|
||||
sut = new EnablePasskeysMigrator(16, 17);
|
||||
});
|
||||
|
||||
it("should remove enablePasskeys from global", async () => {
|
||||
await sut.migrate(helper);
|
||||
expect(helper.set).toHaveBeenCalledWith("global", {
|
||||
otherStuff: "otherStuff1",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("rollback", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(rollbackJSON(), 17);
|
||||
sut = new EnablePasskeysMigrator(16, 17);
|
||||
});
|
||||
|
||||
it("should move enablePasskeys to global", async () => {
|
||||
await sut.rollback(helper);
|
||||
expect(helper.set).toHaveBeenCalledWith("global", {
|
||||
enablePasskeys: true,
|
||||
otherStuff: "otherStuff1",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
|
||||
import { Migrator } from "../migrator";
|
||||
|
||||
type ExpectedGlobalType = {
|
||||
enablePasskeys?: boolean;
|
||||
};
|
||||
|
||||
const USER_ENABLE_PASSKEYS: KeyDefinitionLike = {
|
||||
key: "enablePasskeys",
|
||||
stateDefinition: {
|
||||
name: "vaultSettings",
|
||||
},
|
||||
};
|
||||
|
||||
export class EnablePasskeysMigrator extends Migrator<16, 17> {
|
||||
async migrate(helper: MigrationHelper): Promise<void> {
|
||||
const global = await helper.get<ExpectedGlobalType>("global");
|
||||
|
||||
if (global?.enablePasskeys != null) {
|
||||
await helper.setToGlobal(USER_ENABLE_PASSKEYS, global.enablePasskeys);
|
||||
delete global?.enablePasskeys;
|
||||
await helper.set("global", global);
|
||||
}
|
||||
}
|
||||
|
||||
async rollback(helper: MigrationHelper): Promise<void> {
|
||||
let global = await helper.get<ExpectedGlobalType>("global");
|
||||
const globalEnablePasskeys = await helper.getFromGlobal<boolean>(USER_ENABLE_PASSKEYS);
|
||||
|
||||
if (globalEnablePasskeys != null) {
|
||||
global = Object.assign(global ?? {}, { enablePasskeys: globalEnablePasskeys });
|
||||
await helper.set("global", global);
|
||||
await helper.setToGlobal(USER_ENABLE_PASSKEYS, undefined);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
import { any, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { StateDefinitionLike, MigrationHelper } from "../migration-helper";
|
||||
import { mockMigrationHelper } from "../migration-helper.spec";
|
||||
|
||||
import { AutofillSettingsKeyMigrator } from "./18-move-autofill-settings-to-state-providers";
|
||||
|
||||
const AutofillOverlayVisibility = {
|
||||
Off: 0,
|
||||
OnButtonClick: 1,
|
||||
OnFieldFocus: 2,
|
||||
} as const;
|
||||
|
||||
function exampleJSON() {
|
||||
return {
|
||||
global: {
|
||||
autoFillOverlayVisibility: AutofillOverlayVisibility.OnButtonClick,
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
authenticatedAccounts: ["user-1", "user-2", "user-3"],
|
||||
"user-1": {
|
||||
settings: {
|
||||
autoFillOnPageLoadDefault: true,
|
||||
enableAutoFillOnPageLoad: true,
|
||||
dismissedAutoFillOnPageLoadCallout: true,
|
||||
disableAutoTotpCopy: false,
|
||||
activateAutoFillOnPageLoadFromPolicy: true,
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
},
|
||||
"user-2": {
|
||||
settings: {
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function rollbackJSON() {
|
||||
return {
|
||||
global_autofillSettingsLocal_inlineMenuVisibility: AutofillOverlayVisibility.OnButtonClick,
|
||||
"user_user-1_autofillSettings_autoCopyTotp": true,
|
||||
"user_user-1_autofillSettings_autofillOnPageLoad": true,
|
||||
"user_user-1_autofillSettings_autofillOnPageLoadCalloutIsDismissed": true,
|
||||
"user_user-1_autofillSettings_autofillOnPageLoadDefault": true,
|
||||
"user_user-1_autofillSettingsLocal_activateAutofillOnPageLoadFromPolicy": true,
|
||||
global: {
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
authenticatedAccounts: ["user-1", "user-2", "user-3"],
|
||||
"user-1": {
|
||||
settings: {
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
},
|
||||
"user-2": {
|
||||
settings: {
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const autofillSettingsStateDefinition: {
|
||||
stateDefinition: StateDefinitionLike;
|
||||
} = {
|
||||
stateDefinition: {
|
||||
name: "autofillSettings",
|
||||
},
|
||||
};
|
||||
|
||||
describe("AutofillSettingsKeyMigrator", () => {
|
||||
let helper: MockProxy<MigrationHelper>;
|
||||
let sut: AutofillSettingsKeyMigrator;
|
||||
|
||||
describe("migrate", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(exampleJSON(), 17);
|
||||
sut = new AutofillSettingsKeyMigrator(17, 18);
|
||||
});
|
||||
|
||||
it("should remove autofill settings from all accounts", async () => {
|
||||
await sut.migrate(helper);
|
||||
expect(helper.set).toHaveBeenCalledTimes(2);
|
||||
expect(helper.set).toHaveBeenCalledWith("global", {
|
||||
otherStuff: "otherStuff1",
|
||||
});
|
||||
expect(helper.set).toHaveBeenCalledWith("user-1", {
|
||||
settings: {
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
});
|
||||
});
|
||||
|
||||
it("should set autofill setting values for each account", async () => {
|
||||
await sut.migrate(helper);
|
||||
|
||||
expect(helper.setToGlobal).toHaveBeenCalledTimes(1);
|
||||
expect(helper.setToGlobal).toHaveBeenCalledWith(
|
||||
{
|
||||
stateDefinition: {
|
||||
name: "autofillSettingsLocal",
|
||||
},
|
||||
key: "inlineMenuVisibility",
|
||||
},
|
||||
1,
|
||||
);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledTimes(5);
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(
|
||||
"user-1",
|
||||
{ ...autofillSettingsStateDefinition, key: "autofillOnPageLoadDefault" },
|
||||
true,
|
||||
);
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(
|
||||
"user-1",
|
||||
{ ...autofillSettingsStateDefinition, key: "autofillOnPageLoad" },
|
||||
true,
|
||||
);
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(
|
||||
"user-1",
|
||||
{ ...autofillSettingsStateDefinition, key: "autofillOnPageLoadCalloutIsDismissed" },
|
||||
true,
|
||||
);
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(
|
||||
"user-1",
|
||||
{ ...autofillSettingsStateDefinition, key: "autoCopyTotp" },
|
||||
true,
|
||||
);
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(
|
||||
"user-1",
|
||||
{
|
||||
stateDefinition: {
|
||||
name: "autofillSettingsLocal",
|
||||
},
|
||||
key: "activateAutofillOnPageLoadFromPolicy",
|
||||
},
|
||||
true,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("rollback", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(rollbackJSON(), 18);
|
||||
sut = new AutofillSettingsKeyMigrator(17, 18);
|
||||
});
|
||||
|
||||
it("should null out new values for each account", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.setToGlobal).toHaveBeenCalledTimes(1);
|
||||
expect(helper.setToGlobal).toHaveBeenCalledWith(
|
||||
{
|
||||
stateDefinition: {
|
||||
name: "autofillSettingsLocal",
|
||||
},
|
||||
key: "inlineMenuVisibility",
|
||||
},
|
||||
null,
|
||||
);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledTimes(5);
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(
|
||||
"user-1",
|
||||
{ ...autofillSettingsStateDefinition, key: "autofillOnPageLoadDefault" },
|
||||
null,
|
||||
);
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(
|
||||
"user-1",
|
||||
{ ...autofillSettingsStateDefinition, key: "autofillOnPageLoad" },
|
||||
null,
|
||||
);
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(
|
||||
"user-1",
|
||||
{ ...autofillSettingsStateDefinition, key: "autofillOnPageLoadCalloutIsDismissed" },
|
||||
null,
|
||||
);
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(
|
||||
"user-1",
|
||||
{ ...autofillSettingsStateDefinition, key: "autoCopyTotp" },
|
||||
null,
|
||||
);
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(
|
||||
"user-1",
|
||||
{
|
||||
stateDefinition: {
|
||||
name: "autofillSettingsLocal",
|
||||
},
|
||||
key: "activateAutofillOnPageLoadFromPolicy",
|
||||
},
|
||||
null,
|
||||
);
|
||||
});
|
||||
|
||||
it("should add explicit value back to accounts", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.set).toHaveBeenCalledTimes(2);
|
||||
expect(helper.set).toHaveBeenCalledWith("global", {
|
||||
autoFillOverlayVisibility: 1,
|
||||
otherStuff: "otherStuff1",
|
||||
});
|
||||
expect(helper.set).toHaveBeenCalledWith("user-1", {
|
||||
settings: {
|
||||
otherStuff: "otherStuff2",
|
||||
autoFillOnPageLoadDefault: true,
|
||||
enableAutoFillOnPageLoad: true,
|
||||
dismissedAutoFillOnPageLoadCallout: true,
|
||||
disableAutoTotpCopy: false,
|
||||
activateAutoFillOnPageLoadFromPolicy: true,
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
});
|
||||
});
|
||||
|
||||
it("should not try to restore values to missing accounts", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.set).not.toHaveBeenCalledWith("user-3", any());
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,274 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { StateDefinitionLike, MigrationHelper } from "../migration-helper";
|
||||
import { Migrator } from "../migrator";
|
||||
|
||||
// FIXME: Remove when updating file. Eslint update
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const AutofillOverlayVisibility = {
|
||||
Off: 0,
|
||||
OnButtonClick: 1,
|
||||
OnFieldFocus: 2,
|
||||
} as const;
|
||||
|
||||
type InlineMenuVisibilitySetting =
|
||||
(typeof AutofillOverlayVisibility)[keyof typeof AutofillOverlayVisibility];
|
||||
|
||||
type ExpectedAccountState = {
|
||||
settings?: {
|
||||
autoFillOnPageLoadDefault?: boolean;
|
||||
enableAutoFillOnPageLoad?: boolean;
|
||||
dismissedAutoFillOnPageLoadCallout?: boolean;
|
||||
disableAutoTotpCopy?: boolean;
|
||||
activateAutoFillOnPageLoadFromPolicy?: InlineMenuVisibilitySetting;
|
||||
};
|
||||
};
|
||||
|
||||
type ExpectedGlobalState = { autoFillOverlayVisibility?: InlineMenuVisibilitySetting };
|
||||
|
||||
const autofillSettingsStateDefinition: {
|
||||
stateDefinition: StateDefinitionLike;
|
||||
} = {
|
||||
stateDefinition: {
|
||||
name: "autofillSettings",
|
||||
},
|
||||
};
|
||||
|
||||
export class AutofillSettingsKeyMigrator extends Migrator<17, 18> {
|
||||
async migrate(helper: MigrationHelper): Promise<void> {
|
||||
// global state (e.g. "autoFillOverlayVisibility -> inlineMenuVisibility")
|
||||
const globalState = await helper.get<ExpectedGlobalState>("global");
|
||||
|
||||
if (globalState?.autoFillOverlayVisibility != null) {
|
||||
await helper.setToGlobal(
|
||||
{
|
||||
stateDefinition: {
|
||||
name: "autofillSettingsLocal",
|
||||
},
|
||||
key: "inlineMenuVisibility",
|
||||
},
|
||||
globalState.autoFillOverlayVisibility,
|
||||
);
|
||||
|
||||
// delete `autoFillOverlayVisibility` from state global
|
||||
delete globalState.autoFillOverlayVisibility;
|
||||
|
||||
await helper.set<ExpectedGlobalState>("global", globalState);
|
||||
}
|
||||
|
||||
// account state (e.g. account settings -> state provider framework keys)
|
||||
const accounts = await helper.getAccounts<ExpectedAccountState>();
|
||||
|
||||
await Promise.all([...accounts.map(({ userId, account }) => migrateAccount(userId, account))]);
|
||||
|
||||
// migrate account state
|
||||
async function migrateAccount(userId: string, account: ExpectedAccountState): Promise<void> {
|
||||
let updateAccount = false;
|
||||
const accountSettings = account?.settings;
|
||||
|
||||
if (accountSettings?.autoFillOnPageLoadDefault != null) {
|
||||
await helper.setToUser(
|
||||
userId,
|
||||
{ ...autofillSettingsStateDefinition, key: "autofillOnPageLoadDefault" },
|
||||
accountSettings.autoFillOnPageLoadDefault,
|
||||
);
|
||||
delete account.settings.autoFillOnPageLoadDefault;
|
||||
updateAccount = true;
|
||||
}
|
||||
|
||||
if (accountSettings?.enableAutoFillOnPageLoad != null) {
|
||||
await helper.setToUser(
|
||||
userId,
|
||||
{ ...autofillSettingsStateDefinition, key: "autofillOnPageLoad" },
|
||||
accountSettings?.enableAutoFillOnPageLoad,
|
||||
);
|
||||
delete account.settings.enableAutoFillOnPageLoad;
|
||||
updateAccount = true;
|
||||
}
|
||||
|
||||
if (accountSettings?.dismissedAutoFillOnPageLoadCallout != null) {
|
||||
await helper.setToUser(
|
||||
userId,
|
||||
{ ...autofillSettingsStateDefinition, key: "autofillOnPageLoadCalloutIsDismissed" },
|
||||
accountSettings?.dismissedAutoFillOnPageLoadCallout,
|
||||
);
|
||||
delete account.settings.dismissedAutoFillOnPageLoadCallout;
|
||||
updateAccount = true;
|
||||
}
|
||||
|
||||
if (accountSettings?.disableAutoTotpCopy != null) {
|
||||
await helper.setToUser(
|
||||
userId,
|
||||
{ ...autofillSettingsStateDefinition, key: "autoCopyTotp" },
|
||||
// invert the value to match the new naming convention
|
||||
!accountSettings?.disableAutoTotpCopy,
|
||||
);
|
||||
delete account.settings.disableAutoTotpCopy;
|
||||
updateAccount = true;
|
||||
}
|
||||
|
||||
if (accountSettings?.activateAutoFillOnPageLoadFromPolicy != null) {
|
||||
await helper.setToUser(
|
||||
userId,
|
||||
{
|
||||
stateDefinition: {
|
||||
name: "autofillSettingsLocal",
|
||||
},
|
||||
key: "activateAutofillOnPageLoadFromPolicy",
|
||||
},
|
||||
accountSettings?.activateAutoFillOnPageLoadFromPolicy,
|
||||
);
|
||||
delete account.settings.activateAutoFillOnPageLoadFromPolicy;
|
||||
updateAccount = true;
|
||||
}
|
||||
|
||||
if (updateAccount) {
|
||||
// update the state account settings with the migrated values deleted
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async rollback(helper: MigrationHelper): Promise<void> {
|
||||
// global state (e.g. "inlineMenuVisibility -> autoFillOverlayVisibility")
|
||||
const globalState = (await helper.get<ExpectedGlobalState>("global")) || {};
|
||||
const inlineMenuVisibility: InlineMenuVisibilitySetting = await helper.getFromGlobal({
|
||||
stateDefinition: {
|
||||
name: "autofillSettingsLocal",
|
||||
},
|
||||
key: "inlineMenuVisibility",
|
||||
});
|
||||
|
||||
if (inlineMenuVisibility) {
|
||||
await helper.set<ExpectedGlobalState>("global", {
|
||||
...globalState,
|
||||
autoFillOverlayVisibility: inlineMenuVisibility,
|
||||
});
|
||||
|
||||
// remove the global state provider framework key for `inlineMenuVisibility`
|
||||
await helper.setToGlobal(
|
||||
{
|
||||
stateDefinition: {
|
||||
name: "autofillSettingsLocal",
|
||||
},
|
||||
key: "inlineMenuVisibility",
|
||||
},
|
||||
null,
|
||||
);
|
||||
}
|
||||
|
||||
// account state (e.g. state provider framework keys -> account settings)
|
||||
const accounts = await helper.getAccounts<ExpectedAccountState>();
|
||||
|
||||
await Promise.all([...accounts.map(({ userId, account }) => rollbackAccount(userId, account))]);
|
||||
|
||||
// rollback account state
|
||||
async function rollbackAccount(userId: string, account: ExpectedAccountState): Promise<void> {
|
||||
let updateAccount = false;
|
||||
let settings = account?.settings || {};
|
||||
|
||||
const autoFillOnPageLoadDefault: boolean = await helper.getFromUser(userId, {
|
||||
...autofillSettingsStateDefinition,
|
||||
key: "autofillOnPageLoadDefault",
|
||||
});
|
||||
|
||||
const enableAutoFillOnPageLoad: boolean = await helper.getFromUser(userId, {
|
||||
...autofillSettingsStateDefinition,
|
||||
key: "autofillOnPageLoad",
|
||||
});
|
||||
|
||||
const dismissedAutoFillOnPageLoadCallout: boolean = await helper.getFromUser(userId, {
|
||||
...autofillSettingsStateDefinition,
|
||||
key: "autofillOnPageLoadCalloutIsDismissed",
|
||||
});
|
||||
|
||||
const autoCopyTotp: boolean = await helper.getFromUser(userId, {
|
||||
...autofillSettingsStateDefinition,
|
||||
key: "autoCopyTotp",
|
||||
});
|
||||
|
||||
const activateAutoFillOnPageLoadFromPolicy: InlineMenuVisibilitySetting =
|
||||
await helper.getFromUser(userId, {
|
||||
stateDefinition: {
|
||||
name: "autofillSettingsLocal",
|
||||
},
|
||||
key: "activateAutofillOnPageLoadFromPolicy",
|
||||
});
|
||||
|
||||
// update new settings and remove the account state provider framework keys for the rolled back values
|
||||
if (autoFillOnPageLoadDefault != null) {
|
||||
settings = { ...settings, autoFillOnPageLoadDefault };
|
||||
|
||||
await helper.setToUser(
|
||||
userId,
|
||||
{ ...autofillSettingsStateDefinition, key: "autofillOnPageLoadDefault" },
|
||||
null,
|
||||
);
|
||||
|
||||
updateAccount = true;
|
||||
}
|
||||
|
||||
if (enableAutoFillOnPageLoad != null) {
|
||||
settings = { ...settings, enableAutoFillOnPageLoad };
|
||||
|
||||
await helper.setToUser(
|
||||
userId,
|
||||
{ ...autofillSettingsStateDefinition, key: "autofillOnPageLoad" },
|
||||
null,
|
||||
);
|
||||
|
||||
updateAccount = true;
|
||||
}
|
||||
|
||||
if (dismissedAutoFillOnPageLoadCallout != null) {
|
||||
settings = { ...settings, dismissedAutoFillOnPageLoadCallout };
|
||||
|
||||
await helper.setToUser(
|
||||
userId,
|
||||
{ ...autofillSettingsStateDefinition, key: "autofillOnPageLoadCalloutIsDismissed" },
|
||||
null,
|
||||
);
|
||||
|
||||
updateAccount = true;
|
||||
}
|
||||
|
||||
if (autoCopyTotp != null) {
|
||||
// invert the value to match the new naming convention
|
||||
settings = { ...settings, disableAutoTotpCopy: !autoCopyTotp };
|
||||
|
||||
await helper.setToUser(
|
||||
userId,
|
||||
{ ...autofillSettingsStateDefinition, key: "autoCopyTotp" },
|
||||
null,
|
||||
);
|
||||
|
||||
updateAccount = true;
|
||||
}
|
||||
|
||||
if (activateAutoFillOnPageLoadFromPolicy != null) {
|
||||
settings = { ...settings, activateAutoFillOnPageLoadFromPolicy };
|
||||
|
||||
await helper.setToUser(
|
||||
userId,
|
||||
{
|
||||
stateDefinition: {
|
||||
name: "autofillSettingsLocal",
|
||||
},
|
||||
key: "activateAutofillOnPageLoadFromPolicy",
|
||||
},
|
||||
null,
|
||||
);
|
||||
|
||||
updateAccount = true;
|
||||
}
|
||||
|
||||
if (updateAccount) {
|
||||
// commit updated settings to state
|
||||
await helper.set(userId, {
|
||||
...account,
|
||||
settings,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
import { MockProxy, any } from "jest-mock-extended";
|
||||
|
||||
import { MigrationHelper } from "../migration-helper";
|
||||
import { mockMigrationHelper } from "../migration-helper.spec";
|
||||
|
||||
import {
|
||||
REQUIRE_PASSWORD_ON_START,
|
||||
RequirePasswordOnStartMigrator,
|
||||
} from "./19-migrate-require-password-on-start";
|
||||
|
||||
function exampleJSON() {
|
||||
return {
|
||||
global: {
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
authenticatedAccounts: ["user-1", "user-2", "user-3"],
|
||||
"user-1": {
|
||||
settings: {
|
||||
requirePasswordOnStart: true,
|
||||
otherStuff: "overStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
},
|
||||
"user-2": {
|
||||
keys: {
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function rollbackJSON() {
|
||||
return {
|
||||
"user_user-1_biometricSettings_requirePasswordOnStart": true,
|
||||
global: {
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
authenticatedAccounts: ["user-1", "user-2", "user-3"],
|
||||
"user-1": {
|
||||
settings: {
|
||||
otherStuff: "overStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
},
|
||||
"user-2": {
|
||||
keys: {
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("DesktopBiometricState migrator", () => {
|
||||
let helper: MockProxy<MigrationHelper>;
|
||||
let sut: RequirePasswordOnStartMigrator;
|
||||
|
||||
describe("migrate", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(exampleJSON(), 18);
|
||||
sut = new RequirePasswordOnStartMigrator(18, 19);
|
||||
});
|
||||
|
||||
it("should remove biometricEncryptionClientKeyHalf from all accounts", async () => {
|
||||
await sut.migrate(helper);
|
||||
expect(helper.set).toHaveBeenCalledTimes(1);
|
||||
expect(helper.set).toHaveBeenCalledWith("user-1", {
|
||||
settings: {
|
||||
otherStuff: "overStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
});
|
||||
});
|
||||
|
||||
it("should set biometricEncryptionClientKeyHalf value for account that have it", async () => {
|
||||
await sut.migrate(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("user-1", REQUIRE_PASSWORD_ON_START, true);
|
||||
});
|
||||
|
||||
it("should not call extra setToUser", async () => {
|
||||
await sut.migrate(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("rollback", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(rollbackJSON(), 19);
|
||||
sut = new RequirePasswordOnStartMigrator(18, 19);
|
||||
});
|
||||
|
||||
it("should null out new values", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("user-1", REQUIRE_PASSWORD_ON_START, null);
|
||||
});
|
||||
|
||||
it("should add explicit value back to accounts", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.set).toHaveBeenCalledTimes(1);
|
||||
expect(helper.set).toHaveBeenCalledWith("user-1", {
|
||||
settings: {
|
||||
requirePasswordOnStart: true,
|
||||
otherStuff: "overStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
});
|
||||
});
|
||||
|
||||
it.each(["user-2", "user-3"])(
|
||||
"should not try to restore values to missing accounts",
|
||||
async (userId) => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.set).not.toHaveBeenCalledWith(userId, any());
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
|
||||
import { Migrator } from "../migrator";
|
||||
|
||||
type ExpectedAccountType = {
|
||||
settings?: {
|
||||
requirePasswordOnStart?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
// Biometric text, no auto prompt text, fingerprint validated, and prompt cancelled are refreshed on every app start, so we don't need to migrate them
|
||||
export const REQUIRE_PASSWORD_ON_START: KeyDefinitionLike = {
|
||||
key: "requirePasswordOnStart",
|
||||
stateDefinition: { name: "biometricSettings" },
|
||||
};
|
||||
|
||||
export class RequirePasswordOnStartMigrator extends Migrator<18, 19> {
|
||||
async migrate(helper: MigrationHelper): Promise<void> {
|
||||
const legacyAccounts = await helper.getAccounts<ExpectedAccountType>();
|
||||
|
||||
await Promise.all(
|
||||
legacyAccounts.map(async ({ userId, account }) => {
|
||||
// Move account data
|
||||
if (account?.settings?.requirePasswordOnStart != null) {
|
||||
await helper.setToUser(
|
||||
userId,
|
||||
REQUIRE_PASSWORD_ON_START,
|
||||
account.settings.requirePasswordOnStart,
|
||||
);
|
||||
|
||||
// Delete old account data
|
||||
delete account.settings.requirePasswordOnStart;
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async rollback(helper: MigrationHelper): Promise<void> {
|
||||
async function rollbackUser(userId: string, account: ExpectedAccountType) {
|
||||
const requirePassword = await helper.getFromUser<boolean>(userId, REQUIRE_PASSWORD_ON_START);
|
||||
|
||||
if (requirePassword) {
|
||||
account ??= {};
|
||||
account.settings ??= {};
|
||||
|
||||
account.settings.requirePasswordOnStart = requirePassword;
|
||||
await helper.setToUser(userId, REQUIRE_PASSWORD_ON_START, null);
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
}
|
||||
|
||||
const accounts = await helper.getAccounts<ExpectedAccountType>();
|
||||
|
||||
await Promise.all(accounts.map(({ userId, account }) => rollbackUser(userId, account)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
import { MockProxy, any } from "jest-mock-extended";
|
||||
|
||||
import { MigrationHelper } from "../migration-helper";
|
||||
import { mockMigrationHelper } from "../migration-helper.spec";
|
||||
|
||||
import { PrivateKeyMigrator } from "./20-move-private-key-to-state-providers";
|
||||
|
||||
function exampleJSON() {
|
||||
return {
|
||||
global: {
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
authenticatedAccounts: ["user-1", "user-2", "user-3"],
|
||||
"user-1": {
|
||||
keys: {
|
||||
privateKey: {
|
||||
encrypted: "user-1-encrypted-private-key",
|
||||
},
|
||||
otherStuff: "overStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
},
|
||||
"user-2": {
|
||||
keys: {
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function rollbackJSON() {
|
||||
return {
|
||||
"user_user-1_crypto_privateKey": "encrypted-private-key",
|
||||
"user_user-2_crypto_privateKey": null as any,
|
||||
global: {
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
authenticatedAccounts: ["user-1", "user-2", "user-3"],
|
||||
"user-1": {
|
||||
keys: {
|
||||
otherStuff: "overStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
},
|
||||
"user-2": {
|
||||
keys: {
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("privateKeyMigrator", () => {
|
||||
let helper: MockProxy<MigrationHelper>;
|
||||
let sut: PrivateKeyMigrator;
|
||||
const keyDefinitionLike = {
|
||||
key: "privateKey",
|
||||
stateDefinition: {
|
||||
name: "crypto",
|
||||
},
|
||||
};
|
||||
|
||||
describe("migrate", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(exampleJSON(), 19);
|
||||
sut = new PrivateKeyMigrator(19, 20);
|
||||
});
|
||||
|
||||
it("should remove privateKey from all accounts", async () => {
|
||||
await sut.migrate(helper);
|
||||
expect(helper.set).toHaveBeenCalledTimes(1);
|
||||
expect(helper.set).toHaveBeenCalledWith("user-1", {
|
||||
keys: {
|
||||
otherStuff: "overStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
});
|
||||
});
|
||||
|
||||
it("should set privateKey value for each account", async () => {
|
||||
await sut.migrate(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledTimes(1);
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(
|
||||
"user-1",
|
||||
keyDefinitionLike,
|
||||
"user-1-encrypted-private-key",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("rollback", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(rollbackJSON(), 20);
|
||||
sut = new PrivateKeyMigrator(19, 20);
|
||||
});
|
||||
|
||||
it.each(["user-1", "user-2", "user-3"])("should null out new values %s", async (userId) => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(userId, keyDefinitionLike, null);
|
||||
});
|
||||
|
||||
it("should add explicit value back to accounts", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.set).toHaveBeenCalledTimes(1);
|
||||
expect(helper.set).toHaveBeenCalledWith("user-1", {
|
||||
keys: {
|
||||
privateKey: {
|
||||
encrypted: "encrypted-private-key",
|
||||
},
|
||||
otherStuff: "overStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
});
|
||||
});
|
||||
|
||||
it("should not try to restore values to missing accounts", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.set).not.toHaveBeenCalledWith("user-3", any());
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
|
||||
import { Migrator } from "../migrator";
|
||||
|
||||
type ExpectedAccountType = {
|
||||
keys?: {
|
||||
privateKey?: {
|
||||
encrypted?: string; // EncryptedString
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
const USER_ENCRYPTED_PRIVATE_KEY: KeyDefinitionLike = {
|
||||
key: "privateKey",
|
||||
stateDefinition: {
|
||||
name: "crypto",
|
||||
},
|
||||
};
|
||||
|
||||
export class PrivateKeyMigrator extends Migrator<19, 20> {
|
||||
async migrate(helper: MigrationHelper): Promise<void> {
|
||||
const accounts = await helper.getAccounts<ExpectedAccountType>();
|
||||
async function migrateAccount(userId: string, account: ExpectedAccountType): Promise<void> {
|
||||
const value = account?.keys?.privateKey?.encrypted;
|
||||
if (value != null) {
|
||||
await helper.setToUser(userId, USER_ENCRYPTED_PRIVATE_KEY, value);
|
||||
delete account.keys.privateKey;
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all([...accounts.map(({ userId, account }) => migrateAccount(userId, account))]);
|
||||
}
|
||||
async rollback(helper: MigrationHelper): Promise<void> {
|
||||
const accounts = await helper.getAccounts<ExpectedAccountType>();
|
||||
async function rollbackAccount(userId: string, account: ExpectedAccountType): Promise<void> {
|
||||
const value = await helper.getFromUser<Record<string, string>>(
|
||||
userId,
|
||||
USER_ENCRYPTED_PRIVATE_KEY,
|
||||
);
|
||||
if (account && value) {
|
||||
account.keys = Object.assign(account.keys ?? {}, {
|
||||
privateKey: {
|
||||
encrypted: value,
|
||||
},
|
||||
});
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
await helper.setToUser(userId, USER_ENCRYPTED_PRIVATE_KEY, null);
|
||||
}
|
||||
|
||||
await Promise.all([...accounts.map(({ userId, account }) => rollbackAccount(userId, account))]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
import { MockProxy, any } from "jest-mock-extended";
|
||||
|
||||
import { MigrationHelper } from "../migration-helper";
|
||||
import { mockMigrationHelper } from "../migration-helper.spec";
|
||||
|
||||
import { CollectionMigrator } from "./21-move-collections-state-to-state-provider";
|
||||
|
||||
function exampleJSON() {
|
||||
return {
|
||||
global: {
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
authenticatedAccounts: ["user-1", "user-2"],
|
||||
"user-1": {
|
||||
data: {
|
||||
collections: {
|
||||
encrypted: {
|
||||
"877fef70-be32-439e-8678-b0d80125653d": {
|
||||
id: "877fef70-be32-439e-8678-b0d80125653d",
|
||||
organizationId: "fe1ff6ef-d2d4-49f3-9c07-b0c7013998f9",
|
||||
name: "2.MD9OMDsvYiU1CTSUxjHorw==|uFc4cZhnmQmK2LFCWbyeZg==|syk2d9JESeplxInLvP36BK5RhqS1c/i+ZQp5NR7EUA4=",
|
||||
externalId: "",
|
||||
readOnly: false,
|
||||
manage: true,
|
||||
hidePasswords: false,
|
||||
},
|
||||
"0d3fee82-3f81-434c-aed0-b0c200ee6c7a": {
|
||||
id: "0d3fee82-3f81-434c-aed0-b0c200ee6c7a",
|
||||
organizationId: "5f277723-6391-4b5c-add9-b0c200ee6967",
|
||||
name: "2.GxnXkIbBCGFr57F6lT7+Ow==|3ctMg95FKquG3l+qfv8BgvaCbYzMmuhnukCEHXhUukE=|cJRZWq05xjPBayUgx6P6gsbtNVLi8exQwo8F1SfqQQ4=",
|
||||
externalId: "",
|
||||
readOnly: false,
|
||||
manage: false,
|
||||
hidePasswords: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
},
|
||||
"user-2": {
|
||||
data: {
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function rollbackJSON() {
|
||||
return {
|
||||
"user_user-1_collection_collections": {
|
||||
"877fef70-be32-439e-8678-b0d80125653d": {
|
||||
id: "877fef70-be32-439e-8678-b0d80125653d",
|
||||
organizationId: "fe1ff6ef-d2d4-49f3-9c07-b0c7013998f9",
|
||||
name: "2.MD9OMDsvYiU1CTSUxjHorw==|uFc4cZhnmQmK2LFCWbyeZg==|syk2d9JESeplxInLvP36BK5RhqS1c/i+ZQp5NR7EUA4=",
|
||||
externalId: "",
|
||||
readOnly: false,
|
||||
manage: true,
|
||||
hidePasswords: false,
|
||||
},
|
||||
"0d3fee82-3f81-434c-aed0-b0c200ee6c7a": {
|
||||
id: "0d3fee82-3f81-434c-aed0-b0c200ee6c7a",
|
||||
organizationId: "5f277723-6391-4b5c-add9-b0c200ee6967",
|
||||
name: "2.GxnXkIbBCGFr57F6lT7+Ow==|3ctMg95FKquG3l+qfv8BgvaCbYzMmuhnukCEHXhUukE=|cJRZWq05xjPBayUgx6P6gsbtNVLi8exQwo8F1SfqQQ4=",
|
||||
externalId: "",
|
||||
readOnly: false,
|
||||
manage: false,
|
||||
hidePasswords: false,
|
||||
},
|
||||
},
|
||||
"user_user-2_collection_data": null as any,
|
||||
global: {
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
authenticatedAccounts: ["user-1", "user-2"],
|
||||
"user-1": {
|
||||
data: {
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
},
|
||||
"user-2": {
|
||||
data: {
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("CollectionMigrator", () => {
|
||||
let helper: MockProxy<MigrationHelper>;
|
||||
let sut: CollectionMigrator;
|
||||
const keyDefinitionLike = {
|
||||
key: "collections",
|
||||
stateDefinition: {
|
||||
name: "collection",
|
||||
},
|
||||
};
|
||||
|
||||
describe("migrate", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(exampleJSON(), 20);
|
||||
sut = new CollectionMigrator(20, 21);
|
||||
});
|
||||
|
||||
it("should remove collections from all accounts", async () => {
|
||||
await sut.migrate(helper);
|
||||
expect(helper.set).toHaveBeenCalledWith("user-1", {
|
||||
data: {
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
});
|
||||
});
|
||||
|
||||
it("should set collections value for each account", async () => {
|
||||
await sut.migrate(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("user-1", keyDefinitionLike, {
|
||||
"877fef70-be32-439e-8678-b0d80125653d": {
|
||||
id: "877fef70-be32-439e-8678-b0d80125653d",
|
||||
organizationId: "fe1ff6ef-d2d4-49f3-9c07-b0c7013998f9",
|
||||
name: "2.MD9OMDsvYiU1CTSUxjHorw==|uFc4cZhnmQmK2LFCWbyeZg==|syk2d9JESeplxInLvP36BK5RhqS1c/i+ZQp5NR7EUA4=",
|
||||
externalId: "",
|
||||
readOnly: false,
|
||||
manage: true,
|
||||
hidePasswords: false,
|
||||
},
|
||||
"0d3fee82-3f81-434c-aed0-b0c200ee6c7a": {
|
||||
id: "0d3fee82-3f81-434c-aed0-b0c200ee6c7a",
|
||||
organizationId: "5f277723-6391-4b5c-add9-b0c200ee6967",
|
||||
name: "2.GxnXkIbBCGFr57F6lT7+Ow==|3ctMg95FKquG3l+qfv8BgvaCbYzMmuhnukCEHXhUukE=|cJRZWq05xjPBayUgx6P6gsbtNVLi8exQwo8F1SfqQQ4=",
|
||||
externalId: "",
|
||||
readOnly: false,
|
||||
manage: false,
|
||||
hidePasswords: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("rollback", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(rollbackJSON(), 21);
|
||||
sut = new CollectionMigrator(20, 21);
|
||||
});
|
||||
|
||||
it.each(["user-1", "user-2"])("should null out new values", async (userId) => {
|
||||
await sut.rollback(helper);
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(userId, keyDefinitionLike, null);
|
||||
});
|
||||
|
||||
it("should add collection values back to accounts", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.set).toHaveBeenCalled();
|
||||
expect(helper.set).toHaveBeenCalledWith("user-1", {
|
||||
data: {
|
||||
collections: {
|
||||
encrypted: {
|
||||
"877fef70-be32-439e-8678-b0d80125653d": {
|
||||
id: "877fef70-be32-439e-8678-b0d80125653d",
|
||||
organizationId: "fe1ff6ef-d2d4-49f3-9c07-b0c7013998f9",
|
||||
name: "2.MD9OMDsvYiU1CTSUxjHorw==|uFc4cZhnmQmK2LFCWbyeZg==|syk2d9JESeplxInLvP36BK5RhqS1c/i+ZQp5NR7EUA4=",
|
||||
externalId: "",
|
||||
readOnly: false,
|
||||
manage: true,
|
||||
hidePasswords: false,
|
||||
},
|
||||
"0d3fee82-3f81-434c-aed0-b0c200ee6c7a": {
|
||||
id: "0d3fee82-3f81-434c-aed0-b0c200ee6c7a",
|
||||
organizationId: "5f277723-6391-4b5c-add9-b0c200ee6967",
|
||||
name: "2.GxnXkIbBCGFr57F6lT7+Ow==|3ctMg95FKquG3l+qfv8BgvaCbYzMmuhnukCEHXhUukE=|cJRZWq05xjPBayUgx6P6gsbtNVLi8exQwo8F1SfqQQ4=",
|
||||
externalId: "",
|
||||
readOnly: false,
|
||||
manage: false,
|
||||
hidePasswords: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
});
|
||||
});
|
||||
|
||||
it("should not try to restore values to missing accounts", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.set).not.toHaveBeenCalledWith("user-3", any());
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
|
||||
import { Migrator } from "../migrator";
|
||||
|
||||
type CollectionDataType = {
|
||||
id: string;
|
||||
organizationId: string;
|
||||
name: string;
|
||||
externalId: string;
|
||||
readOnly: boolean;
|
||||
manage: boolean;
|
||||
hidePasswords: boolean;
|
||||
};
|
||||
|
||||
type ExpectedAccountType = {
|
||||
data?: {
|
||||
collections?: {
|
||||
encrypted?: Record<string, CollectionDataType>;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
const USER_ENCRYPTED_COLLECTIONS: KeyDefinitionLike = {
|
||||
key: "collections",
|
||||
stateDefinition: {
|
||||
name: "collection",
|
||||
},
|
||||
};
|
||||
|
||||
export class CollectionMigrator extends Migrator<20, 21> {
|
||||
async migrate(helper: MigrationHelper): Promise<void> {
|
||||
const accounts = await helper.getAccounts<ExpectedAccountType>();
|
||||
|
||||
async function migrateAccount(userId: string, account: ExpectedAccountType): Promise<void> {
|
||||
const value = account?.data?.collections?.encrypted;
|
||||
if (value != null) {
|
||||
await helper.setToUser(userId, USER_ENCRYPTED_COLLECTIONS, value);
|
||||
delete account.data.collections;
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all([...accounts.map(({ userId, account }) => migrateAccount(userId, account))]);
|
||||
}
|
||||
|
||||
async rollback(helper: MigrationHelper): Promise<void> {
|
||||
const accounts = await helper.getAccounts<ExpectedAccountType>();
|
||||
|
||||
async function rollbackAccount(userId: string, account: ExpectedAccountType): Promise<void> {
|
||||
const value = await helper.getFromUser(userId, USER_ENCRYPTED_COLLECTIONS);
|
||||
if (account) {
|
||||
account.data = Object.assign(account.data ?? {}, {
|
||||
collections: {
|
||||
encrypted: value,
|
||||
},
|
||||
});
|
||||
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
await helper.setToUser(userId, USER_ENCRYPTED_COLLECTIONS, null);
|
||||
}
|
||||
await Promise.all([...accounts.map(({ userId, account }) => rollbackAccount(userId, account))]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
import { MockProxy, any } from "jest-mock-extended";
|
||||
|
||||
import { MigrationHelper } from "../migration-helper";
|
||||
import { mockMigrationHelper } from "../migration-helper.spec";
|
||||
|
||||
import { CollapsedGroupingsMigrator } from "./22-move-collapsed-groupings-to-state-provider";
|
||||
|
||||
function exampleJSON() {
|
||||
return {
|
||||
global: {
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
authenticatedAccounts: ["user-1", "user-2", "user-3"],
|
||||
"user-1": {
|
||||
settings: {
|
||||
collapsedGroupings: ["grouping-1", "grouping-2"],
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
},
|
||||
"user-2": {
|
||||
settings: {
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function rollbackJSON() {
|
||||
return {
|
||||
"user_user-1_vaultFilter_collapsedGroupings": ["grouping-1", "grouping-2"],
|
||||
global: {
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
authenticatedAccounts: ["user-1", "user-2"],
|
||||
"user-1": {
|
||||
settings: {
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
},
|
||||
"user-2": {
|
||||
settings: {
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("CollapsedGroupingsMigrator", () => {
|
||||
let helper: MockProxy<MigrationHelper>;
|
||||
let sut: CollapsedGroupingsMigrator;
|
||||
const keyDefinitionLike = {
|
||||
key: "collapsedGroupings",
|
||||
stateDefinition: {
|
||||
name: "vaultFilter",
|
||||
},
|
||||
};
|
||||
|
||||
describe("migrate", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(exampleJSON(), 21);
|
||||
sut = new CollapsedGroupingsMigrator(21, 22);
|
||||
});
|
||||
|
||||
it("should remove collapsedGroupings from all accounts", async () => {
|
||||
await sut.migrate(helper);
|
||||
expect(helper.set).toHaveBeenCalledWith("user-1", {
|
||||
settings: {
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
});
|
||||
});
|
||||
|
||||
it("should set collapsedGroupings values for each account", async () => {
|
||||
await sut.migrate(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("user-1", keyDefinitionLike, [
|
||||
"grouping-1",
|
||||
"grouping-2",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("rollback", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(rollbackJSON(), 22);
|
||||
sut = new CollapsedGroupingsMigrator(21, 22);
|
||||
});
|
||||
|
||||
it.each(["user-1", "user-2"])("should null out new values", async (userId) => {
|
||||
await sut.rollback(helper);
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(userId, keyDefinitionLike, null);
|
||||
});
|
||||
|
||||
it("should add explicit value back to accounts", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.set).toHaveBeenCalledTimes(2);
|
||||
expect(helper.set).toHaveBeenCalledWith("user-1", {
|
||||
settings: {
|
||||
collapsedGroupings: ["grouping-1", "grouping-2"],
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
});
|
||||
});
|
||||
|
||||
it("should not try to restore values to missing accounts", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.set).not.toHaveBeenCalledWith("user-3", any());
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
|
||||
import { Migrator } from "../migrator";
|
||||
|
||||
type ExpectedAccountType = {
|
||||
settings?: {
|
||||
collapsedGroupings?: string[];
|
||||
};
|
||||
};
|
||||
|
||||
const COLLAPSED_GROUPINGS: KeyDefinitionLike = {
|
||||
key: "collapsedGroupings",
|
||||
stateDefinition: {
|
||||
name: "vaultFilter",
|
||||
},
|
||||
};
|
||||
|
||||
export class CollapsedGroupingsMigrator extends Migrator<21, 22> {
|
||||
async migrate(helper: MigrationHelper): Promise<void> {
|
||||
const accounts = await helper.getAccounts<ExpectedAccountType>();
|
||||
async function migrateAccount(userId: string, account: ExpectedAccountType): Promise<void> {
|
||||
const value = account?.settings?.collapsedGroupings;
|
||||
if (value != null) {
|
||||
await helper.setToUser(userId, COLLAPSED_GROUPINGS, value);
|
||||
delete account.settings.collapsedGroupings;
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all([...accounts.map(({ userId, account }) => migrateAccount(userId, account))]);
|
||||
}
|
||||
|
||||
async rollback(helper: MigrationHelper): Promise<void> {
|
||||
const accounts = await helper.getAccounts<ExpectedAccountType>();
|
||||
async function rollbackAccount(userId: string, account: ExpectedAccountType): Promise<void> {
|
||||
const value = await helper.getFromUser(userId, COLLAPSED_GROUPINGS);
|
||||
if (account) {
|
||||
account.settings = Object.assign(account.settings ?? {}, {
|
||||
collapsedGroupings: value,
|
||||
});
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
await helper.setToUser(userId, COLLAPSED_GROUPINGS, null);
|
||||
}
|
||||
|
||||
await Promise.all([...accounts.map(({ userId, account }) => rollbackAccount(userId, account))]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import { MockProxy, any } from "jest-mock-extended";
|
||||
|
||||
import { MigrationHelper } from "../migration-helper";
|
||||
import { mockMigrationHelper } from "../migration-helper.spec";
|
||||
|
||||
import {
|
||||
MoveBiometricPromptsToStateProviders,
|
||||
DISMISSED_BIOMETRIC_REQUIRE_PASSWORD_ON_START_CALLOUT,
|
||||
PROMPT_AUTOMATICALLY,
|
||||
} from "./23-move-biometric-prompts-to-state-providers";
|
||||
|
||||
function exampleJSON() {
|
||||
return {
|
||||
global: {
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
authenticatedAccounts: ["user-1", "user-2", "user-3"],
|
||||
"user-1": {
|
||||
settings: {
|
||||
disableAutoBiometricsPrompt: false,
|
||||
dismissedBiometricRequirePasswordOnStartCallout: true,
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
},
|
||||
"user-2": {
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function rollbackJSON() {
|
||||
return {
|
||||
"user_user-1_biometricSettings_dismissedBiometricRequirePasswordOnStartCallout": true,
|
||||
"user_user-1_biometricSettings_promptAutomatically": "false",
|
||||
global: {
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
authenticatedAccounts: ["user-1", "user-2", "user-3"],
|
||||
"user-1": {
|
||||
settings: {
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
},
|
||||
"user-2": {
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("MoveBiometricPromptsToStateProviders migrator", () => {
|
||||
let helper: MockProxy<MigrationHelper>;
|
||||
let sut: MoveBiometricPromptsToStateProviders;
|
||||
|
||||
describe("migrate", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(exampleJSON(), 22);
|
||||
sut = new MoveBiometricPromptsToStateProviders(22, 23);
|
||||
});
|
||||
|
||||
it("should remove biometricUnlock, dismissedBiometricRequirePasswordOnStartCallout, and biometricEncryptionClientKeyHalf from all accounts", async () => {
|
||||
await sut.migrate(helper);
|
||||
expect(helper.set).toHaveBeenCalledTimes(2);
|
||||
expect(helper.set).toHaveBeenCalledWith("user-1", {
|
||||
settings: {
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
});
|
||||
expect(helper.set).toHaveBeenCalledWith("user-2", {
|
||||
otherStuff: "otherStuff4",
|
||||
});
|
||||
});
|
||||
|
||||
it("should set dismissedBiometricRequirePasswordOnStartCallout value for account that have it", async () => {
|
||||
await sut.migrate(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(
|
||||
"user-1",
|
||||
DISMISSED_BIOMETRIC_REQUIRE_PASSWORD_ON_START_CALLOUT,
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it("should not call extra setToUser", async () => {
|
||||
await sut.migrate(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("rollback", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(rollbackJSON(), 23);
|
||||
sut = new MoveBiometricPromptsToStateProviders(22, 23);
|
||||
});
|
||||
|
||||
it.each([DISMISSED_BIOMETRIC_REQUIRE_PASSWORD_ON_START_CALLOUT, PROMPT_AUTOMATICALLY])(
|
||||
"should null out new values %s",
|
||||
async (keyDefinition) => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("user-1", keyDefinition, null);
|
||||
},
|
||||
);
|
||||
|
||||
it("should add explicit value back to accounts", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.set).toHaveBeenCalledTimes(1);
|
||||
expect(helper.set).toHaveBeenCalledWith("user-1", {
|
||||
settings: {
|
||||
disableAutoBiometricsPrompt: false,
|
||||
dismissedBiometricRequirePasswordOnStartCallout: true,
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
});
|
||||
});
|
||||
|
||||
it.each(["user-2", "user-3"])(
|
||||
"should not try to restore values to missing accounts",
|
||||
async (userId) => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.set).not.toHaveBeenCalledWith(userId, any());
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,99 @@
|
||||
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
|
||||
import { Migrator } from "../migrator";
|
||||
|
||||
type ExpectedAccountType = {
|
||||
settings?: {
|
||||
disableAutoBiometricsPrompt?: boolean;
|
||||
dismissedBiometricRequirePasswordOnStartCallout?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
// prompt cancelled is refreshed on every app start/quit/unlock, so we don't need to migrate it
|
||||
|
||||
export const DISMISSED_BIOMETRIC_REQUIRE_PASSWORD_ON_START_CALLOUT: KeyDefinitionLike = {
|
||||
key: "dismissedBiometricRequirePasswordOnStartCallout",
|
||||
stateDefinition: { name: "biometricSettings" },
|
||||
};
|
||||
|
||||
export const PROMPT_AUTOMATICALLY: KeyDefinitionLike = {
|
||||
key: "promptAutomatically",
|
||||
stateDefinition: { name: "biometricSettings" },
|
||||
};
|
||||
|
||||
export class MoveBiometricPromptsToStateProviders extends Migrator<22, 23> {
|
||||
async migrate(helper: MigrationHelper): Promise<void> {
|
||||
const legacyAccounts = await helper.getAccounts<ExpectedAccountType>();
|
||||
|
||||
await Promise.all(
|
||||
legacyAccounts.map(async ({ userId, account }) => {
|
||||
if (account == null) {
|
||||
return;
|
||||
}
|
||||
// Move account data
|
||||
|
||||
if (account?.settings?.dismissedBiometricRequirePasswordOnStartCallout != null) {
|
||||
await helper.setToUser(
|
||||
userId,
|
||||
DISMISSED_BIOMETRIC_REQUIRE_PASSWORD_ON_START_CALLOUT,
|
||||
account.settings.dismissedBiometricRequirePasswordOnStartCallout,
|
||||
);
|
||||
}
|
||||
|
||||
if (account?.settings?.disableAutoBiometricsPrompt != null) {
|
||||
await helper.setToUser(
|
||||
userId,
|
||||
PROMPT_AUTOMATICALLY,
|
||||
!account.settings.disableAutoBiometricsPrompt,
|
||||
);
|
||||
}
|
||||
|
||||
// Delete old account data
|
||||
delete account?.settings?.dismissedBiometricRequirePasswordOnStartCallout;
|
||||
delete account?.settings?.disableAutoBiometricsPrompt;
|
||||
await helper.set(userId, account);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async rollback(helper: MigrationHelper): Promise<void> {
|
||||
async function rollbackUser(userId: string, account: ExpectedAccountType) {
|
||||
let updatedAccount = false;
|
||||
|
||||
const userDismissed = await helper.getFromUser<boolean>(
|
||||
userId,
|
||||
DISMISSED_BIOMETRIC_REQUIRE_PASSWORD_ON_START_CALLOUT,
|
||||
);
|
||||
|
||||
if (userDismissed) {
|
||||
account ??= {};
|
||||
account.settings ??= {};
|
||||
|
||||
updatedAccount = true;
|
||||
account.settings.dismissedBiometricRequirePasswordOnStartCallout = userDismissed;
|
||||
await helper.setToUser(userId, DISMISSED_BIOMETRIC_REQUIRE_PASSWORD_ON_START_CALLOUT, null);
|
||||
}
|
||||
|
||||
const userPromptAutomatically = await helper.getFromUser<boolean>(
|
||||
userId,
|
||||
PROMPT_AUTOMATICALLY,
|
||||
);
|
||||
|
||||
if (userPromptAutomatically != null) {
|
||||
account ??= {};
|
||||
account.settings ??= {};
|
||||
|
||||
updatedAccount = true;
|
||||
account.settings.disableAutoBiometricsPrompt = !userPromptAutomatically;
|
||||
await helper.setToUser(userId, PROMPT_AUTOMATICALLY, null);
|
||||
}
|
||||
|
||||
if (updatedAccount) {
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
}
|
||||
|
||||
const accounts = await helper.getAccounts<ExpectedAccountType>();
|
||||
|
||||
await Promise.all(accounts.map(({ userId, account }) => rollbackUser(userId, account)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
import { MockProxy, any } from "jest-mock-extended";
|
||||
|
||||
import { MigrationHelper } from "../migration-helper";
|
||||
import { mockMigrationHelper } from "../migration-helper.spec";
|
||||
|
||||
import { SmOnboardingTasksMigrator } from "./24-move-sm-onboarding-key-to-state-providers";
|
||||
|
||||
function exampleJSON() {
|
||||
return {
|
||||
authenticatedAccounts: ["user-1", "user-2", "user-3"],
|
||||
"user-1": {
|
||||
settings: {
|
||||
smOnboardingTasks: {
|
||||
"0bd005de-c722-473b-a00c-b10101006fcd": {
|
||||
createProject: true,
|
||||
createSecret: true,
|
||||
createServiceAccount: true,
|
||||
importSecrets: true,
|
||||
},
|
||||
"2f0d26ec-493a-4ed7-9183-b10d013597c8": {
|
||||
createProject: false,
|
||||
createSecret: true,
|
||||
createServiceAccount: false,
|
||||
importSecrets: true,
|
||||
},
|
||||
},
|
||||
someOtherProperty: "Some other value",
|
||||
},
|
||||
otherStuff: "otherStuff",
|
||||
},
|
||||
"user-2": {
|
||||
settings: {
|
||||
smOnboardingTasks: {
|
||||
"000000-0000000-0000000-000000000": {
|
||||
createProject: false,
|
||||
createSecret: false,
|
||||
createServiceAccount: false,
|
||||
importSecrets: false,
|
||||
},
|
||||
},
|
||||
someOtherProperty: "Some other value",
|
||||
},
|
||||
otherStuff: "otherStuff",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function rollbackJSON() {
|
||||
return {
|
||||
"user_user-1_smOnboarding_tasks": {
|
||||
"0bd005de-c722-473b-a00c-b10101006fcd": {
|
||||
createProject: true,
|
||||
createSecret: true,
|
||||
createServiceAccount: true,
|
||||
importSecrets: true,
|
||||
},
|
||||
"2f0d26ec-493a-4ed7-9183-b10d013597c8": {
|
||||
createProject: false,
|
||||
createSecret: true,
|
||||
createServiceAccount: false,
|
||||
importSecrets: true,
|
||||
},
|
||||
},
|
||||
"user_user-2_smOnboarding_tasks": {
|
||||
"000000-0000000-0000000-000000000": {
|
||||
createProject: false,
|
||||
createSecret: false,
|
||||
createServiceAccount: false,
|
||||
importSecrets: false,
|
||||
},
|
||||
},
|
||||
authenticatedAccounts: ["user-1", "user-2"],
|
||||
"user-1": {
|
||||
settings: {
|
||||
someOtherProperty: "Some other value",
|
||||
},
|
||||
otherStuff: "otherStuff",
|
||||
},
|
||||
"user-2": {
|
||||
settings: {
|
||||
someOtherProperty: "Some other value",
|
||||
},
|
||||
otherStuff: "otherStuff",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("SmOnboardingTasksMigrator", () => {
|
||||
let helper: MockProxy<MigrationHelper>;
|
||||
let sut: SmOnboardingTasksMigrator;
|
||||
|
||||
const keyDefinitionLike = {
|
||||
key: "tasks",
|
||||
stateDefinition: { name: "smOnboarding" },
|
||||
};
|
||||
|
||||
describe("migrate", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(exampleJSON(), 23);
|
||||
sut = new SmOnboardingTasksMigrator(23, 24);
|
||||
});
|
||||
|
||||
it("should remove smOnboardingTasks from all accounts", async () => {
|
||||
await sut.migrate(helper);
|
||||
expect(helper.set).toHaveBeenCalledWith("user-1", {
|
||||
settings: {
|
||||
someOtherProperty: "Some other value",
|
||||
},
|
||||
otherStuff: "otherStuff",
|
||||
});
|
||||
});
|
||||
|
||||
it("should set smOnboardingTasks provider value for each account", async () => {
|
||||
await sut.migrate(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("user-1", keyDefinitionLike, {
|
||||
"0bd005de-c722-473b-a00c-b10101006fcd": {
|
||||
createProject: true,
|
||||
createSecret: true,
|
||||
createServiceAccount: true,
|
||||
importSecrets: true,
|
||||
},
|
||||
"2f0d26ec-493a-4ed7-9183-b10d013597c8": {
|
||||
createProject: false,
|
||||
createSecret: true,
|
||||
createServiceAccount: false,
|
||||
importSecrets: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("user-2", keyDefinitionLike, {
|
||||
"000000-0000000-0000000-000000000": {
|
||||
createProject: false,
|
||||
createSecret: false,
|
||||
createServiceAccount: false,
|
||||
importSecrets: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("rollback", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(rollbackJSON(), 24);
|
||||
sut = new SmOnboardingTasksMigrator(23, 24);
|
||||
});
|
||||
|
||||
it.each(["user-1", "user-2"])("should null out new values", async (userId) => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(userId, keyDefinitionLike, null);
|
||||
});
|
||||
|
||||
it("should add smOnboardingTasks back to accounts", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.set).toHaveBeenCalledWith("user-1", {
|
||||
settings: {
|
||||
smOnboardingTasks: {
|
||||
"0bd005de-c722-473b-a00c-b10101006fcd": {
|
||||
createProject: true,
|
||||
createSecret: true,
|
||||
createServiceAccount: true,
|
||||
importSecrets: true,
|
||||
},
|
||||
"2f0d26ec-493a-4ed7-9183-b10d013597c8": {
|
||||
createProject: false,
|
||||
createSecret: true,
|
||||
createServiceAccount: false,
|
||||
importSecrets: true,
|
||||
},
|
||||
},
|
||||
someOtherProperty: "Some other value",
|
||||
},
|
||||
otherStuff: "otherStuff",
|
||||
});
|
||||
|
||||
expect(helper.set).toHaveBeenCalledWith("user-2", {
|
||||
settings: {
|
||||
smOnboardingTasks: {
|
||||
"000000-0000000-0000000-000000000": {
|
||||
createProject: false,
|
||||
createSecret: false,
|
||||
createServiceAccount: false,
|
||||
importSecrets: false,
|
||||
},
|
||||
},
|
||||
someOtherProperty: "Some other value",
|
||||
},
|
||||
otherStuff: "otherStuff",
|
||||
});
|
||||
});
|
||||
|
||||
it("should not try to restore values to missing accounts", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.set).not.toHaveBeenCalledWith("user-3", any());
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
|
||||
import { Migrator } from "../migrator";
|
||||
|
||||
type ExpectedAccountType = {
|
||||
settings?: {
|
||||
smOnboardingTasks?: Record<string, Record<string, boolean>>;
|
||||
};
|
||||
};
|
||||
|
||||
export const SM_ONBOARDING_TASKS: KeyDefinitionLike = {
|
||||
key: "tasks",
|
||||
stateDefinition: { name: "smOnboarding" },
|
||||
};
|
||||
|
||||
export class SmOnboardingTasksMigrator extends Migrator<23, 24> {
|
||||
async migrate(helper: MigrationHelper): Promise<void> {
|
||||
const legacyAccounts = await helper.getAccounts<ExpectedAccountType>();
|
||||
|
||||
await Promise.all(
|
||||
legacyAccounts.map(async ({ userId, account }) => {
|
||||
// Move account data
|
||||
if (account?.settings?.smOnboardingTasks != null) {
|
||||
await helper.setToUser(userId, SM_ONBOARDING_TASKS, account.settings.smOnboardingTasks);
|
||||
|
||||
// Delete old account data
|
||||
delete account.settings.smOnboardingTasks;
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async rollback(helper: MigrationHelper): Promise<void> {
|
||||
async function rollbackUser(userId: string, account: ExpectedAccountType) {
|
||||
const smOnboardingTasks = await helper.getFromUser<Record<string, Record<string, boolean>>>(
|
||||
userId,
|
||||
SM_ONBOARDING_TASKS,
|
||||
);
|
||||
if (smOnboardingTasks) {
|
||||
account ??= {};
|
||||
account.settings ??= {};
|
||||
|
||||
account.settings.smOnboardingTasks = smOnboardingTasks;
|
||||
await helper.setToUser(userId, SM_ONBOARDING_TASKS, null);
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
}
|
||||
|
||||
const accounts = await helper.getAccounts<ExpectedAccountType>();
|
||||
|
||||
await Promise.all(accounts.map(({ userId, account }) => rollbackUser(userId, account)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
import { any, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { StateDefinitionLike, MigrationHelper } from "../migration-helper";
|
||||
import { mockMigrationHelper } from "../migration-helper.spec";
|
||||
|
||||
import { ClearClipboardDelayMigrator } from "./25-move-clear-clipboard-to-autofill-settings-state-provider";
|
||||
|
||||
export const ClearClipboardDelay = {
|
||||
Never: null as null,
|
||||
TenSeconds: 10,
|
||||
TwentySeconds: 20,
|
||||
ThirtySeconds: 30,
|
||||
OneMinute: 60,
|
||||
TwoMinutes: 120,
|
||||
FiveMinutes: 300,
|
||||
} as const;
|
||||
|
||||
const AutofillOverlayVisibility = {
|
||||
Off: 0,
|
||||
OnButtonClick: 1,
|
||||
OnFieldFocus: 2,
|
||||
} as const;
|
||||
|
||||
function exampleJSON() {
|
||||
return {
|
||||
global: {
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
authenticatedAccounts: ["user-1", "user-2", "user-3"],
|
||||
"user-1": {
|
||||
settings: {
|
||||
clearClipboard: ClearClipboardDelay.TenSeconds,
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
},
|
||||
"user-2": {
|
||||
settings: {
|
||||
clearClipboard: ClearClipboardDelay.Never,
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
},
|
||||
"user-3": {
|
||||
settings: {
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function rollbackJSON() {
|
||||
return {
|
||||
global_autofillSettingsLocal_inlineMenuVisibility: AutofillOverlayVisibility.OnButtonClick,
|
||||
"user_user-1_autofillSettingsLocal_clearClipboardDelay": ClearClipboardDelay.TenSeconds,
|
||||
"user_user-2_autofillSettingsLocal_clearClipboardDelay": ClearClipboardDelay.Never,
|
||||
global: {
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
authenticatedAccounts: ["user-1", "user-2", "user-3"],
|
||||
"user-1": {
|
||||
settings: {
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
},
|
||||
"user-2": {
|
||||
settings: {
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const autofillSettingsLocalStateDefinition: {
|
||||
stateDefinition: StateDefinitionLike;
|
||||
} = {
|
||||
stateDefinition: {
|
||||
name: "autofillSettingsLocal",
|
||||
},
|
||||
};
|
||||
|
||||
describe("ClearClipboardDelayMigrator", () => {
|
||||
let helper: MockProxy<MigrationHelper>;
|
||||
let sut: ClearClipboardDelayMigrator;
|
||||
|
||||
describe("migrate", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(exampleJSON(), 24);
|
||||
sut = new ClearClipboardDelayMigrator(24, 25);
|
||||
});
|
||||
|
||||
it("should remove clearClipboard setting from all accounts", async () => {
|
||||
await sut.migrate(helper);
|
||||
expect(helper.set).toHaveBeenCalledTimes(2);
|
||||
expect(helper.set).toHaveBeenCalledWith("user-1", {
|
||||
settings: {
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
});
|
||||
expect(helper.set).toHaveBeenCalledWith("user-2", {
|
||||
settings: {
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
});
|
||||
});
|
||||
|
||||
it("should set autofill setting values for each account", async () => {
|
||||
await sut.migrate(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledTimes(2);
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(
|
||||
"user-1",
|
||||
{ ...autofillSettingsLocalStateDefinition, key: "clearClipboardDelay" },
|
||||
ClearClipboardDelay.TenSeconds,
|
||||
);
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(
|
||||
"user-2",
|
||||
{ ...autofillSettingsLocalStateDefinition, key: "clearClipboardDelay" },
|
||||
ClearClipboardDelay.Never,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("rollback", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(rollbackJSON(), 25);
|
||||
sut = new ClearClipboardDelayMigrator(24, 25);
|
||||
});
|
||||
|
||||
it("should null out new values for each account", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledTimes(2);
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(
|
||||
"user-1",
|
||||
{ ...autofillSettingsLocalStateDefinition, key: "clearClipboardDelay" },
|
||||
null,
|
||||
);
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(
|
||||
"user-2",
|
||||
{ ...autofillSettingsLocalStateDefinition, key: "clearClipboardDelay" },
|
||||
null,
|
||||
);
|
||||
});
|
||||
|
||||
it("should add explicit value back to accounts", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.set).toHaveBeenCalledTimes(2);
|
||||
expect(helper.set).toHaveBeenCalledWith("user-1", {
|
||||
settings: {
|
||||
clearClipboard: ClearClipboardDelay.TenSeconds,
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
});
|
||||
expect(helper.set).toHaveBeenCalledWith("user-2", {
|
||||
settings: {
|
||||
clearClipboard: ClearClipboardDelay.Never,
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
});
|
||||
});
|
||||
|
||||
it("should not try to restore values to missing accounts", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.set).not.toHaveBeenCalledWith("user-3", any());
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,92 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { StateDefinitionLike, MigrationHelper } from "../migration-helper";
|
||||
import { Migrator } from "../migrator";
|
||||
|
||||
// FIXME: Remove when updating file. Eslint update
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const ClearClipboardDelay = {
|
||||
Never: null as null,
|
||||
TenSeconds: 10,
|
||||
TwentySeconds: 20,
|
||||
ThirtySeconds: 30,
|
||||
OneMinute: 60,
|
||||
TwoMinutes: 120,
|
||||
FiveMinutes: 300,
|
||||
} as const;
|
||||
|
||||
type ClearClipboardDelaySetting = (typeof ClearClipboardDelay)[keyof typeof ClearClipboardDelay];
|
||||
|
||||
type ExpectedAccountState = {
|
||||
settings?: {
|
||||
clearClipboard?: ClearClipboardDelaySetting;
|
||||
};
|
||||
};
|
||||
|
||||
const autofillSettingsLocalStateDefinition: {
|
||||
stateDefinition: StateDefinitionLike;
|
||||
} = {
|
||||
stateDefinition: {
|
||||
name: "autofillSettingsLocal",
|
||||
},
|
||||
};
|
||||
|
||||
export class ClearClipboardDelayMigrator extends Migrator<24, 25> {
|
||||
async migrate(helper: MigrationHelper): Promise<void> {
|
||||
// account state (e.g. account settings -> state provider framework keys)
|
||||
const accounts = await helper.getAccounts<ExpectedAccountState>();
|
||||
|
||||
await Promise.all([...accounts.map(({ userId, account }) => migrateAccount(userId, account))]);
|
||||
|
||||
// migrate account state
|
||||
async function migrateAccount(userId: string, account: ExpectedAccountState): Promise<void> {
|
||||
const accountSettings = account?.settings;
|
||||
|
||||
if (accountSettings?.clearClipboard !== undefined) {
|
||||
await helper.setToUser(
|
||||
userId,
|
||||
{ ...autofillSettingsLocalStateDefinition, key: "clearClipboardDelay" },
|
||||
accountSettings.clearClipboard,
|
||||
);
|
||||
delete account.settings.clearClipboard;
|
||||
|
||||
// update the state account settings with the migrated values deleted
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async rollback(helper: MigrationHelper): Promise<void> {
|
||||
// account state (e.g. state provider framework keys -> account settings)
|
||||
const accounts = await helper.getAccounts<ExpectedAccountState>();
|
||||
|
||||
await Promise.all([...accounts.map(({ userId, account }) => rollbackAccount(userId, account))]);
|
||||
|
||||
// rollback account state
|
||||
async function rollbackAccount(userId: string, account: ExpectedAccountState): Promise<void> {
|
||||
let settings = account?.settings || {};
|
||||
|
||||
const clearClipboardDelay: ClearClipboardDelaySetting = await helper.getFromUser(userId, {
|
||||
...autofillSettingsLocalStateDefinition,
|
||||
key: "clearClipboardDelay",
|
||||
});
|
||||
|
||||
// update new settings and remove the account state provider framework keys for the rolled back values
|
||||
if (clearClipboardDelay !== undefined) {
|
||||
settings = { ...settings, clearClipboard: clearClipboardDelay };
|
||||
|
||||
await helper.setToUser(
|
||||
userId,
|
||||
{ ...autofillSettingsLocalStateDefinition, key: "clearClipboardDelay" },
|
||||
null,
|
||||
);
|
||||
|
||||
// commit updated settings to state
|
||||
await helper.set(userId, {
|
||||
...account,
|
||||
settings,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import { any, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { MigrationHelper } from "../migration-helper";
|
||||
import { mockMigrationHelper } from "../migration-helper.spec";
|
||||
|
||||
import { RevertLastSyncMigrator } from "./26-revert-move-last-sync-to-state-provider";
|
||||
|
||||
function rollbackJSON() {
|
||||
return {
|
||||
global: {
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
authenticatedAccounts: ["user-1", "user-2"],
|
||||
"user-1": {
|
||||
profile: {
|
||||
lastSync: "2024-01-24T00:00:00.000Z",
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function exampleJSON() {
|
||||
return {
|
||||
"user_user-1_sync_lastSync": "2024-01-24T00:00:00.000Z",
|
||||
"user_user-2_sync_lastSync": null as any,
|
||||
global: {
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
authenticatedAccounts: ["user-1", "user-2"],
|
||||
"user-1": {
|
||||
profile: {
|
||||
lastSync: "2024-01-24T00:00:00.000Z",
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("LastSyncMigrator", () => {
|
||||
let helper: MockProxy<MigrationHelper>;
|
||||
let sut: RevertLastSyncMigrator;
|
||||
|
||||
const keyDefinitionLike = {
|
||||
key: "lastSync",
|
||||
stateDefinition: {
|
||||
name: "sync",
|
||||
},
|
||||
};
|
||||
|
||||
describe("rollback", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(rollbackJSON(), 26);
|
||||
sut = new RevertLastSyncMigrator(25, 26);
|
||||
});
|
||||
|
||||
it("should remove lastSync from all accounts", async () => {
|
||||
await sut.rollback(helper);
|
||||
expect(helper.set).toHaveBeenCalledWith("user-1", {
|
||||
profile: {
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
});
|
||||
});
|
||||
|
||||
it("should set lastSync provider value for each account", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(
|
||||
"user-1",
|
||||
keyDefinitionLike,
|
||||
"2024-01-24T00:00:00.000Z",
|
||||
);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("user-2", keyDefinitionLike, null);
|
||||
});
|
||||
});
|
||||
|
||||
describe("migrate", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(exampleJSON(), 25);
|
||||
sut = new RevertLastSyncMigrator(25, 26);
|
||||
});
|
||||
|
||||
it.each(["user-1", "user-2"])("should null out new values", async (userId) => {
|
||||
await sut.migrate(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(userId, keyDefinitionLike, null);
|
||||
});
|
||||
|
||||
it("should add lastSync back to accounts", async () => {
|
||||
await sut.migrate(helper);
|
||||
|
||||
expect(helper.set).toHaveBeenCalledWith("user-1", {
|
||||
profile: {
|
||||
lastSync: "2024-01-24T00:00:00.000Z",
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
});
|
||||
});
|
||||
|
||||
it("should not try to restore values to missing accounts", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.set).not.toHaveBeenCalledWith("user-2", any());
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
|
||||
import { Migrator } from "../migrator";
|
||||
|
||||
type ExpectedAccountType = {
|
||||
profile?: {
|
||||
lastSync?: string;
|
||||
};
|
||||
};
|
||||
|
||||
const LAST_SYNC_KEY: KeyDefinitionLike = {
|
||||
key: "lastSync",
|
||||
stateDefinition: {
|
||||
name: "sync",
|
||||
},
|
||||
};
|
||||
|
||||
export class RevertLastSyncMigrator extends Migrator<25, 26> {
|
||||
async rollback(helper: MigrationHelper): Promise<void> {
|
||||
const accounts = await helper.getAccounts<ExpectedAccountType>();
|
||||
async function rollbackAccount(userId: string, account: ExpectedAccountType): Promise<void> {
|
||||
const value = account?.profile?.lastSync;
|
||||
await helper.setToUser(userId, LAST_SYNC_KEY, value ?? null);
|
||||
if (value != null) {
|
||||
delete account.profile.lastSync;
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all([...accounts.map(({ userId, account }) => rollbackAccount(userId, account))]);
|
||||
}
|
||||
async migrate(helper: MigrationHelper): Promise<void> {
|
||||
const accounts = await helper.getAccounts<ExpectedAccountType>();
|
||||
|
||||
async function migrateAccount(userId: string, account: ExpectedAccountType): Promise<void> {
|
||||
const value = await helper.getFromUser(userId, LAST_SYNC_KEY);
|
||||
if (account) {
|
||||
account.profile = Object.assign(account.profile ?? {}, {
|
||||
lastSync: value,
|
||||
});
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
await helper.setToUser(userId, LAST_SYNC_KEY, null);
|
||||
}
|
||||
|
||||
await Promise.all([...accounts.map(({ userId, account }) => migrateAccount(userId, account))]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
import { any, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { StateDefinitionLike, MigrationHelper } from "../migration-helper";
|
||||
import { mockMigrationHelper } from "../migration-helper.spec";
|
||||
|
||||
import { BadgeSettingsMigrator } from "./27-move-badge-settings-to-state-providers";
|
||||
|
||||
function exampleJSON() {
|
||||
return {
|
||||
global: {
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
authenticatedAccounts: ["user-1", "user-2", "user-3"],
|
||||
"user-1": {
|
||||
settings: {
|
||||
disableBadgeCounter: true,
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
},
|
||||
"user-2": {
|
||||
settings: {
|
||||
disableBadgeCounter: false,
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
},
|
||||
"user-3": {
|
||||
settings: {
|
||||
otherStuff: "otherStuff6",
|
||||
},
|
||||
otherStuff: "otherStuff7",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function rollbackJSON() {
|
||||
return {
|
||||
"user_user-1_badgeSettings_enableBadgeCounter": false,
|
||||
"user_user-2_badgeSettings_enableBadgeCounter": true,
|
||||
global: {
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
authenticatedAccounts: ["user-1", "user-2", "user-3"],
|
||||
"user-1": {
|
||||
settings: {
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
},
|
||||
"user-2": {
|
||||
settings: {
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
},
|
||||
"user-3": {
|
||||
settings: {
|
||||
otherStuff: "otherStuff6",
|
||||
},
|
||||
otherStuff: "otherStuff7",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const badgeSettingsStateDefinition: {
|
||||
stateDefinition: StateDefinitionLike;
|
||||
} = {
|
||||
stateDefinition: {
|
||||
name: "badgeSettings",
|
||||
},
|
||||
};
|
||||
|
||||
describe("BadgeSettingsMigrator", () => {
|
||||
let helper: MockProxy<MigrationHelper>;
|
||||
let sut: BadgeSettingsMigrator;
|
||||
|
||||
describe("migrate", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(exampleJSON(), 26);
|
||||
sut = new BadgeSettingsMigrator(26, 27);
|
||||
});
|
||||
|
||||
it("should remove disableBadgeCounter setting from all accounts", async () => {
|
||||
await sut.migrate(helper);
|
||||
expect(helper.set).toHaveBeenCalledTimes(2);
|
||||
expect(helper.set).toHaveBeenCalledWith("user-1", {
|
||||
settings: {
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
});
|
||||
expect(helper.set).toHaveBeenCalledWith("user-2", {
|
||||
settings: {
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
});
|
||||
});
|
||||
|
||||
it("should set badge setting values for each account", async () => {
|
||||
await sut.migrate(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledTimes(2);
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(
|
||||
"user-1",
|
||||
{ ...badgeSettingsStateDefinition, key: "enableBadgeCounter" },
|
||||
false,
|
||||
);
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(
|
||||
"user-2",
|
||||
{ ...badgeSettingsStateDefinition, key: "enableBadgeCounter" },
|
||||
true,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("rollback", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(rollbackJSON(), 27);
|
||||
sut = new BadgeSettingsMigrator(26, 27);
|
||||
});
|
||||
|
||||
it("should null out new values for each account", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledTimes(2);
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(
|
||||
"user-1",
|
||||
{ ...badgeSettingsStateDefinition, key: "enableBadgeCounter" },
|
||||
null,
|
||||
);
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(
|
||||
"user-2",
|
||||
{ ...badgeSettingsStateDefinition, key: "enableBadgeCounter" },
|
||||
null,
|
||||
);
|
||||
});
|
||||
|
||||
it("should add explicit value back to accounts", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.set).toHaveBeenCalledTimes(2);
|
||||
expect(helper.set).toHaveBeenCalledWith("user-1", {
|
||||
settings: {
|
||||
disableBadgeCounter: true,
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
});
|
||||
expect(helper.set).toHaveBeenCalledWith("user-2", {
|
||||
settings: {
|
||||
disableBadgeCounter: false,
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
});
|
||||
});
|
||||
|
||||
it("should not try to restore values to missing accounts", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.set).not.toHaveBeenCalledWith("user-3", any());
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,73 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
|
||||
import { Migrator } from "../migrator";
|
||||
|
||||
type ExpectedAccountState = {
|
||||
settings?: {
|
||||
disableBadgeCounter?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
const enableBadgeCounterKeyDefinition: KeyDefinitionLike = {
|
||||
stateDefinition: {
|
||||
name: "badgeSettings",
|
||||
},
|
||||
key: "enableBadgeCounter",
|
||||
};
|
||||
|
||||
export class BadgeSettingsMigrator extends Migrator<26, 27> {
|
||||
async migrate(helper: MigrationHelper): Promise<void> {
|
||||
// account state (e.g. account settings -> state provider framework keys)
|
||||
const accounts = await helper.getAccounts<ExpectedAccountState>();
|
||||
|
||||
await Promise.all([...accounts.map(({ userId, account }) => migrateAccount(userId, account))]);
|
||||
|
||||
// migrate account state
|
||||
async function migrateAccount(userId: string, account: ExpectedAccountState): Promise<void> {
|
||||
const accountSettings = account?.settings;
|
||||
|
||||
if (accountSettings?.disableBadgeCounter != undefined) {
|
||||
await helper.setToUser(
|
||||
userId,
|
||||
enableBadgeCounterKeyDefinition,
|
||||
!accountSettings.disableBadgeCounter,
|
||||
);
|
||||
delete account.settings.disableBadgeCounter;
|
||||
|
||||
// update the state account settings with the migrated values deleted
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async rollback(helper: MigrationHelper): Promise<void> {
|
||||
// account state (e.g. state provider framework keys -> account settings)
|
||||
const accounts = await helper.getAccounts<ExpectedAccountState>();
|
||||
|
||||
await Promise.all([...accounts.map(({ userId, account }) => rollbackAccount(userId, account))]);
|
||||
|
||||
// rollback account state
|
||||
async function rollbackAccount(userId: string, account: ExpectedAccountState): Promise<void> {
|
||||
let settings = account?.settings || {};
|
||||
|
||||
const enableBadgeCounter: boolean = await helper.getFromUser(
|
||||
userId,
|
||||
enableBadgeCounterKeyDefinition,
|
||||
);
|
||||
|
||||
// update new settings and remove the account state provider framework keys for the rolled back values
|
||||
if (enableBadgeCounter != undefined) {
|
||||
settings = { ...settings, disableBadgeCounter: !enableBadgeCounter };
|
||||
|
||||
await helper.setToUser(userId, enableBadgeCounterKeyDefinition, null);
|
||||
|
||||
// commit updated settings to state
|
||||
await helper.set(userId, {
|
||||
...account,
|
||||
settings,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import { MockProxy, any } from "jest-mock-extended";
|
||||
|
||||
import { MigrationHelper } from "../migration-helper";
|
||||
import { mockMigrationHelper } from "../migration-helper.spec";
|
||||
|
||||
import {
|
||||
BIOMETRIC_UNLOCK_ENABLED,
|
||||
MoveBiometricUnlockToStateProviders,
|
||||
} from "./28-move-biometric-unlock-to-state-providers";
|
||||
|
||||
function exampleJSON() {
|
||||
return {
|
||||
global: {
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
authenticatedAccounts: ["user-1", "user-2", "user-3"],
|
||||
"user-1": {
|
||||
settings: {
|
||||
biometricUnlock: true,
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
},
|
||||
"user-2": {
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function rollbackJSON() {
|
||||
return {
|
||||
"user_user-1_biometricSettings_biometricUnlockEnabled": true,
|
||||
global: {
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
authenticatedAccounts: ["user-1", "user-2", "user-3"],
|
||||
"user-1": {
|
||||
settings: {
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
},
|
||||
"user-2": {
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("MoveBiometricPromptsToStateProviders migrator", () => {
|
||||
let helper: MockProxy<MigrationHelper>;
|
||||
let sut: MoveBiometricUnlockToStateProviders;
|
||||
|
||||
describe("migrate", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(exampleJSON(), 27);
|
||||
sut = new MoveBiometricUnlockToStateProviders(27, 28);
|
||||
});
|
||||
|
||||
it("removes biometricUnlock from all accounts", async () => {
|
||||
await sut.migrate(helper);
|
||||
expect(helper.set).toHaveBeenCalledTimes(2);
|
||||
expect(helper.set).toHaveBeenCalledWith("user-1", {
|
||||
settings: {
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
});
|
||||
expect(helper.set).toHaveBeenCalledWith("user-2", {
|
||||
otherStuff: "otherStuff4",
|
||||
});
|
||||
});
|
||||
|
||||
it("sets biometricUnlock value for account that have it", async () => {
|
||||
await sut.migrate(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("user-1", BIOMETRIC_UNLOCK_ENABLED, true);
|
||||
});
|
||||
|
||||
it("should not call extra setToUser", async () => {
|
||||
await sut.migrate(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("rollback", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(rollbackJSON(), 28);
|
||||
sut = new MoveBiometricUnlockToStateProviders(27, 28);
|
||||
});
|
||||
|
||||
it("nulls out new values", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("user-1", BIOMETRIC_UNLOCK_ENABLED, null);
|
||||
});
|
||||
|
||||
it("adds explicit value back to accounts", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.set).toHaveBeenCalledTimes(1);
|
||||
expect(helper.set).toHaveBeenCalledWith("user-1", {
|
||||
settings: {
|
||||
biometricUnlock: true,
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
});
|
||||
});
|
||||
|
||||
it.each(["user-2", "user-3"])(
|
||||
"does not restore values when accounts are not present",
|
||||
async (userId) => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.set).not.toHaveBeenCalledWith(userId, any());
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
|
||||
import { Migrator } from "../migrator";
|
||||
|
||||
type ExpectedAccountType = {
|
||||
settings?: {
|
||||
biometricUnlock?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export const BIOMETRIC_UNLOCK_ENABLED: KeyDefinitionLike = {
|
||||
key: "biometricUnlockEnabled",
|
||||
stateDefinition: { name: "biometricSettings" },
|
||||
};
|
||||
|
||||
export class MoveBiometricUnlockToStateProviders extends Migrator<27, 28> {
|
||||
async migrate(helper: MigrationHelper): Promise<void> {
|
||||
const legacyAccounts = await helper.getAccounts<ExpectedAccountType>();
|
||||
|
||||
await Promise.all(
|
||||
legacyAccounts.map(async ({ userId, account }) => {
|
||||
if (account == null) {
|
||||
return;
|
||||
}
|
||||
// Move account data
|
||||
if (account?.settings?.biometricUnlock != null) {
|
||||
await helper.setToUser(
|
||||
userId,
|
||||
BIOMETRIC_UNLOCK_ENABLED,
|
||||
account.settings.biometricUnlock,
|
||||
);
|
||||
}
|
||||
|
||||
// Delete old account data
|
||||
delete account?.settings?.biometricUnlock;
|
||||
await helper.set(userId, account);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async rollback(helper: MigrationHelper): Promise<void> {
|
||||
async function rollbackUser(userId: string, account: ExpectedAccountType) {
|
||||
const biometricUnlock = await helper.getFromUser<boolean>(userId, BIOMETRIC_UNLOCK_ENABLED);
|
||||
|
||||
if (biometricUnlock != null) {
|
||||
account ??= {};
|
||||
account.settings ??= {};
|
||||
|
||||
account.settings.biometricUnlock = biometricUnlock;
|
||||
await helper.setToUser(userId, BIOMETRIC_UNLOCK_ENABLED, null);
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
}
|
||||
|
||||
const accounts = await helper.getAccounts<ExpectedAccountType>();
|
||||
|
||||
await Promise.all(accounts.map(({ userId, account }) => rollbackUser(userId, account)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
import { any, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { MigrationHelper } from "../migration-helper";
|
||||
import { mockMigrationHelper } from "../migration-helper.spec";
|
||||
|
||||
import { ProviderMigrator } from "./28-move-provider-state-to-state-provider";
|
||||
|
||||
function exampleProvider1() {
|
||||
return JSON.stringify({
|
||||
id: "id",
|
||||
name: "name",
|
||||
status: 0,
|
||||
type: 0,
|
||||
enabled: true,
|
||||
useEvents: true,
|
||||
});
|
||||
}
|
||||
|
||||
function exampleJSON() {
|
||||
return {
|
||||
global: {
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
authenticatedAccounts: ["user-1", "user-2"],
|
||||
"user-1": {
|
||||
data: {
|
||||
providers: {
|
||||
"provider-id-1": exampleProvider1(),
|
||||
"provider-id-2": {
|
||||
// ...
|
||||
},
|
||||
},
|
||||
otherStuff: "overStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
},
|
||||
"user-2": {
|
||||
data: {
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function rollbackJSON() {
|
||||
return {
|
||||
"user_user-1_providers_providers": {
|
||||
"provider-id-1": exampleProvider1(),
|
||||
"provider-id-2": {
|
||||
// ...
|
||||
},
|
||||
},
|
||||
"user_user-2_providers_providers": null as any,
|
||||
global: {
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
authenticatedAccounts: ["user-1", "user-2"],
|
||||
"user-1": {
|
||||
data: {
|
||||
otherStuff: "overStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
},
|
||||
"user-2": {
|
||||
data: {
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("ProviderMigrator", () => {
|
||||
let helper: MockProxy<MigrationHelper>;
|
||||
let sut: ProviderMigrator;
|
||||
const keyDefinitionLike = {
|
||||
key: "providers",
|
||||
stateDefinition: {
|
||||
name: "providers",
|
||||
},
|
||||
};
|
||||
|
||||
describe("migrate", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(exampleJSON(), 28);
|
||||
sut = new ProviderMigrator(27, 28);
|
||||
});
|
||||
|
||||
it("should remove providers from all accounts", async () => {
|
||||
await sut.migrate(helper);
|
||||
expect(helper.set).toHaveBeenCalledWith("user-1", {
|
||||
data: {
|
||||
otherStuff: "overStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
});
|
||||
});
|
||||
|
||||
it("should set providers value for each account", async () => {
|
||||
await sut.migrate(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("user-1", keyDefinitionLike, {
|
||||
"provider-id-1": exampleProvider1(),
|
||||
"provider-id-2": {
|
||||
// ...
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("rollback", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(rollbackJSON(), 27);
|
||||
sut = new ProviderMigrator(27, 28);
|
||||
});
|
||||
|
||||
it.each(["user-1", "user-2"])("should null out new values", async (userId) => {
|
||||
await sut.rollback(helper);
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(userId, keyDefinitionLike, null);
|
||||
});
|
||||
|
||||
it("should add explicit value back to accounts", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.set).toHaveBeenCalledWith("user-1", {
|
||||
data: {
|
||||
providers: {
|
||||
"provider-id-1": exampleProvider1(),
|
||||
"provider-id-2": {
|
||||
// ...
|
||||
},
|
||||
},
|
||||
otherStuff: "overStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
});
|
||||
});
|
||||
|
||||
it("should not try to restore values to missing accounts", async () => {
|
||||
await sut.rollback(helper);
|
||||
expect(helper.set).not.toHaveBeenCalledWith("user-3", any());
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,77 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
|
||||
import { Migrator } from "../migrator";
|
||||
|
||||
// FIXME: update to use a const object instead of a typescript enum
|
||||
// eslint-disable-next-line @bitwarden/platform/no-enums
|
||||
enum ProviderUserStatusType {
|
||||
Invited = 0,
|
||||
Accepted = 1,
|
||||
Confirmed = 2,
|
||||
Revoked = -1,
|
||||
}
|
||||
|
||||
// FIXME: update to use a const object instead of a typescript enum
|
||||
// eslint-disable-next-line @bitwarden/platform/no-enums
|
||||
enum ProviderUserType {
|
||||
ProviderAdmin = 0,
|
||||
ServiceUser = 1,
|
||||
}
|
||||
|
||||
type ProviderData = {
|
||||
id: string;
|
||||
name: string;
|
||||
status: ProviderUserStatusType;
|
||||
type: ProviderUserType;
|
||||
enabled: boolean;
|
||||
userId: string;
|
||||
useEvents: boolean;
|
||||
};
|
||||
|
||||
type ExpectedAccountType = {
|
||||
data?: {
|
||||
providers?: Record<string, Jsonify<ProviderData>>;
|
||||
};
|
||||
};
|
||||
|
||||
const USER_PROVIDERS: KeyDefinitionLike = {
|
||||
key: "providers",
|
||||
stateDefinition: {
|
||||
name: "providers",
|
||||
},
|
||||
};
|
||||
|
||||
export class ProviderMigrator extends Migrator<27, 28> {
|
||||
async migrate(helper: MigrationHelper): Promise<void> {
|
||||
const accounts = await helper.getAccounts<ExpectedAccountType>();
|
||||
async function migrateAccount(userId: string, account: ExpectedAccountType): Promise<void> {
|
||||
const value = account?.data?.providers;
|
||||
if (value != null) {
|
||||
await helper.setToUser(userId, USER_PROVIDERS, value);
|
||||
delete account.data.providers;
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(accounts.map(({ userId, account }) => migrateAccount(userId, account)));
|
||||
}
|
||||
|
||||
async rollback(helper: MigrationHelper): Promise<void> {
|
||||
const accounts = await helper.getAccounts<ExpectedAccountType>();
|
||||
async function rollbackAccount(userId: string, account: ExpectedAccountType): Promise<void> {
|
||||
const value = await helper.getFromUser(userId, USER_PROVIDERS);
|
||||
if (account) {
|
||||
account.data = Object.assign(account.data ?? {}, {
|
||||
providers: value,
|
||||
});
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
await helper.setToUser(userId, USER_PROVIDERS, null);
|
||||
}
|
||||
|
||||
await Promise.all(accounts.map(({ userId, account }) => rollbackAccount(userId, account)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import { MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { StateDefinitionLike, MigrationHelper } from "../migration-helper";
|
||||
import { mockMigrationHelper } from "../migration-helper.spec";
|
||||
|
||||
import { UserNotificationSettingsKeyMigrator } from "./29-move-user-notification-settings-to-state-provider";
|
||||
|
||||
function exampleJSON() {
|
||||
return {
|
||||
global: {
|
||||
disableAddLoginNotification: false,
|
||||
disableChangedPasswordNotification: false,
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function rollbackJSON() {
|
||||
return {
|
||||
global_userNotificationSettings_enableAddedLoginPrompt: true,
|
||||
global_userNotificationSettings_enableChangedPasswordPrompt: true,
|
||||
global: {
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const userNotificationSettingsLocalStateDefinition: {
|
||||
stateDefinition: StateDefinitionLike;
|
||||
} = {
|
||||
stateDefinition: {
|
||||
name: "userNotificationSettings",
|
||||
},
|
||||
};
|
||||
|
||||
describe("ProviderKeysMigrator", () => {
|
||||
let helper: MockProxy<MigrationHelper>;
|
||||
let sut: UserNotificationSettingsKeyMigrator;
|
||||
|
||||
describe("migrate", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(exampleJSON(), 28);
|
||||
sut = new UserNotificationSettingsKeyMigrator(28, 29);
|
||||
});
|
||||
|
||||
it("should remove disableAddLoginNotification and disableChangedPasswordNotification global setting", async () => {
|
||||
await sut.migrate(helper);
|
||||
expect(helper.set).toHaveBeenCalledTimes(2);
|
||||
expect(helper.set).toHaveBeenCalledWith("global", { otherStuff: "otherStuff1" });
|
||||
expect(helper.set).toHaveBeenCalledWith("global", { otherStuff: "otherStuff1" });
|
||||
});
|
||||
|
||||
it("should set global user notification setting values", async () => {
|
||||
await sut.migrate(helper);
|
||||
|
||||
expect(helper.setToGlobal).toHaveBeenCalledTimes(2);
|
||||
expect(helper.setToGlobal).toHaveBeenCalledWith(
|
||||
{ ...userNotificationSettingsLocalStateDefinition, key: "enableAddedLoginPrompt" },
|
||||
true,
|
||||
);
|
||||
expect(helper.setToGlobal).toHaveBeenCalledWith(
|
||||
{ ...userNotificationSettingsLocalStateDefinition, key: "enableChangedPasswordPrompt" },
|
||||
true,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("rollback", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(rollbackJSON(), 29);
|
||||
sut = new UserNotificationSettingsKeyMigrator(28, 29);
|
||||
});
|
||||
|
||||
it("should null out new global values", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.setToGlobal).toHaveBeenCalledTimes(2);
|
||||
expect(helper.setToGlobal).toHaveBeenCalledWith(
|
||||
{ ...userNotificationSettingsLocalStateDefinition, key: "enableAddedLoginPrompt" },
|
||||
null,
|
||||
);
|
||||
expect(helper.setToGlobal).toHaveBeenCalledWith(
|
||||
{ ...userNotificationSettingsLocalStateDefinition, key: "enableChangedPasswordPrompt" },
|
||||
null,
|
||||
);
|
||||
});
|
||||
|
||||
it("should add explicit global values back", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.set).toHaveBeenCalledTimes(2);
|
||||
expect(helper.set).toHaveBeenCalledWith("global", {
|
||||
disableAddLoginNotification: false,
|
||||
otherStuff: "otherStuff1",
|
||||
});
|
||||
expect(helper.set).toHaveBeenCalledWith("global", {
|
||||
disableChangedPasswordNotification: false,
|
||||
otherStuff: "otherStuff1",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,105 @@
|
||||
import { MigrationHelper } from "../migration-helper";
|
||||
import { Migrator } from "../migrator";
|
||||
|
||||
type ExpectedGlobalState = {
|
||||
disableAddLoginNotification?: boolean;
|
||||
disableChangedPasswordNotification?: boolean;
|
||||
};
|
||||
|
||||
export class UserNotificationSettingsKeyMigrator extends Migrator<28, 29> {
|
||||
async migrate(helper: MigrationHelper): Promise<void> {
|
||||
const globalState = await helper.get<ExpectedGlobalState>("global");
|
||||
|
||||
// disableAddLoginNotification -> enableAddedLoginPrompt
|
||||
if (globalState?.disableAddLoginNotification != null) {
|
||||
await helper.setToGlobal(
|
||||
{
|
||||
stateDefinition: {
|
||||
name: "userNotificationSettings",
|
||||
},
|
||||
key: "enableAddedLoginPrompt",
|
||||
},
|
||||
!globalState.disableAddLoginNotification,
|
||||
);
|
||||
|
||||
// delete `disableAddLoginNotification` from state global
|
||||
delete globalState.disableAddLoginNotification;
|
||||
|
||||
await helper.set<ExpectedGlobalState>("global", globalState);
|
||||
}
|
||||
|
||||
// disableChangedPasswordNotification -> enableChangedPasswordPrompt
|
||||
if (globalState?.disableChangedPasswordNotification != null) {
|
||||
await helper.setToGlobal(
|
||||
{
|
||||
stateDefinition: {
|
||||
name: "userNotificationSettings",
|
||||
},
|
||||
key: "enableChangedPasswordPrompt",
|
||||
},
|
||||
!globalState.disableChangedPasswordNotification,
|
||||
);
|
||||
|
||||
// delete `disableChangedPasswordNotification` from state global
|
||||
delete globalState.disableChangedPasswordNotification;
|
||||
|
||||
await helper.set<ExpectedGlobalState>("global", globalState);
|
||||
}
|
||||
}
|
||||
|
||||
async rollback(helper: MigrationHelper): Promise<void> {
|
||||
const globalState = (await helper.get<ExpectedGlobalState>("global")) || {};
|
||||
|
||||
const enableAddedLoginPrompt: boolean = await helper.getFromGlobal({
|
||||
stateDefinition: {
|
||||
name: "userNotificationSettings",
|
||||
},
|
||||
key: "enableAddedLoginPrompt",
|
||||
});
|
||||
|
||||
const enableChangedPasswordPrompt: boolean = await helper.getFromGlobal({
|
||||
stateDefinition: {
|
||||
name: "userNotificationSettings",
|
||||
},
|
||||
key: "enableChangedPasswordPrompt",
|
||||
});
|
||||
|
||||
// enableAddedLoginPrompt -> disableAddLoginNotification
|
||||
if (enableAddedLoginPrompt) {
|
||||
await helper.set<ExpectedGlobalState>("global", {
|
||||
...globalState,
|
||||
disableAddLoginNotification: !enableAddedLoginPrompt,
|
||||
});
|
||||
|
||||
// remove the global state provider framework key for `enableAddedLoginPrompt`
|
||||
await helper.setToGlobal(
|
||||
{
|
||||
stateDefinition: {
|
||||
name: "userNotificationSettings",
|
||||
},
|
||||
key: "enableAddedLoginPrompt",
|
||||
},
|
||||
null,
|
||||
);
|
||||
}
|
||||
|
||||
// enableChangedPasswordPrompt -> disableChangedPasswordNotification
|
||||
if (enableChangedPasswordPrompt) {
|
||||
await helper.set<ExpectedGlobalState>("global", {
|
||||
...globalState,
|
||||
disableChangedPasswordNotification: !enableChangedPasswordPrompt,
|
||||
});
|
||||
|
||||
// remove the global state provider framework key for `enableChangedPasswordPrompt`
|
||||
await helper.setToGlobal(
|
||||
{
|
||||
stateDefinition: {
|
||||
name: "userNotificationSettings",
|
||||
},
|
||||
key: "enableChangedPasswordPrompt",
|
||||
},
|
||||
null,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
import { MockProxy, any } from "jest-mock-extended";
|
||||
|
||||
import { MigrationHelper } from "../migration-helper";
|
||||
import { mockMigrationHelper } from "../migration-helper.spec";
|
||||
|
||||
import { PolicyMigrator } from "./30-move-policy-state-to-state-provider";
|
||||
|
||||
function exampleJSON() {
|
||||
return {
|
||||
global: {
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
authenticatedAccounts: ["user-1", "user-2"],
|
||||
"user-1": {
|
||||
data: {
|
||||
policies: {
|
||||
encrypted: {
|
||||
"policy-1": {
|
||||
id: "policy-1",
|
||||
organizationId: "fe1ff6ef-d2d4-49f3-9c07-b0c7013998f9",
|
||||
type: 9, // max vault timeout
|
||||
enabled: true,
|
||||
data: {
|
||||
hours: 1,
|
||||
minutes: 30,
|
||||
action: "lock",
|
||||
},
|
||||
},
|
||||
"policy-2": {
|
||||
id: "policy-2",
|
||||
organizationId: "5f277723-6391-4b5c-add9-b0c200ee6967",
|
||||
type: 3, // single org
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
},
|
||||
"user-2": {
|
||||
data: {
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function rollbackJSON() {
|
||||
return {
|
||||
"user_user-1_policies_policies": {
|
||||
"policy-1": {
|
||||
id: "policy-1",
|
||||
organizationId: "fe1ff6ef-d2d4-49f3-9c07-b0c7013998f9",
|
||||
type: 9,
|
||||
enabled: true,
|
||||
data: {
|
||||
hours: 1,
|
||||
minutes: 30,
|
||||
action: "lock",
|
||||
},
|
||||
},
|
||||
"policy-2": {
|
||||
id: "policy-2",
|
||||
organizationId: "5f277723-6391-4b5c-add9-b0c200ee6967",
|
||||
type: 3,
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
"user_user-2_policies_policies": null as any,
|
||||
global: {
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
authenticatedAccounts: ["user-1", "user-2"],
|
||||
"user-1": {
|
||||
data: {
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
},
|
||||
"user-2": {
|
||||
data: {
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("PoliciesMigrator", () => {
|
||||
let helper: MockProxy<MigrationHelper>;
|
||||
let sut: PolicyMigrator;
|
||||
const keyDefinitionLike = {
|
||||
key: "policies",
|
||||
stateDefinition: {
|
||||
name: "policies",
|
||||
},
|
||||
};
|
||||
|
||||
describe("migrate", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(exampleJSON(), 22);
|
||||
sut = new PolicyMigrator(29, 30);
|
||||
});
|
||||
|
||||
it("should remove policies from all old accounts", async () => {
|
||||
await sut.migrate(helper);
|
||||
expect(helper.set).toHaveBeenCalledWith("user-1", {
|
||||
data: {
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
});
|
||||
});
|
||||
|
||||
it("should set policies value in StateProvider framework for each account", async () => {
|
||||
await sut.migrate(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("user-1", keyDefinitionLike, {
|
||||
"policy-1": {
|
||||
id: "policy-1",
|
||||
organizationId: "fe1ff6ef-d2d4-49f3-9c07-b0c7013998f9",
|
||||
type: 9,
|
||||
enabled: true,
|
||||
data: {
|
||||
hours: 1,
|
||||
minutes: 30,
|
||||
action: "lock",
|
||||
},
|
||||
},
|
||||
"policy-2": {
|
||||
id: "policy-2",
|
||||
organizationId: "5f277723-6391-4b5c-add9-b0c200ee6967",
|
||||
type: 3,
|
||||
enabled: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("rollback", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(rollbackJSON(), 23);
|
||||
sut = new PolicyMigrator(29, 30);
|
||||
});
|
||||
|
||||
it.each(["user-1", "user-2"])("should null out new values", async (userId) => {
|
||||
await sut.rollback(helper);
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(userId, keyDefinitionLike, null);
|
||||
});
|
||||
|
||||
it("should add policy values back to accounts", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.set).toHaveBeenCalled();
|
||||
expect(helper.set).toHaveBeenCalledWith("user-1", {
|
||||
data: {
|
||||
policies: {
|
||||
encrypted: {
|
||||
"policy-1": {
|
||||
id: "policy-1",
|
||||
organizationId: "fe1ff6ef-d2d4-49f3-9c07-b0c7013998f9",
|
||||
type: 9,
|
||||
enabled: true,
|
||||
data: {
|
||||
hours: 1,
|
||||
minutes: 30,
|
||||
action: "lock",
|
||||
},
|
||||
},
|
||||
"policy-2": {
|
||||
id: "policy-2",
|
||||
organizationId: "5f277723-6391-4b5c-add9-b0c200ee6967",
|
||||
type: 3,
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
});
|
||||
});
|
||||
|
||||
it("should not try to restore values to missing accounts", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.set).not.toHaveBeenCalledWith("user-3", any());
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,80 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
|
||||
import { Migrator } from "../migrator";
|
||||
|
||||
// FIXME: update to use a const object instead of a typescript enum
|
||||
// eslint-disable-next-line @bitwarden/platform/no-enums
|
||||
enum PolicyType {
|
||||
TwoFactorAuthentication = 0, // Requires users to have 2fa enabled
|
||||
MasterPassword = 1, // Sets minimum requirements for master password complexity
|
||||
PasswordGenerator = 2, // Sets minimum requirements/default type for generated passwords/passphrases
|
||||
SingleOrg = 3, // Allows users to only be apart of one organization
|
||||
RequireSso = 4, // Requires users to authenticate with SSO
|
||||
PersonalOwnership = 5, // Disables personal vault ownership for adding/cloning items
|
||||
DisableSend = 6, // Disables the ability to create and edit Bitwarden Sends
|
||||
SendOptions = 7, // Sets restrictions or defaults for Bitwarden Sends
|
||||
ResetPassword = 8, // Allows orgs to use reset password : also can enable auto-enrollment during invite flow
|
||||
MaximumVaultTimeout = 9, // Sets the maximum allowed vault timeout
|
||||
DisablePersonalVaultExport = 10, // Disable personal vault export
|
||||
ActivateAutofill = 11, // Activates autofill with page load on the browser extension
|
||||
}
|
||||
|
||||
type PolicyDataType = {
|
||||
id: string;
|
||||
organizationId: string;
|
||||
type: PolicyType;
|
||||
data: Record<string, string | number | boolean>;
|
||||
enabled: boolean;
|
||||
};
|
||||
|
||||
type ExpectedAccountType = {
|
||||
data?: {
|
||||
policies?: {
|
||||
encrypted?: Record<string, PolicyDataType>;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
const POLICIES_KEY: KeyDefinitionLike = {
|
||||
key: "policies",
|
||||
stateDefinition: {
|
||||
name: "policies",
|
||||
},
|
||||
};
|
||||
|
||||
export class PolicyMigrator extends Migrator<29, 30> {
|
||||
async migrate(helper: MigrationHelper): Promise<void> {
|
||||
const accounts = await helper.getAccounts<ExpectedAccountType>();
|
||||
|
||||
async function migrateAccount(userId: string, account: ExpectedAccountType): Promise<void> {
|
||||
const value = account?.data?.policies?.encrypted;
|
||||
if (value != null) {
|
||||
await helper.setToUser(userId, POLICIES_KEY, value);
|
||||
delete account.data.policies;
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(accounts.map(({ userId, account }) => migrateAccount(userId, account)));
|
||||
}
|
||||
|
||||
async rollback(helper: MigrationHelper): Promise<void> {
|
||||
const accounts = await helper.getAccounts<ExpectedAccountType>();
|
||||
|
||||
async function rollbackAccount(userId: string, account: ExpectedAccountType): Promise<void> {
|
||||
const value = await helper.getFromUser(userId, POLICIES_KEY);
|
||||
if (account) {
|
||||
account.data = Object.assign(account.data ?? {}, {
|
||||
policies: {
|
||||
encrypted: value,
|
||||
},
|
||||
});
|
||||
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
await helper.setToUser(userId, POLICIES_KEY, null);
|
||||
}
|
||||
await Promise.all(accounts.map(({ userId, account }) => rollbackAccount(userId, account)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import { any, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
|
||||
import { mockMigrationHelper } from "../migration-helper.spec";
|
||||
|
||||
import { EnableContextMenuMigrator } from "./31-move-enable-context-menu-to-autofill-settings-state-provider";
|
||||
|
||||
function exampleJSON() {
|
||||
return {
|
||||
global: {
|
||||
disableContextMenuItem: true,
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
otherStuff: "otherStuff2",
|
||||
};
|
||||
}
|
||||
|
||||
function rollbackJSON() {
|
||||
return {
|
||||
global_autofillSettings_enableContextMenu: false,
|
||||
global: {
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
otherStuff: "otherStuff2",
|
||||
};
|
||||
}
|
||||
|
||||
const enableContextMenuKeyDefinition: KeyDefinitionLike = {
|
||||
stateDefinition: {
|
||||
name: "autofillSettings",
|
||||
},
|
||||
key: "enableContextMenu",
|
||||
};
|
||||
|
||||
describe("EnableContextMenuMigrator", () => {
|
||||
let helper: MockProxy<MigrationHelper>;
|
||||
let sut: EnableContextMenuMigrator;
|
||||
|
||||
describe("migrate", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(exampleJSON(), 30);
|
||||
sut = new EnableContextMenuMigrator(30, 31);
|
||||
});
|
||||
|
||||
it("should remove global disableContextMenuItem setting", async () => {
|
||||
await sut.migrate(helper);
|
||||
|
||||
expect(helper.set).toHaveBeenCalledTimes(1);
|
||||
expect(helper.set).toHaveBeenCalledWith("global", {
|
||||
otherStuff: "otherStuff1",
|
||||
});
|
||||
});
|
||||
|
||||
it("should set enableContextMenu globally", async () => {
|
||||
await sut.migrate(helper);
|
||||
|
||||
expect(helper.setToGlobal).toHaveBeenCalledTimes(1);
|
||||
expect(helper.setToGlobal).toHaveBeenCalledWith(enableContextMenuKeyDefinition, false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("rollback", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(rollbackJSON(), 31);
|
||||
sut = new EnableContextMenuMigrator(30, 31);
|
||||
});
|
||||
|
||||
it("should null out new enableContextMenu global value", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.setToGlobal).toHaveBeenCalledTimes(1);
|
||||
expect(helper.setToGlobal).toHaveBeenCalledWith(enableContextMenuKeyDefinition, null);
|
||||
});
|
||||
|
||||
it("should add disableContextMenuItem global value back", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.set).toHaveBeenCalledTimes(1);
|
||||
expect(helper.set).toHaveBeenCalledWith("global", {
|
||||
disableContextMenuItem: true,
|
||||
otherStuff: "otherStuff1",
|
||||
});
|
||||
});
|
||||
|
||||
it("should not try to restore values to missing accounts", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.set).not.toHaveBeenCalledWith("user-3", any());
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
|
||||
import { Migrator } from "../migrator";
|
||||
|
||||
type ExpectedGlobalState = {
|
||||
disableContextMenuItem?: boolean;
|
||||
};
|
||||
|
||||
const enableContextMenuKeyDefinition: KeyDefinitionLike = {
|
||||
stateDefinition: {
|
||||
name: "autofillSettings",
|
||||
},
|
||||
key: "enableContextMenu",
|
||||
};
|
||||
|
||||
export class EnableContextMenuMigrator extends Migrator<30, 31> {
|
||||
async migrate(helper: MigrationHelper): Promise<void> {
|
||||
const globalState = await helper.get<ExpectedGlobalState>("global");
|
||||
|
||||
// disableContextMenuItem -> enableContextMenu
|
||||
if (globalState?.disableContextMenuItem != null) {
|
||||
await helper.setToGlobal(enableContextMenuKeyDefinition, !globalState.disableContextMenuItem);
|
||||
|
||||
// delete `disableContextMenuItem` from state global
|
||||
delete globalState.disableContextMenuItem;
|
||||
|
||||
await helper.set<ExpectedGlobalState>("global", globalState);
|
||||
}
|
||||
}
|
||||
|
||||
async rollback(helper: MigrationHelper): Promise<void> {
|
||||
const globalState = (await helper.get<ExpectedGlobalState>("global")) || {};
|
||||
|
||||
const enableContextMenu: boolean = await helper.getFromGlobal(enableContextMenuKeyDefinition);
|
||||
|
||||
// enableContextMenu -> disableContextMenuItem
|
||||
if (enableContextMenu != null) {
|
||||
await helper.set<ExpectedGlobalState>("global", {
|
||||
...globalState,
|
||||
disableContextMenuItem: !enableContextMenu,
|
||||
});
|
||||
|
||||
// remove the global state provider framework key for `enableContextMenu`
|
||||
await helper.setToGlobal(enableContextMenuKeyDefinition, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import { MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { MigrationHelper } from "../migration-helper";
|
||||
import { mockMigrationHelper } from "../migration-helper.spec";
|
||||
|
||||
import { LOCALE_KEY, PreferredLanguageMigrator } from "./32-move-preferred-language";
|
||||
|
||||
function exampleJSON() {
|
||||
return {
|
||||
global: {
|
||||
locale: "en",
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
otherStuff: "otherStuff2",
|
||||
};
|
||||
}
|
||||
|
||||
function rollbackJSON() {
|
||||
return {
|
||||
global_translation_locale: "en",
|
||||
global: {
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
otherStuff: "otherStuff2",
|
||||
};
|
||||
}
|
||||
|
||||
describe("PreferredLanguageMigrator", () => {
|
||||
let helper: MockProxy<MigrationHelper>;
|
||||
let sut: PreferredLanguageMigrator;
|
||||
|
||||
describe("migrate", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(exampleJSON(), 31);
|
||||
sut = new PreferredLanguageMigrator(31, 32);
|
||||
});
|
||||
|
||||
it("should remove locale setting from global", async () => {
|
||||
await sut.migrate(helper);
|
||||
expect(helper.set).toHaveBeenCalledTimes(1);
|
||||
expect(helper.set).toHaveBeenCalledWith("global", {
|
||||
otherStuff: "otherStuff1",
|
||||
});
|
||||
});
|
||||
|
||||
it("should set locale for global state provider", async () => {
|
||||
await sut.migrate(helper);
|
||||
|
||||
expect(helper.setToGlobal).toHaveBeenCalledTimes(1);
|
||||
expect(helper.setToGlobal).toHaveBeenCalledWith(LOCALE_KEY, "en");
|
||||
});
|
||||
});
|
||||
|
||||
describe("rollback", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(rollbackJSON(), 32);
|
||||
sut = new PreferredLanguageMigrator(31, 32);
|
||||
});
|
||||
|
||||
it("should null out new values for global", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.setToGlobal).toHaveBeenCalledTimes(1);
|
||||
expect(helper.setToGlobal).toHaveBeenCalledWith(LOCALE_KEY, null);
|
||||
});
|
||||
|
||||
it("should add locale back to the old global object", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.set).toHaveBeenCalledTimes(1);
|
||||
expect(helper.set).toHaveBeenCalledWith("global", {
|
||||
locale: "en",
|
||||
otherStuff: "otherStuff1",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
import { MigrationHelper } from "../migration-helper";
|
||||
import { Migrator } from "../migrator";
|
||||
|
||||
type ExpectedGlobal = {
|
||||
locale?: string;
|
||||
};
|
||||
|
||||
export const LOCALE_KEY = {
|
||||
key: "locale",
|
||||
stateDefinition: {
|
||||
name: "translation",
|
||||
},
|
||||
};
|
||||
|
||||
export class PreferredLanguageMigrator extends Migrator<31, 32> {
|
||||
async migrate(helper: MigrationHelper): Promise<void> {
|
||||
// global state
|
||||
const global = await helper.get<ExpectedGlobal>("global");
|
||||
if (!global?.locale) {
|
||||
return;
|
||||
}
|
||||
|
||||
await helper.setToGlobal(LOCALE_KEY, global.locale);
|
||||
delete global.locale;
|
||||
await helper.set("global", global);
|
||||
}
|
||||
|
||||
async rollback(helper: MigrationHelper): Promise<void> {
|
||||
const locale = await helper.getFromGlobal<string>(LOCALE_KEY);
|
||||
|
||||
if (!locale) {
|
||||
return;
|
||||
}
|
||||
const global = (await helper.get<ExpectedGlobal>("global")) ?? {};
|
||||
global.locale = locale;
|
||||
await helper.set("global", global);
|
||||
await helper.setToGlobal(LOCALE_KEY, null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
import { MockProxy, any } from "jest-mock-extended";
|
||||
|
||||
import { MigrationHelper } from "../migration-helper";
|
||||
import { mockMigrationHelper } from "../migration-helper.spec";
|
||||
|
||||
import {
|
||||
ANONYMOUS_APP_ID_KEY,
|
||||
APP_ID_KEY,
|
||||
AppIdMigrator,
|
||||
} from "./33-move-app-id-to-state-providers";
|
||||
|
||||
function exampleJSON() {
|
||||
return {
|
||||
appId: "appId",
|
||||
anonymousAppId: "anonymousAppId",
|
||||
otherStuff: "otherStuff1",
|
||||
};
|
||||
}
|
||||
|
||||
function missingAppIdJSON() {
|
||||
return {
|
||||
anonymousAppId: "anonymousAppId",
|
||||
otherStuff: "otherStuff1",
|
||||
};
|
||||
}
|
||||
|
||||
function missingAnonymousAppIdJSON() {
|
||||
return {
|
||||
appId: "appId",
|
||||
otherStuff: "otherStuff1",
|
||||
};
|
||||
}
|
||||
|
||||
function missingBothJSON() {
|
||||
return {
|
||||
otherStuff: "otherStuff1",
|
||||
};
|
||||
}
|
||||
|
||||
function rollbackJSON() {
|
||||
return {
|
||||
global_applicationId_appId: "appId",
|
||||
global_applicationId_anonymousAppId: "anonymousAppId",
|
||||
otherStuff: "otherStuff1",
|
||||
};
|
||||
}
|
||||
|
||||
describe("AppIdMigrator", () => {
|
||||
let helper: MockProxy<MigrationHelper>;
|
||||
let sut: AppIdMigrator;
|
||||
|
||||
describe("migrate with both ids", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(exampleJSON(), 32);
|
||||
sut = new AppIdMigrator(32, 33);
|
||||
});
|
||||
|
||||
it("removes appId", async () => {
|
||||
await sut.migrate(helper);
|
||||
expect(helper.set).toHaveBeenCalledWith("appId", null);
|
||||
});
|
||||
|
||||
it("removes anonymousAppId", async () => {
|
||||
await sut.migrate(helper);
|
||||
expect(helper.set).toHaveBeenCalledWith("anonymousAppId", null);
|
||||
});
|
||||
|
||||
it("sets appId", async () => {
|
||||
await sut.migrate(helper);
|
||||
expect(helper.setToGlobal).toHaveBeenCalledWith(APP_ID_KEY, "appId");
|
||||
});
|
||||
|
||||
it("sets anonymousAppId", async () => {
|
||||
await sut.migrate(helper);
|
||||
expect(helper.setToGlobal).toHaveBeenCalledWith(ANONYMOUS_APP_ID_KEY, "anonymousAppId");
|
||||
});
|
||||
});
|
||||
|
||||
describe("migrate with missing appId", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(missingAppIdJSON(), 32);
|
||||
sut = new AppIdMigrator(32, 33);
|
||||
});
|
||||
|
||||
it("does not set appId", async () => {
|
||||
await sut.migrate(helper);
|
||||
expect(helper.setToGlobal).not.toHaveBeenCalledWith(APP_ID_KEY, any());
|
||||
});
|
||||
|
||||
it("removes anonymousAppId", async () => {
|
||||
await sut.migrate(helper);
|
||||
expect(helper.set).toHaveBeenCalledWith("anonymousAppId", null);
|
||||
});
|
||||
|
||||
it("does not set appId", async () => {
|
||||
await sut.migrate(helper);
|
||||
expect(helper.setToGlobal).not.toHaveBeenCalledWith(APP_ID_KEY, any());
|
||||
});
|
||||
|
||||
it("sets anonymousAppId", async () => {
|
||||
await sut.migrate(helper);
|
||||
expect(helper.setToGlobal).toHaveBeenCalledWith(ANONYMOUS_APP_ID_KEY, "anonymousAppId");
|
||||
});
|
||||
});
|
||||
|
||||
describe("migrate with missing anonymousAppId", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(missingAnonymousAppIdJSON(), 32);
|
||||
sut = new AppIdMigrator(32, 33);
|
||||
});
|
||||
|
||||
it("sets appId", async () => {
|
||||
await sut.migrate(helper);
|
||||
expect(helper.setToGlobal).toHaveBeenCalledWith(APP_ID_KEY, "appId");
|
||||
});
|
||||
|
||||
it("does not set anonymousAppId", async () => {
|
||||
await sut.migrate(helper);
|
||||
expect(helper.setToGlobal).not.toHaveBeenCalledWith(ANONYMOUS_APP_ID_KEY, any());
|
||||
});
|
||||
|
||||
it("removes appId", async () => {
|
||||
await sut.migrate(helper);
|
||||
expect(helper.set).toHaveBeenCalledWith("appId", null);
|
||||
});
|
||||
|
||||
it("does not remove anonymousAppId", async () => {
|
||||
await sut.migrate(helper);
|
||||
expect(helper.set).not.toHaveBeenCalledWith("anonymousAppId", any());
|
||||
});
|
||||
});
|
||||
|
||||
describe("migrate with missing appId and anonymousAppId", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(missingBothJSON(), 32);
|
||||
sut = new AppIdMigrator(32, 33);
|
||||
});
|
||||
|
||||
it("does not set appId", async () => {
|
||||
await sut.migrate(helper);
|
||||
expect(helper.setToGlobal).not.toHaveBeenCalledWith(APP_ID_KEY, any());
|
||||
});
|
||||
|
||||
it("does not set anonymousAppId", async () => {
|
||||
await sut.migrate(helper);
|
||||
expect(helper.setToGlobal).not.toHaveBeenCalledWith(ANONYMOUS_APP_ID_KEY, any());
|
||||
});
|
||||
|
||||
it("does not remove appId", async () => {
|
||||
await sut.migrate(helper);
|
||||
expect(helper.set).not.toHaveBeenCalledWith("appId", any());
|
||||
});
|
||||
|
||||
it("does not remove anonymousAppId", async () => {
|
||||
await sut.migrate(helper);
|
||||
expect(helper.set).not.toHaveBeenCalledWith("anonymousAppId", any());
|
||||
});
|
||||
});
|
||||
|
||||
describe("rollback with both Ids", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(rollbackJSON(), 33);
|
||||
sut = new AppIdMigrator(32, 33);
|
||||
});
|
||||
|
||||
it("removes appId", async () => {
|
||||
await sut.rollback(helper);
|
||||
expect(helper.setToGlobal).toHaveBeenCalledWith(APP_ID_KEY, null);
|
||||
});
|
||||
|
||||
it("sets appId", async () => {
|
||||
await sut.rollback(helper);
|
||||
expect(helper.set).toHaveBeenCalledWith("appId", "appId");
|
||||
});
|
||||
|
||||
it("removes anonymousAppId", async () => {
|
||||
await sut.rollback(helper);
|
||||
expect(helper.setToGlobal).toHaveBeenCalledWith(ANONYMOUS_APP_ID_KEY, null);
|
||||
});
|
||||
|
||||
it("sets anonymousAppId", async () => {
|
||||
await sut.rollback(helper);
|
||||
expect(helper.set).toHaveBeenCalledWith("anonymousAppId", "anonymousAppId");
|
||||
});
|
||||
});
|
||||
|
||||
describe("rollback missing both Ids", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(missingBothJSON(), 33);
|
||||
sut = new AppIdMigrator(32, 33);
|
||||
});
|
||||
|
||||
it("does not set appId for providers", async () => {
|
||||
await sut.rollback(helper);
|
||||
expect(helper.setToGlobal).not.toHaveBeenCalledWith(APP_ID_KEY, any());
|
||||
});
|
||||
|
||||
it("does not set anonymousAppId for providers", async () => {
|
||||
await sut.rollback(helper);
|
||||
expect(helper.setToGlobal).not.toHaveBeenCalledWith(ANONYMOUS_APP_ID_KEY, any());
|
||||
});
|
||||
|
||||
it("does not revert appId", async () => {
|
||||
await sut.rollback(helper);
|
||||
expect(helper.set).not.toHaveBeenCalledWith("appId", any());
|
||||
});
|
||||
|
||||
it("does not revert anonymousAppId", async () => {
|
||||
await sut.rollback(helper);
|
||||
expect(helper.set).not.toHaveBeenCalledWith("anonymousAppId", any());
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
|
||||
import { Migrator } from "../migrator";
|
||||
|
||||
export const APP_ID_STORAGE_KEY = "appId";
|
||||
export const ANONYMOUS_APP_ID_STORAGE_KEY = "anonymousAppId";
|
||||
|
||||
export const APP_ID_KEY: KeyDefinitionLike = {
|
||||
key: APP_ID_STORAGE_KEY,
|
||||
stateDefinition: { name: "applicationId" },
|
||||
};
|
||||
|
||||
export const ANONYMOUS_APP_ID_KEY: KeyDefinitionLike = {
|
||||
key: ANONYMOUS_APP_ID_STORAGE_KEY,
|
||||
stateDefinition: { name: "applicationId" },
|
||||
};
|
||||
|
||||
export class AppIdMigrator extends Migrator<32, 33> {
|
||||
async migrate(helper: MigrationHelper): Promise<void> {
|
||||
const appId = await helper.get<string>(APP_ID_STORAGE_KEY);
|
||||
const anonymousAppId = await helper.get<string>(ANONYMOUS_APP_ID_STORAGE_KEY);
|
||||
|
||||
if (appId != null) {
|
||||
await helper.setToGlobal(APP_ID_KEY, appId);
|
||||
await helper.set(APP_ID_STORAGE_KEY, null);
|
||||
}
|
||||
|
||||
if (anonymousAppId != null) {
|
||||
await helper.setToGlobal(ANONYMOUS_APP_ID_KEY, anonymousAppId);
|
||||
await helper.set(ANONYMOUS_APP_ID_STORAGE_KEY, null);
|
||||
}
|
||||
}
|
||||
|
||||
async rollback(helper: MigrationHelper): Promise<void> {
|
||||
const appId = await helper.getFromGlobal<string>(APP_ID_KEY);
|
||||
const anonymousAppId = await helper.getFromGlobal<string>(ANONYMOUS_APP_ID_KEY);
|
||||
|
||||
if (appId != null) {
|
||||
await helper.set(APP_ID_STORAGE_KEY, appId);
|
||||
await helper.setToGlobal(APP_ID_KEY, null);
|
||||
}
|
||||
if (anonymousAppId != null) {
|
||||
await helper.set(ANONYMOUS_APP_ID_STORAGE_KEY, anonymousAppId);
|
||||
await helper.setToGlobal(ANONYMOUS_APP_ID_KEY, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
import { any, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { StateDefinitionLike, MigrationHelper } from "../migration-helper";
|
||||
import { mockMigrationHelper } from "../migration-helper.spec";
|
||||
|
||||
import { DomainSettingsMigrator } from "./34-move-domain-settings-to-state-providers";
|
||||
|
||||
const mockNeverDomains = { "bitwarden.test": null, locahost: null, "www.example.com": null } as {
|
||||
[key: string]: null;
|
||||
};
|
||||
|
||||
function exampleJSON() {
|
||||
return {
|
||||
global: {
|
||||
otherStuff: "otherStuff1",
|
||||
neverDomains: mockNeverDomains,
|
||||
},
|
||||
authenticatedAccounts: ["user-1", "user-2", "user-3"],
|
||||
"user-1": {
|
||||
settings: {
|
||||
defaultUriMatch: 3,
|
||||
settings: {
|
||||
equivalentDomains: [] as string[][],
|
||||
},
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
},
|
||||
"user-2": {
|
||||
settings: {
|
||||
settings: {
|
||||
equivalentDomains: [["apple.com", "icloud.com"]],
|
||||
},
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
},
|
||||
"user-3": {
|
||||
settings: {
|
||||
defaultUriMatch: 1,
|
||||
otherStuff: "otherStuff6",
|
||||
},
|
||||
otherStuff: "otherStuff7",
|
||||
},
|
||||
"user-4": {
|
||||
settings: {
|
||||
otherStuff: "otherStuff8",
|
||||
},
|
||||
otherStuff: "otherStuff9",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function rollbackJSON() {
|
||||
return {
|
||||
global_domainSettings_neverDomains: mockNeverDomains,
|
||||
"user_user-1_domainSettings_defaultUriMatchStrategy": 3,
|
||||
"user_user-1_domainSettings_equivalentDomains": [] as string[][],
|
||||
"user_user-2_domainSettings_equivalentDomains": [["apple.com", "icloud.com"]],
|
||||
"user_user-3_domainSettings_defaultUriMatchStrategy": 1,
|
||||
global: {
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
authenticatedAccounts: ["user-1", "user-2", "user-3"],
|
||||
"user-1": {
|
||||
settings: {
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
},
|
||||
"user-2": {
|
||||
settings: {
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
},
|
||||
"user-3": {
|
||||
settings: {
|
||||
otherStuff: "otherStuff6",
|
||||
},
|
||||
otherStuff: "otherStuff7",
|
||||
},
|
||||
"user-4": {
|
||||
settings: {
|
||||
otherStuff: "otherStuff8",
|
||||
},
|
||||
otherStuff: "otherStuff9",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const domainSettingsStateDefinition: {
|
||||
stateDefinition: StateDefinitionLike;
|
||||
} = {
|
||||
stateDefinition: {
|
||||
name: "domainSettings",
|
||||
},
|
||||
};
|
||||
|
||||
describe("DomainSettingsMigrator", () => {
|
||||
let helper: MockProxy<MigrationHelper>;
|
||||
let sut: DomainSettingsMigrator;
|
||||
|
||||
describe("migrate", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(exampleJSON(), 33);
|
||||
sut = new DomainSettingsMigrator(33, 34);
|
||||
});
|
||||
|
||||
it("should remove global neverDomains and defaultUriMatch and equivalentDomains settings from all accounts", async () => {
|
||||
await sut.migrate(helper);
|
||||
expect(helper.set).toHaveBeenCalledTimes(4);
|
||||
expect(helper.set).toHaveBeenCalledWith("global", {
|
||||
otherStuff: "otherStuff1",
|
||||
});
|
||||
expect(helper.set).toHaveBeenCalledWith("user-1", {
|
||||
settings: {
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
});
|
||||
expect(helper.set).toHaveBeenCalledWith("user-1", {
|
||||
settings: {
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
});
|
||||
expect(helper.set).toHaveBeenCalledWith("user-2", {
|
||||
settings: {
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
});
|
||||
expect(helper.set).toHaveBeenCalledWith("user-3", {
|
||||
settings: {
|
||||
otherStuff: "otherStuff6",
|
||||
},
|
||||
otherStuff: "otherStuff7",
|
||||
});
|
||||
});
|
||||
|
||||
it("should set global neverDomains and defaultUriMatchStrategy and equivalentDomains setting values for each account", async () => {
|
||||
await sut.migrate(helper);
|
||||
|
||||
expect(helper.setToGlobal).toHaveBeenCalledTimes(1);
|
||||
expect(helper.setToGlobal).toHaveBeenCalledWith(
|
||||
{ ...domainSettingsStateDefinition, key: "neverDomains" },
|
||||
mockNeverDomains,
|
||||
);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledTimes(4);
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(
|
||||
"user-1",
|
||||
{ ...domainSettingsStateDefinition, key: "defaultUriMatchStrategy" },
|
||||
3,
|
||||
);
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(
|
||||
"user-1",
|
||||
{ ...domainSettingsStateDefinition, key: "equivalentDomains" },
|
||||
[],
|
||||
);
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(
|
||||
"user-2",
|
||||
{ ...domainSettingsStateDefinition, key: "equivalentDomains" },
|
||||
[["apple.com", "icloud.com"]],
|
||||
);
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(
|
||||
"user-3",
|
||||
{ ...domainSettingsStateDefinition, key: "defaultUriMatchStrategy" },
|
||||
1,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("rollback", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(rollbackJSON(), 34);
|
||||
sut = new DomainSettingsMigrator(33, 34);
|
||||
});
|
||||
|
||||
it("should null out new values globally and for each account", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.setToGlobal).toHaveBeenCalledTimes(1);
|
||||
expect(helper.setToGlobal).toHaveBeenCalledWith(
|
||||
{ ...domainSettingsStateDefinition, key: "neverDomains" },
|
||||
null,
|
||||
);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledTimes(4);
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(
|
||||
"user-1",
|
||||
{ ...domainSettingsStateDefinition, key: "defaultUriMatchStrategy" },
|
||||
null,
|
||||
);
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(
|
||||
"user-1",
|
||||
{ ...domainSettingsStateDefinition, key: "equivalentDomains" },
|
||||
null,
|
||||
);
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(
|
||||
"user-2",
|
||||
{ ...domainSettingsStateDefinition, key: "equivalentDomains" },
|
||||
null,
|
||||
);
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(
|
||||
"user-3",
|
||||
{ ...domainSettingsStateDefinition, key: "defaultUriMatchStrategy" },
|
||||
null,
|
||||
);
|
||||
});
|
||||
|
||||
it("should add explicit value back to accounts", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.set).toHaveBeenCalledTimes(4);
|
||||
expect(helper.set).toHaveBeenCalledWith("global", {
|
||||
neverDomains: mockNeverDomains,
|
||||
otherStuff: "otherStuff1",
|
||||
});
|
||||
expect(helper.set).toHaveBeenCalledWith("user-1", {
|
||||
settings: {
|
||||
defaultUriMatch: 3,
|
||||
settings: {
|
||||
equivalentDomains: [] as string[][],
|
||||
},
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
});
|
||||
expect(helper.set).toHaveBeenCalledWith("user-2", {
|
||||
settings: {
|
||||
settings: {
|
||||
equivalentDomains: [["apple.com", "icloud.com"]],
|
||||
},
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
});
|
||||
expect(helper.set).toHaveBeenCalledWith("user-3", {
|
||||
settings: {
|
||||
defaultUriMatch: 1,
|
||||
otherStuff: "otherStuff6",
|
||||
},
|
||||
otherStuff: "otherStuff7",
|
||||
});
|
||||
});
|
||||
|
||||
it("should not try to restore values to missing accounts", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.set).not.toHaveBeenCalledWith("user-4", any());
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,171 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
|
||||
import { Migrator } from "../migrator";
|
||||
|
||||
// FIXME: Remove when updating file. Eslint update
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const UriMatchStrategy = {
|
||||
Domain: 0,
|
||||
Host: 1,
|
||||
StartsWith: 2,
|
||||
Exact: 3,
|
||||
RegularExpression: 4,
|
||||
Never: 5,
|
||||
} as const;
|
||||
|
||||
type UriMatchStrategySetting = (typeof UriMatchStrategy)[keyof typeof UriMatchStrategy];
|
||||
|
||||
type ExpectedAccountState = {
|
||||
settings?: {
|
||||
defaultUriMatch?: UriMatchStrategySetting;
|
||||
settings?: {
|
||||
equivalentDomains?: string[][];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
type ExpectedGlobalState = {
|
||||
neverDomains?: { [key: string]: null };
|
||||
};
|
||||
|
||||
const defaultUriMatchStrategyDefinition: KeyDefinitionLike = {
|
||||
stateDefinition: {
|
||||
name: "domainSettings",
|
||||
},
|
||||
key: "defaultUriMatchStrategy",
|
||||
};
|
||||
|
||||
const equivalentDomainsDefinition: KeyDefinitionLike = {
|
||||
stateDefinition: {
|
||||
name: "domainSettings",
|
||||
},
|
||||
key: "equivalentDomains",
|
||||
};
|
||||
|
||||
const neverDomainsDefinition: KeyDefinitionLike = {
|
||||
stateDefinition: {
|
||||
name: "domainSettings",
|
||||
},
|
||||
key: "neverDomains",
|
||||
};
|
||||
|
||||
export class DomainSettingsMigrator extends Migrator<33, 34> {
|
||||
async migrate(helper: MigrationHelper): Promise<void> {
|
||||
let updateAccount = false;
|
||||
|
||||
// global state ("neverDomains")
|
||||
const globalState = await helper.get<ExpectedGlobalState>("global");
|
||||
|
||||
if (globalState?.neverDomains != null) {
|
||||
await helper.setToGlobal(neverDomainsDefinition, globalState.neverDomains);
|
||||
|
||||
// delete `neverDomains` from state global
|
||||
delete globalState.neverDomains;
|
||||
|
||||
await helper.set<ExpectedGlobalState>("global", globalState);
|
||||
}
|
||||
|
||||
// account state ("defaultUriMatch" and "settings.equivalentDomains")
|
||||
const accounts = await helper.getAccounts<ExpectedAccountState>();
|
||||
|
||||
await Promise.all([...accounts.map(({ userId, account }) => migrateAccount(userId, account))]);
|
||||
|
||||
// migrate account state
|
||||
async function migrateAccount(userId: string, account: ExpectedAccountState): Promise<void> {
|
||||
const accountSettings = account?.settings;
|
||||
|
||||
if (accountSettings?.defaultUriMatch != undefined) {
|
||||
await helper.setToUser(
|
||||
userId,
|
||||
defaultUriMatchStrategyDefinition,
|
||||
accountSettings.defaultUriMatch,
|
||||
);
|
||||
delete account.settings.defaultUriMatch;
|
||||
|
||||
updateAccount = true;
|
||||
}
|
||||
|
||||
if (accountSettings?.settings?.equivalentDomains != undefined) {
|
||||
await helper.setToUser(
|
||||
userId,
|
||||
equivalentDomainsDefinition,
|
||||
accountSettings.settings.equivalentDomains,
|
||||
);
|
||||
delete account.settings.settings.equivalentDomains;
|
||||
delete account.settings.settings;
|
||||
|
||||
updateAccount = true;
|
||||
}
|
||||
|
||||
if (updateAccount) {
|
||||
// update the state account settings with the migrated values deleted
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async rollback(helper: MigrationHelper): Promise<void> {
|
||||
let updateAccount = false;
|
||||
|
||||
// global state ("neverDomains")
|
||||
const globalState = (await helper.get<ExpectedGlobalState>("global")) || {};
|
||||
const neverDomains: { [key: string]: null } =
|
||||
await helper.getFromGlobal(neverDomainsDefinition);
|
||||
|
||||
if (neverDomains != null) {
|
||||
await helper.set<ExpectedGlobalState>("global", {
|
||||
...globalState,
|
||||
neverDomains: neverDomains,
|
||||
});
|
||||
|
||||
// remove the global state provider framework key for `neverDomains`
|
||||
await helper.setToGlobal(neverDomainsDefinition, null);
|
||||
}
|
||||
|
||||
// account state ("defaultUriMatchStrategy" and "equivalentDomains")
|
||||
const accounts = await helper.getAccounts<ExpectedAccountState>();
|
||||
|
||||
await Promise.all([...accounts.map(({ userId, account }) => rollbackAccount(userId, account))]);
|
||||
|
||||
// rollback account state
|
||||
async function rollbackAccount(userId: string, account: ExpectedAccountState): Promise<void> {
|
||||
let settings = account?.settings || {};
|
||||
|
||||
const defaultUriMatchStrategy: UriMatchStrategySetting = await helper.getFromUser(
|
||||
userId,
|
||||
defaultUriMatchStrategyDefinition,
|
||||
);
|
||||
|
||||
const equivalentDomains: string[][] = await helper.getFromUser(
|
||||
userId,
|
||||
equivalentDomainsDefinition,
|
||||
);
|
||||
|
||||
// update new settings and remove the account state provider framework keys for the rolled back values
|
||||
if (defaultUriMatchStrategy != null) {
|
||||
settings = { ...settings, defaultUriMatch: defaultUriMatchStrategy };
|
||||
|
||||
await helper.setToUser(userId, defaultUriMatchStrategyDefinition, null);
|
||||
|
||||
updateAccount = true;
|
||||
}
|
||||
|
||||
if (equivalentDomains != null) {
|
||||
settings = { ...settings, settings: { equivalentDomains } };
|
||||
|
||||
await helper.setToUser(userId, equivalentDomainsDefinition, null);
|
||||
|
||||
updateAccount = true;
|
||||
}
|
||||
|
||||
// commit updated settings to state
|
||||
if (updateAccount) {
|
||||
await helper.set(userId, {
|
||||
...account,
|
||||
settings,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { runMigrator } from "../migration-helper.spec";
|
||||
|
||||
import { MoveThemeToStateProviderMigrator } from "./35-move-theme-to-state-providers";
|
||||
|
||||
describe("MoveThemeToStateProviders", () => {
|
||||
const sut = new MoveThemeToStateProviderMigrator(34, 35);
|
||||
|
||||
describe("migrate", () => {
|
||||
it("migrates global theme and deletes it", async () => {
|
||||
const output = await runMigrator(sut, {
|
||||
global: {
|
||||
theme: "dark",
|
||||
},
|
||||
});
|
||||
|
||||
expect(output).toEqual({
|
||||
global_theming_selection: "dark",
|
||||
global: {},
|
||||
});
|
||||
});
|
||||
|
||||
it.each([{}, null])(
|
||||
"doesn't touch it if global state looks like: '%s'",
|
||||
async (globalState) => {
|
||||
const output = await runMigrator(sut, {
|
||||
global: globalState,
|
||||
});
|
||||
|
||||
expect(output).toEqual({
|
||||
global: globalState,
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("rollback", () => {
|
||||
it("migrates state provider theme back to original location when no global", async () => {
|
||||
const output = await runMigrator(
|
||||
sut,
|
||||
{
|
||||
global_theming_selection: "disk",
|
||||
},
|
||||
"rollback",
|
||||
);
|
||||
|
||||
expect(output).toEqual({
|
||||
global: {
|
||||
theme: "disk",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("migrates state provider theme back to legacy location when there is an existing global object", async () => {
|
||||
const output = await runMigrator(
|
||||
sut,
|
||||
{
|
||||
global_theming_selection: "disk",
|
||||
global: {
|
||||
other: "stuff",
|
||||
},
|
||||
},
|
||||
"rollback",
|
||||
);
|
||||
|
||||
expect(output).toEqual({
|
||||
global: {
|
||||
theme: "disk",
|
||||
other: "stuff",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("does nothing if no theme in state provider location", async () => {
|
||||
const output = await runMigrator(sut, {}, "rollback");
|
||||
expect(output).toEqual({});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
|
||||
import { Migrator } from "../migrator";
|
||||
|
||||
type ExpectedGlobal = { theme?: string };
|
||||
|
||||
const THEME_SELECTION: KeyDefinitionLike = {
|
||||
key: "selection",
|
||||
stateDefinition: { name: "theming" },
|
||||
};
|
||||
|
||||
export class MoveThemeToStateProviderMigrator extends Migrator<34, 35> {
|
||||
async migrate(helper: MigrationHelper): Promise<void> {
|
||||
const legacyGlobalState = await helper.get<ExpectedGlobal>("global");
|
||||
const theme = legacyGlobalState?.theme;
|
||||
if (theme != null) {
|
||||
await helper.setToGlobal(THEME_SELECTION, theme);
|
||||
delete legacyGlobalState.theme;
|
||||
await helper.set("global", legacyGlobalState);
|
||||
}
|
||||
}
|
||||
|
||||
async rollback(helper: MigrationHelper): Promise<void> {
|
||||
const theme = await helper.getFromGlobal<string>(THEME_SELECTION);
|
||||
if (theme != null) {
|
||||
const legacyGlobal = (await helper.get<ExpectedGlobal>("global")) ?? {};
|
||||
legacyGlobal.theme = theme;
|
||||
await helper.set("global", legacyGlobal);
|
||||
await helper.removeFromGlobal(THEME_SELECTION);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
import { MockProxy, any } from "jest-mock-extended";
|
||||
|
||||
import { MigrationHelper, StateDefinitionLike } from "../migration-helper";
|
||||
import { mockMigrationHelper } from "../migration-helper.spec";
|
||||
|
||||
import { VaultSettingsKeyMigrator } from "./36-move-show-card-and-identity-to-state-provider";
|
||||
|
||||
function exampleJSON() {
|
||||
return {
|
||||
global: {
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
authenticatedAccounts: ["user-1", "user-2", "user-3"],
|
||||
"user-1": {
|
||||
settings: {
|
||||
dontShowCardsCurrentTab: true,
|
||||
dontShowIdentitiesCurrentTab: true,
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
},
|
||||
"user-2": {
|
||||
settings: {
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function rollbackJSON() {
|
||||
return {
|
||||
"user_user-1_vaultSettings_showCardsCurrentTab": true,
|
||||
"user_user-1_vaultSettings_showIdentitiesCurrentTab": true,
|
||||
global: {
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
authenticatedAccounts: ["user-1", "user-2", "user-3"],
|
||||
"user-1": {
|
||||
settings: {
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
},
|
||||
"user-2": {
|
||||
settings: {
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const vaultSettingsStateDefinition: {
|
||||
stateDefinition: StateDefinitionLike;
|
||||
} = {
|
||||
stateDefinition: {
|
||||
name: "vaultSettings",
|
||||
},
|
||||
};
|
||||
|
||||
describe("VaultSettingsKeyMigrator", () => {
|
||||
let helper: MockProxy<MigrationHelper>;
|
||||
let sut: VaultSettingsKeyMigrator;
|
||||
|
||||
describe("migrate", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(exampleJSON(), 35);
|
||||
sut = new VaultSettingsKeyMigrator(35, 36);
|
||||
});
|
||||
|
||||
it("should remove dontShowCardsCurrentTab and dontShowIdentitiesCurrentTab from all accounts", async () => {
|
||||
await sut.migrate(helper);
|
||||
expect(helper.set).toHaveBeenCalledTimes(1);
|
||||
expect(helper.set).toHaveBeenCalledWith("user-1", {
|
||||
settings: {
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
});
|
||||
});
|
||||
|
||||
it("should set showCardsCurrentTab and showIdentitiesCurrentTab values for each account", async () => {
|
||||
await sut.migrate(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledTimes(2);
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(
|
||||
"user-1",
|
||||
{ ...vaultSettingsStateDefinition, key: "showCardsCurrentTab" },
|
||||
false,
|
||||
);
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(
|
||||
"user-1",
|
||||
{ ...vaultSettingsStateDefinition, key: "showIdentitiesCurrentTab" },
|
||||
false,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("rollback", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(rollbackJSON(), 36);
|
||||
sut = new VaultSettingsKeyMigrator(35, 36);
|
||||
});
|
||||
|
||||
it("should null out new values for each account", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledTimes(2);
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(
|
||||
"user-1",
|
||||
{ ...vaultSettingsStateDefinition, key: "showCardsCurrentTab" },
|
||||
null,
|
||||
);
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(
|
||||
"user-1",
|
||||
{ ...vaultSettingsStateDefinition, key: "showIdentitiesCurrentTab" },
|
||||
null,
|
||||
);
|
||||
});
|
||||
|
||||
it("should add explicit value back to accounts", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.set).toHaveBeenCalledTimes(1);
|
||||
expect(helper.set).toHaveBeenCalledWith("user-1", {
|
||||
settings: {
|
||||
otherStuff: "otherStuff2",
|
||||
dontShowCardsCurrentTab: false,
|
||||
dontShowIdentitiesCurrentTab: false,
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
});
|
||||
});
|
||||
|
||||
it("should not try to restore values to missing accounts", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.set).not.toHaveBeenCalledWith("user-3", any());
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,107 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { MigrationHelper, StateDefinitionLike } from "../migration-helper";
|
||||
import { Migrator } from "../migrator";
|
||||
|
||||
type ExpectedAccountState = {
|
||||
settings?: {
|
||||
dontShowCardsCurrentTab?: boolean;
|
||||
dontShowIdentitiesCurrentTab?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
const vaultSettingsStateDefinition: {
|
||||
stateDefinition: StateDefinitionLike;
|
||||
} = {
|
||||
stateDefinition: {
|
||||
name: "vaultSettings",
|
||||
},
|
||||
};
|
||||
|
||||
export class VaultSettingsKeyMigrator extends Migrator<35, 36> {
|
||||
async migrate(helper: MigrationHelper): Promise<void> {
|
||||
const accounts = await helper.getAccounts<ExpectedAccountState>();
|
||||
|
||||
await Promise.all([...accounts.map(({ userId, account }) => migrateAccount(userId, account))]);
|
||||
|
||||
async function migrateAccount(userId: string, account: ExpectedAccountState): Promise<void> {
|
||||
let updateAccount = false;
|
||||
const accountSettings = account?.settings;
|
||||
|
||||
if (accountSettings?.dontShowCardsCurrentTab != null) {
|
||||
await helper.setToUser(
|
||||
userId,
|
||||
{ ...vaultSettingsStateDefinition, key: "showCardsCurrentTab" },
|
||||
!accountSettings.dontShowCardsCurrentTab,
|
||||
);
|
||||
delete account.settings.dontShowCardsCurrentTab;
|
||||
updateAccount = true;
|
||||
}
|
||||
|
||||
if (accountSettings?.dontShowIdentitiesCurrentTab != null) {
|
||||
await helper.setToUser(
|
||||
userId,
|
||||
{ ...vaultSettingsStateDefinition, key: "showIdentitiesCurrentTab" },
|
||||
!accountSettings.dontShowIdentitiesCurrentTab,
|
||||
);
|
||||
delete account.settings.dontShowIdentitiesCurrentTab;
|
||||
updateAccount = true;
|
||||
}
|
||||
|
||||
if (updateAccount) {
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async rollback(helper: MigrationHelper): Promise<void> {
|
||||
const accounts = await helper.getAccounts<ExpectedAccountState>();
|
||||
|
||||
await Promise.all([...accounts.map(({ userId, account }) => rollbackAccount(userId, account))]);
|
||||
|
||||
async function rollbackAccount(userId: string, account: ExpectedAccountState): Promise<void> {
|
||||
let updateAccount = false;
|
||||
let settings = account?.settings ?? {};
|
||||
|
||||
const showCardsCurrentTab = await helper.getFromUser<boolean>(userId, {
|
||||
...vaultSettingsStateDefinition,
|
||||
key: "showCardsCurrentTab",
|
||||
});
|
||||
|
||||
const showIdentitiesCurrentTab = await helper.getFromUser<boolean>(userId, {
|
||||
...vaultSettingsStateDefinition,
|
||||
key: "showIdentitiesCurrentTab",
|
||||
});
|
||||
|
||||
if (showCardsCurrentTab != null) {
|
||||
// invert the value to match the new naming convention
|
||||
settings = { ...settings, dontShowCardsCurrentTab: !showCardsCurrentTab };
|
||||
|
||||
await helper.setToUser(
|
||||
userId,
|
||||
{ ...vaultSettingsStateDefinition, key: "showCardsCurrentTab" },
|
||||
null,
|
||||
);
|
||||
|
||||
updateAccount = true;
|
||||
}
|
||||
|
||||
if (showIdentitiesCurrentTab != null) {
|
||||
// invert the value to match the new naming convention
|
||||
settings = { ...settings, dontShowIdentitiesCurrentTab: !showIdentitiesCurrentTab };
|
||||
|
||||
await helper.setToUser(
|
||||
userId,
|
||||
{ ...vaultSettingsStateDefinition, key: "showIdentitiesCurrentTab" },
|
||||
null,
|
||||
);
|
||||
|
||||
updateAccount = true;
|
||||
}
|
||||
|
||||
if (updateAccount) {
|
||||
await helper.set(userId, { ...account, settings });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
import { MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { MigrationHelper } from "../migration-helper";
|
||||
import { mockMigrationHelper, runMigrator } from "../migration-helper.spec";
|
||||
|
||||
import { AvatarColorMigrator } from "./37-move-avatar-color-to-state-providers";
|
||||
|
||||
function rollbackJSON() {
|
||||
return {
|
||||
authenticatedAccounts: ["user-1", "user-2"],
|
||||
"user_user-1_avatar_avatarColor": "#ff0000",
|
||||
"user_user-2_avatar_avatarColor": "#cccccc",
|
||||
"user-1": {
|
||||
settings: {
|
||||
extra: "data",
|
||||
},
|
||||
extra: "data",
|
||||
},
|
||||
"user-2": {
|
||||
settings: {
|
||||
extra: "data",
|
||||
},
|
||||
extra: "data",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("AvatarColorMigrator", () => {
|
||||
const migrator = new AvatarColorMigrator(36, 37);
|
||||
|
||||
it("should migrate the avatarColor property from the account settings object to a user StorageKey", async () => {
|
||||
const output = await runMigrator(migrator, {
|
||||
authenticatedAccounts: ["user-1", "user-2"] as const,
|
||||
"user-1": {
|
||||
settings: {
|
||||
avatarColor: "#ff0000",
|
||||
extra: "data",
|
||||
},
|
||||
extra: "data",
|
||||
},
|
||||
"user-2": {
|
||||
settings: {
|
||||
avatarColor: "#cccccc",
|
||||
extra: "data",
|
||||
},
|
||||
extra: "data",
|
||||
},
|
||||
});
|
||||
|
||||
expect(output).toEqual({
|
||||
authenticatedAccounts: ["user-1", "user-2"],
|
||||
"user_user-1_avatar_avatarColor": "#ff0000",
|
||||
"user_user-2_avatar_avatarColor": "#cccccc",
|
||||
"user-1": {
|
||||
settings: {
|
||||
extra: "data",
|
||||
},
|
||||
extra: "data",
|
||||
},
|
||||
"user-2": {
|
||||
settings: {
|
||||
extra: "data",
|
||||
},
|
||||
extra: "data",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle missing parts", async () => {
|
||||
const output = await runMigrator(migrator, {
|
||||
authenticatedAccounts: ["user-1", "user-2"],
|
||||
global: {
|
||||
extra: "data",
|
||||
},
|
||||
"user-1": {
|
||||
extra: "data",
|
||||
settings: {
|
||||
extra: "data",
|
||||
},
|
||||
},
|
||||
"user-2": null,
|
||||
});
|
||||
|
||||
expect(output).toEqual({
|
||||
authenticatedAccounts: ["user-1", "user-2"],
|
||||
global: {
|
||||
extra: "data",
|
||||
},
|
||||
"user-1": {
|
||||
extra: "data",
|
||||
settings: {
|
||||
extra: "data",
|
||||
},
|
||||
},
|
||||
"user-2": null,
|
||||
});
|
||||
});
|
||||
|
||||
describe("rollback", () => {
|
||||
let helper: MockProxy<MigrationHelper>;
|
||||
let sut: AvatarColorMigrator;
|
||||
|
||||
const keyDefinitionLike = {
|
||||
key: "avatarColor",
|
||||
stateDefinition: {
|
||||
name: "avatar",
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(rollbackJSON(), 37);
|
||||
sut = new AvatarColorMigrator(36, 37);
|
||||
});
|
||||
|
||||
it("should null out the avatarColor user StorageKey for each account", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledTimes(2);
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("user-1", keyDefinitionLike, null);
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("user-2", keyDefinitionLike, null);
|
||||
});
|
||||
|
||||
it("should add the avatarColor property back to the account settings object", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.set).toHaveBeenCalledTimes(2);
|
||||
expect(helper.set).toHaveBeenCalledWith("user-1", {
|
||||
settings: {
|
||||
avatarColor: "#ff0000",
|
||||
extra: "data",
|
||||
},
|
||||
extra: "data",
|
||||
});
|
||||
expect(helper.set).toHaveBeenCalledWith("user-2", {
|
||||
settings: {
|
||||
avatarColor: "#cccccc",
|
||||
extra: "data",
|
||||
},
|
||||
extra: "data",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { KeyDefinitionLike, MigrationHelper, StateDefinitionLike } from "../migration-helper";
|
||||
import { Migrator } from "../migrator";
|
||||
|
||||
type ExpectedAccountState = {
|
||||
settings?: { avatarColor?: string };
|
||||
};
|
||||
|
||||
const AVATAR_COLOR_STATE: StateDefinitionLike = { name: "avatar" };
|
||||
|
||||
const AVATAR_COLOR_KEY: KeyDefinitionLike = {
|
||||
key: "avatarColor",
|
||||
stateDefinition: AVATAR_COLOR_STATE,
|
||||
};
|
||||
|
||||
export class AvatarColorMigrator extends Migrator<36, 37> {
|
||||
async migrate(helper: MigrationHelper): Promise<void> {
|
||||
const legacyAccounts = await helper.getAccounts<ExpectedAccountState>();
|
||||
|
||||
await Promise.all(
|
||||
legacyAccounts.map(async ({ userId, account }) => {
|
||||
// Move account avatarColor
|
||||
if (account?.settings?.avatarColor != null) {
|
||||
await helper.setToUser(userId, AVATAR_COLOR_KEY, account.settings.avatarColor);
|
||||
|
||||
// Delete old account avatarColor property
|
||||
delete account?.settings?.avatarColor;
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async rollback(helper: MigrationHelper): Promise<void> {
|
||||
async function rollbackUser(userId: string, account: ExpectedAccountState) {
|
||||
let updatedAccount = false;
|
||||
const userAvatarColor = await helper.getFromUser<string>(userId, AVATAR_COLOR_KEY);
|
||||
|
||||
if (userAvatarColor) {
|
||||
if (!account) {
|
||||
account = {};
|
||||
}
|
||||
|
||||
updatedAccount = true;
|
||||
account.settings.avatarColor = userAvatarColor;
|
||||
await helper.setToUser(userId, AVATAR_COLOR_KEY, null);
|
||||
}
|
||||
|
||||
if (updatedAccount) {
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
}
|
||||
|
||||
const accounts = await helper.getAccounts<ExpectedAccountState>();
|
||||
|
||||
await Promise.all(accounts.map(({ userId, account }) => rollbackUser(userId, account)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,300 @@
|
||||
import { MockProxy, any } from "jest-mock-extended";
|
||||
|
||||
import { MigrationHelper } from "../migration-helper";
|
||||
import { mockMigrationHelper } from "../migration-helper.spec";
|
||||
|
||||
import {
|
||||
EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL,
|
||||
ACCESS_TOKEN_DISK,
|
||||
REFRESH_TOKEN_DISK,
|
||||
API_KEY_CLIENT_ID_DISK,
|
||||
API_KEY_CLIENT_SECRET_DISK,
|
||||
TokenServiceStateProviderMigrator,
|
||||
} from "./38-migrate-token-svc-to-state-provider";
|
||||
|
||||
// Represents data in state service pre-migration
|
||||
function preMigrationJson() {
|
||||
return {
|
||||
global: {
|
||||
twoFactorToken: "twoFactorToken",
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
authenticatedAccounts: ["user1", "user2", "user3"],
|
||||
user1: {
|
||||
tokens: {
|
||||
accessToken: "accessToken",
|
||||
refreshToken: "refreshToken",
|
||||
otherStuff: "overStuff2",
|
||||
},
|
||||
profile: {
|
||||
apiKeyClientId: "apiKeyClientId",
|
||||
email: "user1Email",
|
||||
otherStuff: "overStuff3",
|
||||
},
|
||||
keys: {
|
||||
apiKeyClientSecret: "apiKeyClientSecret",
|
||||
otherStuff: "overStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
},
|
||||
user2: {
|
||||
tokens: {
|
||||
// no tokens to migrate
|
||||
otherStuff: "overStuff2",
|
||||
},
|
||||
profile: {
|
||||
// no apiKeyClientId to migrate
|
||||
otherStuff: "overStuff3",
|
||||
email: "user2Email",
|
||||
},
|
||||
keys: {
|
||||
// no apiKeyClientSecret to migrate
|
||||
otherStuff: "overStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function rollbackJSON() {
|
||||
return {
|
||||
// User specific state provider data
|
||||
// use pattern user_{userId}_{stateDefinitionName}_{keyDefinitionKey} for user data
|
||||
|
||||
// User1 migrated data
|
||||
user_user1_token_accessToken: "accessToken",
|
||||
user_user1_token_refreshToken: "refreshToken",
|
||||
user_user1_token_apiKeyClientId: "apiKeyClientId",
|
||||
user_user1_token_apiKeyClientSecret: "apiKeyClientSecret",
|
||||
|
||||
// User2 migrated data
|
||||
user_user2_token_accessToken: null as any,
|
||||
user_user2_token_refreshToken: null as any,
|
||||
user_user2_token_apiKeyClientId: null as any,
|
||||
user_user2_token_apiKeyClientSecret: null as any,
|
||||
|
||||
// Global state provider data
|
||||
// use pattern global_{stateDefinitionName}_{keyDefinitionKey} for global data
|
||||
global_tokenDiskLocal_emailTwoFactorTokenRecord: {
|
||||
user1Email: "twoFactorToken",
|
||||
user2Email: "twoFactorToken",
|
||||
},
|
||||
|
||||
global: {
|
||||
// no longer has twoFactorToken
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
authenticatedAccounts: ["user1", "user2", "user3"],
|
||||
user1: {
|
||||
tokens: {
|
||||
otherStuff: "overStuff2",
|
||||
},
|
||||
profile: {
|
||||
email: "user1Email",
|
||||
otherStuff: "overStuff3",
|
||||
},
|
||||
keys: {
|
||||
otherStuff: "overStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
},
|
||||
user2: {
|
||||
tokens: {
|
||||
otherStuff: "overStuff2",
|
||||
},
|
||||
profile: {
|
||||
email: "user2Email",
|
||||
otherStuff: "overStuff3",
|
||||
},
|
||||
keys: {
|
||||
otherStuff: "overStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("TokenServiceStateProviderMigrator", () => {
|
||||
let helper: MockProxy<MigrationHelper>;
|
||||
let sut: TokenServiceStateProviderMigrator;
|
||||
|
||||
describe("migrate", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(preMigrationJson(), 37);
|
||||
sut = new TokenServiceStateProviderMigrator(37, 38);
|
||||
});
|
||||
|
||||
describe("Session storage", () => {
|
||||
it("should remove state service data from all accounts that have it", async () => {
|
||||
await sut.migrate(helper);
|
||||
|
||||
expect(helper.set).toHaveBeenCalledWith("user1", {
|
||||
tokens: {
|
||||
otherStuff: "overStuff2",
|
||||
},
|
||||
profile: {
|
||||
email: "user1Email",
|
||||
otherStuff: "overStuff3",
|
||||
},
|
||||
keys: {
|
||||
otherStuff: "overStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
});
|
||||
|
||||
expect(helper.set).toHaveBeenCalledTimes(2);
|
||||
expect(helper.set).not.toHaveBeenCalledWith("user2", any());
|
||||
expect(helper.set).not.toHaveBeenCalledWith("user3", any());
|
||||
});
|
||||
|
||||
it("should migrate data to state providers for defined accounts that have the data", async () => {
|
||||
await sut.migrate(helper);
|
||||
|
||||
// Two factor Token Migration
|
||||
expect(helper.setToGlobal).toHaveBeenLastCalledWith(
|
||||
EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL,
|
||||
{
|
||||
user1Email: "twoFactorToken",
|
||||
user2Email: "twoFactorToken",
|
||||
},
|
||||
);
|
||||
expect(helper.setToGlobal).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("user1", ACCESS_TOKEN_DISK, "accessToken");
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("user1", REFRESH_TOKEN_DISK, "refreshToken");
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(
|
||||
"user1",
|
||||
API_KEY_CLIENT_ID_DISK,
|
||||
"apiKeyClientId",
|
||||
);
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(
|
||||
"user1",
|
||||
API_KEY_CLIENT_SECRET_DISK,
|
||||
"apiKeyClientSecret",
|
||||
);
|
||||
|
||||
expect(helper.setToUser).not.toHaveBeenCalledWith("user2", ACCESS_TOKEN_DISK, any());
|
||||
expect(helper.setToUser).not.toHaveBeenCalledWith("user2", REFRESH_TOKEN_DISK, any());
|
||||
expect(helper.setToUser).not.toHaveBeenCalledWith("user2", API_KEY_CLIENT_ID_DISK, any());
|
||||
expect(helper.setToUser).not.toHaveBeenCalledWith(
|
||||
"user2",
|
||||
API_KEY_CLIENT_SECRET_DISK,
|
||||
any(),
|
||||
);
|
||||
|
||||
// Expect that we didn't migrate anything to user 3
|
||||
|
||||
expect(helper.setToUser).not.toHaveBeenCalledWith("user3", ACCESS_TOKEN_DISK, any());
|
||||
expect(helper.setToUser).not.toHaveBeenCalledWith("user3", REFRESH_TOKEN_DISK, any());
|
||||
expect(helper.setToUser).not.toHaveBeenCalledWith("user3", API_KEY_CLIENT_ID_DISK, any());
|
||||
expect(helper.setToUser).not.toHaveBeenCalledWith(
|
||||
"user3",
|
||||
API_KEY_CLIENT_SECRET_DISK,
|
||||
any(),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe("Local storage", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(preMigrationJson(), 37, "web-disk-local");
|
||||
});
|
||||
it("should remove state service data from all accounts that have it", async () => {
|
||||
await sut.migrate(helper);
|
||||
|
||||
expect(helper.set).toHaveBeenCalledWith("user1", {
|
||||
tokens: {
|
||||
otherStuff: "overStuff2",
|
||||
},
|
||||
profile: {
|
||||
email: "user1Email",
|
||||
otherStuff: "overStuff3",
|
||||
},
|
||||
keys: {
|
||||
otherStuff: "overStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
});
|
||||
|
||||
expect(helper.set).toHaveBeenCalledTimes(2);
|
||||
expect(helper.set).not.toHaveBeenCalledWith("user2", any());
|
||||
expect(helper.set).not.toHaveBeenCalledWith("user3", any());
|
||||
});
|
||||
|
||||
it("should not migrate any data to local storage", async () => {
|
||||
await sut.migrate(helper);
|
||||
|
||||
expect(helper.setToUser).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("rollback", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(rollbackJSON(), 38);
|
||||
sut = new TokenServiceStateProviderMigrator(37, 38);
|
||||
});
|
||||
|
||||
it("should null out newly migrated entries in state provider framework", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.setToGlobal).toHaveBeenCalledWith(
|
||||
EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL,
|
||||
null,
|
||||
);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("user1", ACCESS_TOKEN_DISK, null);
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("user1", REFRESH_TOKEN_DISK, null);
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("user1", API_KEY_CLIENT_ID_DISK, null);
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("user1", API_KEY_CLIENT_SECRET_DISK, null);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("user2", ACCESS_TOKEN_DISK, null);
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("user2", REFRESH_TOKEN_DISK, null);
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("user2", API_KEY_CLIENT_ID_DISK, null);
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("user2", API_KEY_CLIENT_SECRET_DISK, null);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("user3", ACCESS_TOKEN_DISK, null);
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("user3", REFRESH_TOKEN_DISK, null);
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("user3", API_KEY_CLIENT_ID_DISK, null);
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("user3", API_KEY_CLIENT_SECRET_DISK, null);
|
||||
});
|
||||
|
||||
it("should add back data to all accounts that had migrated data (only user 1)", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.set).toHaveBeenCalledWith("user1", {
|
||||
tokens: {
|
||||
accessToken: "accessToken",
|
||||
refreshToken: "refreshToken",
|
||||
otherStuff: "overStuff2",
|
||||
},
|
||||
profile: {
|
||||
apiKeyClientId: "apiKeyClientId",
|
||||
email: "user1Email",
|
||||
otherStuff: "overStuff3",
|
||||
},
|
||||
keys: {
|
||||
apiKeyClientSecret: "apiKeyClientSecret",
|
||||
otherStuff: "overStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
});
|
||||
});
|
||||
|
||||
it("should add back the global twoFactorToken", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.set).toHaveBeenCalledWith("global", {
|
||||
twoFactorToken: "twoFactorToken",
|
||||
otherStuff: "otherStuff1",
|
||||
});
|
||||
});
|
||||
|
||||
it("should not add data back if data wasn't migrated or acct doesn't exist", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
// no data to add back for user2 (acct exists but no migrated data) and user3 (no acct)
|
||||
expect(helper.set).not.toHaveBeenCalledWith("user2", any());
|
||||
expect(helper.set).not.toHaveBeenCalledWith("user3", any());
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,245 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { KeyDefinitionLike, MigrationHelper, StateDefinitionLike } from "../migration-helper";
|
||||
import { Migrator } from "../migrator";
|
||||
|
||||
// Types to represent data as it is stored in JSON
|
||||
type ExpectedAccountType = {
|
||||
tokens?: {
|
||||
accessToken?: string;
|
||||
refreshToken?: string;
|
||||
};
|
||||
profile?: {
|
||||
apiKeyClientId?: string;
|
||||
email?: string;
|
||||
};
|
||||
keys?: {
|
||||
apiKeyClientSecret?: string;
|
||||
};
|
||||
};
|
||||
|
||||
type ExpectedGlobalType = {
|
||||
twoFactorToken?: string;
|
||||
};
|
||||
|
||||
export const EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL: KeyDefinitionLike = {
|
||||
key: "emailTwoFactorTokenRecord",
|
||||
stateDefinition: {
|
||||
name: "tokenDiskLocal",
|
||||
},
|
||||
};
|
||||
|
||||
const TOKEN_STATE_DEF_LIKE: StateDefinitionLike = {
|
||||
name: "token",
|
||||
};
|
||||
|
||||
export const ACCESS_TOKEN_DISK: KeyDefinitionLike = {
|
||||
key: "accessToken", // matches KeyDefinition.key
|
||||
stateDefinition: TOKEN_STATE_DEF_LIKE,
|
||||
};
|
||||
|
||||
export const REFRESH_TOKEN_DISK: KeyDefinitionLike = {
|
||||
key: "refreshToken",
|
||||
stateDefinition: TOKEN_STATE_DEF_LIKE,
|
||||
};
|
||||
|
||||
export const API_KEY_CLIENT_ID_DISK: KeyDefinitionLike = {
|
||||
key: "apiKeyClientId",
|
||||
stateDefinition: TOKEN_STATE_DEF_LIKE,
|
||||
};
|
||||
|
||||
export const API_KEY_CLIENT_SECRET_DISK: KeyDefinitionLike = {
|
||||
key: "apiKeyClientSecret",
|
||||
stateDefinition: TOKEN_STATE_DEF_LIKE,
|
||||
};
|
||||
|
||||
export class TokenServiceStateProviderMigrator extends Migrator<37, 38> {
|
||||
async migrate(helper: MigrationHelper): Promise<void> {
|
||||
// Move global data
|
||||
const globalData = await helper.get<ExpectedGlobalType>("global");
|
||||
|
||||
// Create new global record for 2FA token that we can accumulate data in
|
||||
const emailTwoFactorTokenRecord = {};
|
||||
|
||||
const accounts = await helper.getAccounts<ExpectedAccountType>();
|
||||
async function migrateAccount(
|
||||
userId: string,
|
||||
account: ExpectedAccountType | undefined,
|
||||
globalTwoFactorToken: string | undefined,
|
||||
emailTwoFactorTokenRecord: Record<string, string>,
|
||||
): Promise<void> {
|
||||
let updatedAccount = false;
|
||||
|
||||
// migrate 2FA token from global to user state
|
||||
// Due to the existing implmentation, n users on the same device share the same global state value for 2FA token.
|
||||
// So, we will just migrate it to all users to keep it valid for whichever was the user that set it previously.
|
||||
// Note: don't bother migrating 2FA Token if user account or email is undefined
|
||||
const email = account?.profile?.email;
|
||||
if (globalTwoFactorToken != undefined && account != undefined && email != undefined) {
|
||||
emailTwoFactorTokenRecord[email] = globalTwoFactorToken;
|
||||
// Note: don't set updatedAccount to true here as we aren't updating
|
||||
// the legacy user state, just migrating a global state to a new user state
|
||||
}
|
||||
|
||||
// Migrate access token
|
||||
const existingAccessToken = account?.tokens?.accessToken;
|
||||
|
||||
if (existingAccessToken != null) {
|
||||
// Only migrate data that exists
|
||||
if (helper.type !== "web-disk-local") {
|
||||
// only migrate access token to session storage - never local.
|
||||
await helper.setToUser(userId, ACCESS_TOKEN_DISK, existingAccessToken);
|
||||
}
|
||||
delete account.tokens.accessToken;
|
||||
updatedAccount = true;
|
||||
}
|
||||
|
||||
// Migrate refresh token
|
||||
const existingRefreshToken = account?.tokens?.refreshToken;
|
||||
|
||||
if (existingRefreshToken != null) {
|
||||
if (helper.type !== "web-disk-local") {
|
||||
// only migrate refresh token to session storage - never local.
|
||||
await helper.setToUser(userId, REFRESH_TOKEN_DISK, existingRefreshToken);
|
||||
}
|
||||
delete account.tokens.refreshToken;
|
||||
updatedAccount = true;
|
||||
}
|
||||
|
||||
// Migrate API key client id
|
||||
const existingApiKeyClientId = account?.profile?.apiKeyClientId;
|
||||
|
||||
if (existingApiKeyClientId != null) {
|
||||
if (helper.type !== "web-disk-local") {
|
||||
// only migrate client id to session storage - never local.
|
||||
await helper.setToUser(userId, API_KEY_CLIENT_ID_DISK, existingApiKeyClientId);
|
||||
}
|
||||
delete account.profile.apiKeyClientId;
|
||||
updatedAccount = true;
|
||||
}
|
||||
|
||||
// Migrate API key client secret
|
||||
const existingApiKeyClientSecret = account?.keys?.apiKeyClientSecret;
|
||||
if (existingApiKeyClientSecret != null) {
|
||||
if (helper.type !== "web-disk-local") {
|
||||
// only migrate client secret to session storage - never local.
|
||||
await helper.setToUser(userId, API_KEY_CLIENT_SECRET_DISK, existingApiKeyClientSecret);
|
||||
}
|
||||
delete account.keys.apiKeyClientSecret;
|
||||
updatedAccount = true;
|
||||
}
|
||||
|
||||
if (updatedAccount) {
|
||||
// Save the migrated account only if it was updated
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
...accounts.map(({ userId, account }) =>
|
||||
migrateAccount(userId, account, globalData?.twoFactorToken, emailTwoFactorTokenRecord),
|
||||
),
|
||||
]);
|
||||
|
||||
// Save the global 2FA token record
|
||||
await helper.setToGlobal(EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL, emailTwoFactorTokenRecord);
|
||||
|
||||
// Delete global data
|
||||
delete globalData?.twoFactorToken;
|
||||
await helper.set("global", globalData);
|
||||
}
|
||||
|
||||
async rollback(helper: MigrationHelper): Promise<void> {
|
||||
const accounts = await helper.getAccounts<ExpectedAccountType>();
|
||||
|
||||
// Since we migrated the global 2FA token to all users, we need to rollback the 2FA token for all users
|
||||
// but we only need to set it to the global state once
|
||||
|
||||
// Go through accounts and find the first user that has a non-null email and 2FA token
|
||||
let migratedTwoFactorToken: string | null = null;
|
||||
for (const { account } of accounts) {
|
||||
const email = account?.profile?.email;
|
||||
if (email == null) {
|
||||
continue;
|
||||
}
|
||||
const emailTwoFactorTokenRecord: Record<string, string> = await helper.getFromGlobal(
|
||||
EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL,
|
||||
);
|
||||
|
||||
migratedTwoFactorToken = emailTwoFactorTokenRecord[email];
|
||||
|
||||
if (migratedTwoFactorToken != null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (migratedTwoFactorToken != null) {
|
||||
let legacyGlobal = await helper.get<ExpectedGlobalType>("global");
|
||||
if (!legacyGlobal) {
|
||||
legacyGlobal = {};
|
||||
}
|
||||
legacyGlobal.twoFactorToken = migratedTwoFactorToken;
|
||||
await helper.set("global", legacyGlobal);
|
||||
}
|
||||
|
||||
// delete global 2FA token record
|
||||
await helper.setToGlobal(EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL, null);
|
||||
|
||||
async function rollbackAccount(userId: string, account: ExpectedAccountType): Promise<void> {
|
||||
let updatedLegacyAccount = false;
|
||||
|
||||
// Rollback access token
|
||||
const migratedAccessToken = await helper.getFromUser<string>(userId, ACCESS_TOKEN_DISK);
|
||||
|
||||
if (account?.tokens && migratedAccessToken != null) {
|
||||
account.tokens.accessToken = migratedAccessToken;
|
||||
updatedLegacyAccount = true;
|
||||
}
|
||||
|
||||
await helper.setToUser(userId, ACCESS_TOKEN_DISK, null);
|
||||
|
||||
// Rollback refresh token
|
||||
const migratedRefreshToken = await helper.getFromUser<string>(userId, REFRESH_TOKEN_DISK);
|
||||
|
||||
if (account?.tokens && migratedRefreshToken != null) {
|
||||
account.tokens.refreshToken = migratedRefreshToken;
|
||||
updatedLegacyAccount = true;
|
||||
}
|
||||
|
||||
await helper.setToUser(userId, REFRESH_TOKEN_DISK, null);
|
||||
|
||||
// Rollback API key client id
|
||||
|
||||
const migratedApiKeyClientId = await helper.getFromUser<string>(
|
||||
userId,
|
||||
API_KEY_CLIENT_ID_DISK,
|
||||
);
|
||||
|
||||
if (account?.profile && migratedApiKeyClientId != null) {
|
||||
account.profile.apiKeyClientId = migratedApiKeyClientId;
|
||||
updatedLegacyAccount = true;
|
||||
}
|
||||
|
||||
await helper.setToUser(userId, API_KEY_CLIENT_ID_DISK, null);
|
||||
|
||||
// Rollback API key client secret
|
||||
const migratedApiKeyClientSecret = await helper.getFromUser<string>(
|
||||
userId,
|
||||
API_KEY_CLIENT_SECRET_DISK,
|
||||
);
|
||||
|
||||
if (account?.keys && migratedApiKeyClientSecret != null) {
|
||||
account.keys.apiKeyClientSecret = migratedApiKeyClientSecret;
|
||||
updatedLegacyAccount = true;
|
||||
}
|
||||
|
||||
await helper.setToUser(userId, API_KEY_CLIENT_SECRET_DISK, null);
|
||||
|
||||
if (updatedLegacyAccount) {
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all([...accounts.map(({ userId, account }) => rollbackAccount(userId, account))]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
import { MockProxy, any } from "jest-mock-extended";
|
||||
|
||||
import { MigrationHelper } from "../migration-helper";
|
||||
import { mockMigrationHelper } from "../migration-helper.spec";
|
||||
|
||||
import {
|
||||
BILLING_ACCOUNT_PROFILE_KEY_DEFINITION,
|
||||
MoveBillingAccountProfileMigrator,
|
||||
} from "./39-move-billing-account-profile-to-state-providers";
|
||||
|
||||
const exampleJSON = () => ({
|
||||
global: {
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
authenticatedAccounts: ["user-1", "user-2", "user-3"],
|
||||
"user-1": {
|
||||
profile: {
|
||||
hasPremiumPersonally: true,
|
||||
hasPremiumFromOrganization: false,
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
},
|
||||
"user-2": {
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
});
|
||||
|
||||
const rollbackJSON = () => ({
|
||||
"user_user-1_billing_accountProfile": {
|
||||
hasPremiumPersonally: true,
|
||||
hasPremiumFromOrganization: false,
|
||||
},
|
||||
global: {
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
authenticatedAccounts: ["user-1", "user-2", "user-3"],
|
||||
"user-1": {
|
||||
profile: {
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
},
|
||||
"user-2": {
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
});
|
||||
|
||||
describe("MoveBillingAccountProfileToStateProviders migrator", () => {
|
||||
let helper: MockProxy<MigrationHelper>;
|
||||
let sut: MoveBillingAccountProfileMigrator;
|
||||
|
||||
describe("migrate", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(exampleJSON(), 39);
|
||||
sut = new MoveBillingAccountProfileMigrator(38, 39);
|
||||
});
|
||||
|
||||
it("removes from all accounts", async () => {
|
||||
await sut.migrate(helper);
|
||||
expect(helper.set).toHaveBeenCalledTimes(1);
|
||||
expect(helper.set).toHaveBeenCalledWith("user-1", {
|
||||
profile: {
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
});
|
||||
});
|
||||
|
||||
it("sets hasPremiumPersonally value for account that have it", async () => {
|
||||
await sut.migrate(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(
|
||||
"user-1",
|
||||
BILLING_ACCOUNT_PROFILE_KEY_DEFINITION,
|
||||
{ hasPremiumFromOrganization: false, hasPremiumPersonally: true },
|
||||
);
|
||||
});
|
||||
|
||||
it("should not call extra setToUser", async () => {
|
||||
await sut.migrate(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("rollback", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(rollbackJSON(), 39);
|
||||
sut = new MoveBillingAccountProfileMigrator(38, 39);
|
||||
});
|
||||
|
||||
it("nulls out new values", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(
|
||||
"user-1",
|
||||
BILLING_ACCOUNT_PROFILE_KEY_DEFINITION,
|
||||
null,
|
||||
);
|
||||
});
|
||||
|
||||
it("adds explicit value back to accounts", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.set).toHaveBeenCalledTimes(1);
|
||||
expect(helper.set).toHaveBeenCalledWith("user-1", {
|
||||
profile: {
|
||||
hasPremiumPersonally: true,
|
||||
hasPremiumFromOrganization: false,
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
});
|
||||
});
|
||||
|
||||
it.each(["user-2", "user-3"])(
|
||||
"does not restore values when accounts are not present",
|
||||
async (userId) => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.set).not.toHaveBeenCalledWith(userId, any());
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
|
||||
import { Migrator } from "../migrator";
|
||||
|
||||
type ExpectedAccountType = {
|
||||
profile?: {
|
||||
hasPremiumPersonally?: boolean;
|
||||
hasPremiumFromOrganization?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
type ExpectedBillingAccountProfileType = {
|
||||
hasPremiumPersonally: boolean;
|
||||
hasPremiumFromOrganization: boolean;
|
||||
};
|
||||
|
||||
export const BILLING_ACCOUNT_PROFILE_KEY_DEFINITION: KeyDefinitionLike = {
|
||||
key: "accountProfile",
|
||||
stateDefinition: {
|
||||
name: "billing",
|
||||
},
|
||||
};
|
||||
|
||||
export class MoveBillingAccountProfileMigrator extends Migrator<38, 39> {
|
||||
async migrate(helper: MigrationHelper): Promise<void> {
|
||||
const accounts = await helper.getAccounts<ExpectedAccountType>();
|
||||
|
||||
const migrateAccount = async (userId: string, account: ExpectedAccountType): Promise<void> => {
|
||||
const hasPremiumPersonally = account?.profile?.hasPremiumPersonally;
|
||||
const hasPremiumFromOrganization = account?.profile?.hasPremiumFromOrganization;
|
||||
|
||||
if (hasPremiumPersonally != null || hasPremiumFromOrganization != null) {
|
||||
await helper.setToUser(userId, BILLING_ACCOUNT_PROFILE_KEY_DEFINITION, {
|
||||
hasPremiumPersonally: hasPremiumPersonally,
|
||||
hasPremiumFromOrganization: hasPremiumFromOrganization,
|
||||
});
|
||||
|
||||
delete account?.profile?.hasPremiumPersonally;
|
||||
delete account?.profile?.hasPremiumFromOrganization;
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
};
|
||||
|
||||
await Promise.all([...accounts.map(({ userId, account }) => migrateAccount(userId, account))]);
|
||||
}
|
||||
|
||||
async rollback(helper: MigrationHelper): Promise<void> {
|
||||
const accounts = await helper.getAccounts<ExpectedAccountType>();
|
||||
const rollbackAccount = async (userId: string, account: ExpectedAccountType): Promise<void> => {
|
||||
const value = await helper.getFromUser<ExpectedBillingAccountProfileType>(
|
||||
userId,
|
||||
BILLING_ACCOUNT_PROFILE_KEY_DEFINITION,
|
||||
);
|
||||
|
||||
if (account && value) {
|
||||
account.profile = Object.assign(account.profile ?? {}, {
|
||||
hasPremiumPersonally: value?.hasPremiumPersonally,
|
||||
hasPremiumFromOrganization: value?.hasPremiumFromOrganization,
|
||||
});
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
|
||||
await helper.setToUser(userId, BILLING_ACCOUNT_PROFILE_KEY_DEFINITION, null);
|
||||
};
|
||||
|
||||
await Promise.all([...accounts.map(({ userId, account }) => rollbackAccount(userId, account))]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import { MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { MigrationHelper } from "../migration-helper";
|
||||
import { mockMigrationHelper } from "../migration-helper.spec";
|
||||
|
||||
import { RemoveEverBeenUnlockedMigrator } from "./4-remove-ever-been-unlocked";
|
||||
|
||||
function migrateExampleJSON() {
|
||||
return {
|
||||
global: {
|
||||
stateVersion: 3,
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
authenticatedAccounts: [
|
||||
"c493ed01-4e08-4e88-abc7-332f380ca760",
|
||||
"23e61a5f-2ece-4f5e-b499-f0bc489482a9",
|
||||
],
|
||||
"c493ed01-4e08-4e88-abc7-332f380ca760": {
|
||||
profile: {
|
||||
otherStuff: "otherStuff2",
|
||||
everBeenUnlocked: true,
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
},
|
||||
"23e61a5f-2ece-4f5e-b499-f0bc489482a9": {
|
||||
profile: {
|
||||
otherStuff: "otherStuff4",
|
||||
everBeenUnlocked: false,
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
},
|
||||
otherStuff: "otherStuff6",
|
||||
};
|
||||
}
|
||||
|
||||
describe("RemoveEverBeenUnlockedMigrator", () => {
|
||||
let helper: MockProxy<MigrationHelper>;
|
||||
let sut: RemoveEverBeenUnlockedMigrator;
|
||||
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(migrateExampleJSON());
|
||||
sut = new RemoveEverBeenUnlockedMigrator(3, 4);
|
||||
});
|
||||
|
||||
describe("migrate", () => {
|
||||
it("should remove everBeenUnlocked from profile", async () => {
|
||||
await sut.migrate(helper);
|
||||
expect(helper.set).toHaveBeenCalledTimes(2);
|
||||
expect(helper.set).toHaveBeenCalledWith("c493ed01-4e08-4e88-abc7-332f380ca760", {
|
||||
profile: {
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
});
|
||||
expect(helper.set).toHaveBeenCalledWith("23e61a5f-2ece-4f5e-b499-f0bc489482a9", {
|
||||
profile: {
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("updateVersion", () => {
|
||||
it("should update version up", async () => {
|
||||
await sut.updateVersion(helper, "up");
|
||||
|
||||
expect(helper.set).toHaveBeenCalledTimes(1);
|
||||
expect(helper.set).toHaveBeenCalledWith("global", {
|
||||
stateVersion: 4,
|
||||
otherStuff: "otherStuff1",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
import { MigrationHelper } from "../migration-helper";
|
||||
import { Direction, IRREVERSIBLE, Migrator } from "../migrator";
|
||||
|
||||
type ExpectedAccountType = { profile?: { everBeenUnlocked?: boolean } };
|
||||
|
||||
export class RemoveEverBeenUnlockedMigrator extends Migrator<3, 4> {
|
||||
async migrate(helper: MigrationHelper): Promise<void> {
|
||||
const accounts = await helper.getAccounts<ExpectedAccountType>();
|
||||
|
||||
async function removeEverBeenUnlocked(userId: string, account: ExpectedAccountType) {
|
||||
if (account?.profile?.everBeenUnlocked != null) {
|
||||
delete account.profile.everBeenUnlocked;
|
||||
return helper.set(userId, account);
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
Promise.all(accounts.map(({ userId, account }) => removeEverBeenUnlocked(userId, account)));
|
||||
}
|
||||
|
||||
rollback(helper: MigrationHelper): Promise<void> {
|
||||
throw IRREVERSIBLE;
|
||||
}
|
||||
|
||||
// Override is necessary because default implementation assumes `stateVersion` at the root, but for this version
|
||||
// it is nested inside a global object.
|
||||
override async updateVersion(helper: MigrationHelper, direction: Direction): Promise<void> {
|
||||
const endVersion = direction === "up" ? this.toVersion : this.fromVersion;
|
||||
helper.currentVersion = endVersion;
|
||||
const global: { stateVersion: number } = (await helper.get("global")) || ({} as any);
|
||||
await helper.set("global", { ...global, stateVersion: endVersion });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
import { any, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { MigrationHelper } from "../migration-helper";
|
||||
import { mockMigrationHelper } from "../migration-helper.spec";
|
||||
|
||||
import { OrganizationMigrator } from "./40-move-organization-state-to-state-provider";
|
||||
|
||||
const testDate = new Date();
|
||||
function exampleOrganization1() {
|
||||
return JSON.stringify({
|
||||
id: "id",
|
||||
name: "name",
|
||||
status: 0,
|
||||
type: 0,
|
||||
enabled: false,
|
||||
usePolicies: false,
|
||||
useGroups: false,
|
||||
useDirectory: false,
|
||||
useEvents: false,
|
||||
useTotp: false,
|
||||
use2fa: false,
|
||||
useApi: false,
|
||||
useSso: false,
|
||||
useKeyConnector: false,
|
||||
useScim: false,
|
||||
useCustomPermissions: false,
|
||||
useResetPassword: false,
|
||||
useSecretsManager: false,
|
||||
usePasswordManager: false,
|
||||
useActivateAutofillPolicy: false,
|
||||
selfHost: false,
|
||||
usersGetPremium: false,
|
||||
seats: 0,
|
||||
maxCollections: 0,
|
||||
ssoBound: false,
|
||||
identifier: "identifier",
|
||||
resetPasswordEnrolled: false,
|
||||
userId: "userId",
|
||||
hasPublicAndPrivateKeys: false,
|
||||
providerId: "providerId",
|
||||
providerName: "providerName",
|
||||
isProviderUser: false,
|
||||
isMember: false,
|
||||
familySponsorshipFriendlyName: "fsfn",
|
||||
familySponsorshipAvailable: false,
|
||||
planProductType: 0,
|
||||
keyConnectorEnabled: false,
|
||||
keyConnectorUrl: "kcu",
|
||||
accessSecretsManager: false,
|
||||
limitCollectionCreationDeletion: false,
|
||||
allowAdminAccessToAllCollectionItems: false,
|
||||
flexibleCollections: false,
|
||||
familySponsorshipLastSyncDate: testDate,
|
||||
});
|
||||
}
|
||||
|
||||
function exampleJSON() {
|
||||
return {
|
||||
global: {
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
authenticatedAccounts: ["user-1", "user-2"],
|
||||
"user-1": {
|
||||
data: {
|
||||
organizations: {
|
||||
"organization-id-1": exampleOrganization1(),
|
||||
"organization-id-2": {
|
||||
// ...
|
||||
},
|
||||
},
|
||||
otherStuff: "overStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
},
|
||||
"user-2": {
|
||||
data: {
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function rollbackJSON() {
|
||||
return {
|
||||
"user_user-1_organizations_organizations": {
|
||||
"organization-id-1": exampleOrganization1(),
|
||||
"organization-id-2": {
|
||||
// ...
|
||||
},
|
||||
},
|
||||
"user_user-2_organizations_organizations": null as any,
|
||||
global: {
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
authenticatedAccounts: ["user-1", "user-2"],
|
||||
"user-1": {
|
||||
data: {
|
||||
otherStuff: "overStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
},
|
||||
"user-2": {
|
||||
data: {
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("OrganizationMigrator", () => {
|
||||
let helper: MockProxy<MigrationHelper>;
|
||||
let sut: OrganizationMigrator;
|
||||
const keyDefinitionLike = {
|
||||
key: "organizations",
|
||||
stateDefinition: {
|
||||
name: "organizations",
|
||||
},
|
||||
};
|
||||
|
||||
describe("migrate", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(exampleJSON(), 40);
|
||||
sut = new OrganizationMigrator(39, 40);
|
||||
});
|
||||
|
||||
it("should remove organizations from all accounts", async () => {
|
||||
await sut.migrate(helper);
|
||||
expect(helper.set).toHaveBeenCalledWith("user-1", {
|
||||
data: {
|
||||
otherStuff: "overStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
});
|
||||
});
|
||||
|
||||
it("should set organizations value for each account", async () => {
|
||||
await sut.migrate(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("user-1", keyDefinitionLike, {
|
||||
"organization-id-1": exampleOrganization1(),
|
||||
"organization-id-2": {
|
||||
// ...
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("rollback", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(rollbackJSON(), 40);
|
||||
sut = new OrganizationMigrator(39, 40);
|
||||
});
|
||||
|
||||
it.each(["user-1", "user-2"])("should null out new values", async (userId) => {
|
||||
await sut.rollback(helper);
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(userId, keyDefinitionLike, null);
|
||||
});
|
||||
|
||||
it("should add explicit value back to accounts", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.set).toHaveBeenCalledWith("user-1", {
|
||||
data: {
|
||||
organizations: {
|
||||
"organization-id-1": exampleOrganization1(),
|
||||
"organization-id-2": {
|
||||
// ...
|
||||
},
|
||||
},
|
||||
otherStuff: "overStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
});
|
||||
});
|
||||
|
||||
it("should not try to restore values to missing accounts", async () => {
|
||||
await sut.rollback(helper);
|
||||
expect(helper.set).not.toHaveBeenCalledWith("user-3", any());
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,158 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
|
||||
import { Migrator } from "../migrator";
|
||||
|
||||
// Local declarations of `OrganizationData` and the types of it's properties.
|
||||
// Duplicated to remain frozen in time when migration occurs.
|
||||
// FIXME: update to use a const object instead of a typescript enum
|
||||
// eslint-disable-next-line @bitwarden/platform/no-enums
|
||||
enum OrganizationUserStatusType {
|
||||
Invited = 0,
|
||||
Accepted = 1,
|
||||
Confirmed = 2,
|
||||
Revoked = -1,
|
||||
}
|
||||
|
||||
// FIXME: update to use a const object instead of a typescript enum
|
||||
// eslint-disable-next-line @bitwarden/platform/no-enums
|
||||
enum OrganizationUserType {
|
||||
Owner = 0,
|
||||
Admin = 1,
|
||||
User = 2,
|
||||
Manager = 3,
|
||||
Custom = 4,
|
||||
}
|
||||
|
||||
type PermissionsApi = {
|
||||
accessEventLogs: boolean;
|
||||
accessImportExport: boolean;
|
||||
accessReports: boolean;
|
||||
createNewCollections: boolean;
|
||||
editAnyCollection: boolean;
|
||||
deleteAnyCollection: boolean;
|
||||
editAssignedCollections: boolean;
|
||||
deleteAssignedCollections: boolean;
|
||||
manageCiphers: boolean;
|
||||
manageGroups: boolean;
|
||||
manageSso: boolean;
|
||||
managePolicies: boolean;
|
||||
manageUsers: boolean;
|
||||
manageResetPassword: boolean;
|
||||
manageScim: boolean;
|
||||
};
|
||||
|
||||
// FIXME: update to use a const object instead of a typescript enum
|
||||
// eslint-disable-next-line @bitwarden/platform/no-enums
|
||||
enum ProviderType {
|
||||
Msp = 0,
|
||||
Reseller = 1,
|
||||
}
|
||||
|
||||
// FIXME: update to use a const object instead of a typescript enum
|
||||
// eslint-disable-next-line @bitwarden/platform/no-enums
|
||||
enum ProductType {
|
||||
Free = 0,
|
||||
Families = 1,
|
||||
Teams = 2,
|
||||
Enterprise = 3,
|
||||
TeamsStarter = 4,
|
||||
}
|
||||
|
||||
type OrganizationData = {
|
||||
id: string;
|
||||
name: string;
|
||||
status: OrganizationUserStatusType;
|
||||
type: OrganizationUserType;
|
||||
enabled: boolean;
|
||||
usePolicies: boolean;
|
||||
useGroups: boolean;
|
||||
useDirectory: boolean;
|
||||
useEvents: boolean;
|
||||
useTotp: boolean;
|
||||
use2fa: boolean;
|
||||
useApi: boolean;
|
||||
useSso: boolean;
|
||||
useKeyConnector: boolean;
|
||||
useScim: boolean;
|
||||
useCustomPermissions: boolean;
|
||||
useResetPassword: boolean;
|
||||
useSecretsManager: boolean;
|
||||
usePasswordManager: boolean;
|
||||
useActivateAutofillPolicy: boolean;
|
||||
selfHost: boolean;
|
||||
usersGetPremium: boolean;
|
||||
seats: number;
|
||||
maxCollections: number;
|
||||
maxStorageGb?: number;
|
||||
ssoBound: boolean;
|
||||
identifier: string;
|
||||
permissions: PermissionsApi;
|
||||
resetPasswordEnrolled: boolean;
|
||||
userId: string;
|
||||
hasPublicAndPrivateKeys: boolean;
|
||||
providerId: string;
|
||||
providerName: string;
|
||||
providerType?: ProviderType;
|
||||
isProviderUser: boolean;
|
||||
isMember: boolean;
|
||||
familySponsorshipFriendlyName: string;
|
||||
familySponsorshipAvailable: boolean;
|
||||
planProductType: ProductType;
|
||||
keyConnectorEnabled: boolean;
|
||||
keyConnectorUrl: string;
|
||||
familySponsorshipLastSyncDate?: Date;
|
||||
familySponsorshipValidUntil?: Date;
|
||||
familySponsorshipToDelete?: boolean;
|
||||
accessSecretsManager: boolean;
|
||||
limitCollectionCreationDeletion: boolean;
|
||||
allowAdminAccessToAllCollectionItems: boolean;
|
||||
flexibleCollections: boolean;
|
||||
};
|
||||
|
||||
type ExpectedAccountType = {
|
||||
data?: {
|
||||
organizations?: Record<string, Jsonify<OrganizationData>>;
|
||||
};
|
||||
};
|
||||
|
||||
const USER_ORGANIZATIONS: KeyDefinitionLike = {
|
||||
key: "organizations",
|
||||
stateDefinition: {
|
||||
name: "organizations",
|
||||
},
|
||||
};
|
||||
|
||||
export class OrganizationMigrator extends Migrator<39, 40> {
|
||||
async migrate(helper: MigrationHelper): Promise<void> {
|
||||
const accounts = await helper.getAccounts<ExpectedAccountType>();
|
||||
async function migrateAccount(userId: string, account: ExpectedAccountType): Promise<void> {
|
||||
const value = account?.data?.organizations;
|
||||
if (value != null) {
|
||||
await helper.setToUser(userId, USER_ORGANIZATIONS, value);
|
||||
delete account.data.organizations;
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(accounts.map(({ userId, account }) => migrateAccount(userId, account)));
|
||||
}
|
||||
|
||||
async rollback(helper: MigrationHelper): Promise<void> {
|
||||
const accounts = await helper.getAccounts<ExpectedAccountType>();
|
||||
async function rollbackAccount(userId: string, account: ExpectedAccountType): Promise<void> {
|
||||
const value = await helper.getFromUser(userId, USER_ORGANIZATIONS);
|
||||
if (account) {
|
||||
account.data = Object.assign(account.data ?? {}, {
|
||||
organizations: value,
|
||||
});
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
await helper.setToUser(userId, USER_ORGANIZATIONS, null);
|
||||
}
|
||||
|
||||
await Promise.all(accounts.map(({ userId, account }) => rollbackAccount(userId, account)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
import { MockProxy, any } from "jest-mock-extended";
|
||||
|
||||
import { MigrationHelper } from "../migration-helper";
|
||||
import { mockMigrationHelper } from "../migration-helper.spec";
|
||||
|
||||
import { EventCollectionMigrator } from "./41-move-event-collection-to-state-provider";
|
||||
|
||||
function exampleJSON() {
|
||||
return {
|
||||
global: {
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
authenticatedAccounts: ["user-1", "user-2"],
|
||||
"user-1": {
|
||||
data: {
|
||||
eventCollection: [
|
||||
{
|
||||
type: 1107,
|
||||
cipherId: "5154f91d-c469-4d23-aefa-b12a0140d684",
|
||||
organizationId: "278d5f91-835b-459a-a229-b11e01336d6d",
|
||||
date: "2024-03-05T21:59:50.169Z",
|
||||
},
|
||||
{
|
||||
type: 1107,
|
||||
cipherId: "ed4661bd-412c-4b05-89a2-b12a01697a2c",
|
||||
organizationId: "278d5f91-835b-459a-a229-b11e01336d6d",
|
||||
date: "2024-03-05T22:02:06.089Z",
|
||||
},
|
||||
],
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
},
|
||||
"user-2": {
|
||||
data: {
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function rollbackJSON() {
|
||||
return {
|
||||
"user_user-1_eventCollection_eventCollection": [
|
||||
{
|
||||
type: 1107,
|
||||
cipherId: "5154f91d-c469-4d23-aefa-b12a0140d684",
|
||||
organizationId: "278d5f91-835b-459a-a229-b11e01336d6d",
|
||||
date: "2024-03-05T21:59:50.169Z",
|
||||
},
|
||||
{
|
||||
type: 1107,
|
||||
cipherId: "ed4661bd-412c-4b05-89a2-b12a01697a2c",
|
||||
organizationId: "278d5f91-835b-459a-a229-b11e01336d6d",
|
||||
date: "2024-03-05T22:02:06.089Z",
|
||||
},
|
||||
],
|
||||
"user_user-2_eventCollection_data": null as any,
|
||||
global: {
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
authenticatedAccounts: ["user-1", "user-2"],
|
||||
"user-1": {
|
||||
data: {
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
},
|
||||
"user-2": {
|
||||
data: {
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("EventCollectionMigrator", () => {
|
||||
let helper: MockProxy<MigrationHelper>;
|
||||
let sut: EventCollectionMigrator;
|
||||
const keyDefinitionLike = {
|
||||
stateDefinition: {
|
||||
name: "eventCollection",
|
||||
},
|
||||
key: "eventCollection",
|
||||
};
|
||||
|
||||
describe("migrate", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(exampleJSON(), 40);
|
||||
sut = new EventCollectionMigrator(40, 41);
|
||||
});
|
||||
|
||||
it("should remove event collections from all accounts", async () => {
|
||||
await sut.migrate(helper);
|
||||
expect(helper.set).toHaveBeenCalledWith("user-1", {
|
||||
data: {
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
});
|
||||
});
|
||||
|
||||
it("should set event collections for each account", async () => {
|
||||
await sut.migrate(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("user-1", keyDefinitionLike, [
|
||||
{
|
||||
type: 1107,
|
||||
cipherId: "5154f91d-c469-4d23-aefa-b12a0140d684",
|
||||
organizationId: "278d5f91-835b-459a-a229-b11e01336d6d",
|
||||
date: "2024-03-05T21:59:50.169Z",
|
||||
},
|
||||
{
|
||||
type: 1107,
|
||||
cipherId: "ed4661bd-412c-4b05-89a2-b12a01697a2c",
|
||||
organizationId: "278d5f91-835b-459a-a229-b11e01336d6d",
|
||||
date: "2024-03-05T22:02:06.089Z",
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("rollback", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(rollbackJSON(), 41);
|
||||
sut = new EventCollectionMigrator(40, 41);
|
||||
});
|
||||
|
||||
it.each(["user-1", "user-2"])("should null out new values", async (userId) => {
|
||||
await sut.rollback(helper);
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(userId, keyDefinitionLike, null);
|
||||
});
|
||||
|
||||
it("should add event collection values back to accounts", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.set).toHaveBeenCalled();
|
||||
expect(helper.set).toHaveBeenCalledWith("user-1", {
|
||||
data: {
|
||||
eventCollection: [
|
||||
{
|
||||
type: 1107,
|
||||
cipherId: "5154f91d-c469-4d23-aefa-b12a0140d684",
|
||||
organizationId: "278d5f91-835b-459a-a229-b11e01336d6d",
|
||||
date: "2024-03-05T21:59:50.169Z",
|
||||
},
|
||||
{
|
||||
type: 1107,
|
||||
cipherId: "ed4661bd-412c-4b05-89a2-b12a01697a2c",
|
||||
organizationId: "278d5f91-835b-459a-a229-b11e01336d6d",
|
||||
date: "2024-03-05T22:02:06.089Z",
|
||||
},
|
||||
],
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
});
|
||||
});
|
||||
|
||||
it("should not try to restore values to missing accounts", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.set).not.toHaveBeenCalledWith("user-3", any());
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
|
||||
import { Migrator } from "../migrator";
|
||||
|
||||
type ExpectedAccountState = {
|
||||
data?: {
|
||||
eventCollection?: [];
|
||||
};
|
||||
};
|
||||
|
||||
const EVENT_COLLECTION: KeyDefinitionLike = {
|
||||
stateDefinition: {
|
||||
name: "eventCollection",
|
||||
},
|
||||
key: "eventCollection",
|
||||
};
|
||||
|
||||
export class EventCollectionMigrator extends Migrator<40, 41> {
|
||||
async migrate(helper: MigrationHelper): Promise<void> {
|
||||
const accounts = await helper.getAccounts<ExpectedAccountState>();
|
||||
|
||||
async function migrateAccount(userId: string, account: ExpectedAccountState): Promise<void> {
|
||||
const value = account?.data?.eventCollection;
|
||||
if (value != null) {
|
||||
await helper.setToUser(userId, EVENT_COLLECTION, value);
|
||||
delete account.data.eventCollection;
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all([...accounts.map(({ userId, account }) => migrateAccount(userId, account))]);
|
||||
}
|
||||
|
||||
async rollback(helper: MigrationHelper): Promise<void> {
|
||||
const accounts = await helper.getAccounts<ExpectedAccountState>();
|
||||
|
||||
async function rollbackAccount(userId: string, account: ExpectedAccountState): Promise<void> {
|
||||
const value = await helper.getFromUser(userId, EVENT_COLLECTION);
|
||||
if (account) {
|
||||
account.data = Object.assign(account.data ?? {}, {
|
||||
eventCollection: value,
|
||||
});
|
||||
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
await helper.setToUser(userId, EVENT_COLLECTION, null);
|
||||
}
|
||||
await Promise.all([...accounts.map(({ userId, account }) => rollbackAccount(userId, account))]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
import { MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
|
||||
import { mockMigrationHelper } from "../migration-helper.spec";
|
||||
|
||||
import { EnableFaviconMigrator } from "./42-move-enable-favicon-to-domain-settings-state-provider";
|
||||
|
||||
function exampleJSON() {
|
||||
return {
|
||||
global: {
|
||||
otherStuff: "otherStuff1",
|
||||
disableFavicon: true,
|
||||
},
|
||||
authenticatedAccounts: ["user-1", "user-2"],
|
||||
"user-1": {
|
||||
settings: {
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
},
|
||||
"user-2": {
|
||||
settings: {
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function rollbackJSON() {
|
||||
return {
|
||||
global_domainSettings_showFavicons: false,
|
||||
global: {
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
authenticatedAccounts: ["user-1", "user-2"],
|
||||
"user-1": {
|
||||
settings: {
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
},
|
||||
"user-2": {
|
||||
settings: {
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const showFaviconsKeyDefinition: KeyDefinitionLike = {
|
||||
stateDefinition: {
|
||||
name: "domainSettings",
|
||||
},
|
||||
key: "showFavicons",
|
||||
};
|
||||
|
||||
describe("EnableFaviconMigrator", () => {
|
||||
let helper: MockProxy<MigrationHelper>;
|
||||
let sut: EnableFaviconMigrator;
|
||||
|
||||
describe("migrate", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(exampleJSON(), 41);
|
||||
sut = new EnableFaviconMigrator(41, 42);
|
||||
});
|
||||
|
||||
it("should remove global disableFavicon", async () => {
|
||||
await sut.migrate(helper);
|
||||
expect(helper.set).toHaveBeenCalledTimes(1);
|
||||
expect(helper.set).toHaveBeenCalledWith("global", {
|
||||
otherStuff: "otherStuff1",
|
||||
});
|
||||
});
|
||||
|
||||
it("should set global showFavicons", async () => {
|
||||
await sut.migrate(helper);
|
||||
|
||||
expect(helper.setToGlobal).toHaveBeenCalledTimes(1);
|
||||
expect(helper.setToGlobal).toHaveBeenCalledWith(showFaviconsKeyDefinition, false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("rollback", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(rollbackJSON(), 42);
|
||||
sut = new EnableFaviconMigrator(41, 42);
|
||||
});
|
||||
|
||||
it("should null global showFavicons", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.setToGlobal).toHaveBeenCalledTimes(1);
|
||||
expect(helper.setToGlobal).toHaveBeenCalledWith(showFaviconsKeyDefinition, null);
|
||||
});
|
||||
|
||||
it("should add global disableFavicon back", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.set).toHaveBeenCalledTimes(1);
|
||||
expect(helper.set).toHaveBeenCalledWith("global", {
|
||||
disableFavicon: true,
|
||||
otherStuff: "otherStuff1",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
|
||||
import { Migrator } from "../migrator";
|
||||
|
||||
type ExpectedGlobalState = {
|
||||
disableFavicon?: boolean;
|
||||
};
|
||||
|
||||
const ShowFaviconDefinition: KeyDefinitionLike = {
|
||||
stateDefinition: {
|
||||
name: "domainSettings",
|
||||
},
|
||||
key: "showFavicons",
|
||||
};
|
||||
|
||||
export class EnableFaviconMigrator extends Migrator<41, 42> {
|
||||
async migrate(helper: MigrationHelper): Promise<void> {
|
||||
// global state ("disableFavicon" -> "showFavicons")
|
||||
const globalState = await helper.get<ExpectedGlobalState>("global");
|
||||
|
||||
if (globalState?.disableFavicon != null) {
|
||||
await helper.setToGlobal(ShowFaviconDefinition, !globalState.disableFavicon);
|
||||
|
||||
// delete `disableFavicon` from state global
|
||||
delete globalState.disableFavicon;
|
||||
|
||||
await helper.set<ExpectedGlobalState>("global", globalState);
|
||||
}
|
||||
}
|
||||
|
||||
async rollback(helper: MigrationHelper): Promise<void> {
|
||||
// global state ("showFavicons" -> "disableFavicon")
|
||||
const globalState = (await helper.get<ExpectedGlobalState>("global")) || {};
|
||||
const showFavicons: boolean = await helper.getFromGlobal(ShowFaviconDefinition);
|
||||
|
||||
if (showFavicons != null) {
|
||||
await helper.set<ExpectedGlobalState>("global", {
|
||||
...globalState,
|
||||
disableFavicon: !showFavicons,
|
||||
});
|
||||
|
||||
// remove the global state provider framework key for `showFavicons`
|
||||
await helper.setToGlobal(ShowFaviconDefinition, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import { MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { MigrationHelper } from "../migration-helper";
|
||||
import { mockMigrationHelper, runMigrator } from "../migration-helper.spec";
|
||||
|
||||
import { AutoConfirmFingerPrintsMigrator } from "./43-move-auto-confirm-finger-prints-to-state-provider";
|
||||
|
||||
function rollbackJSON() {
|
||||
return {
|
||||
authenticatedAccounts: ["user-1", "user-2"],
|
||||
"user_user-1_organizationManagementPreferences_autoConfirmFingerPrints": true,
|
||||
"user_user-2_organizationManagementPreferences_autoConfirmFingerPrints": false,
|
||||
"user-1": {
|
||||
settings: {
|
||||
extra: "data",
|
||||
},
|
||||
extra: "data",
|
||||
},
|
||||
"user-2": {
|
||||
settings: {
|
||||
extra: "data",
|
||||
},
|
||||
extra: "data",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("AutoConfirmFingerPrintsMigrator", () => {
|
||||
const migrator = new AutoConfirmFingerPrintsMigrator(42, 43);
|
||||
|
||||
it("should migrate the autoConfirmFingerPrints property from the account settings object to a user StorageKey", async () => {
|
||||
const output = await runMigrator(migrator, {
|
||||
authenticatedAccounts: ["user-1", "user-2"] as const,
|
||||
"user-1": {
|
||||
settings: {
|
||||
autoConfirmFingerPrints: true,
|
||||
extra: "data",
|
||||
},
|
||||
extra: "data",
|
||||
},
|
||||
"user-2": {
|
||||
settings: {
|
||||
autoConfirmFingerPrints: false,
|
||||
extra: "data",
|
||||
},
|
||||
extra: "data",
|
||||
},
|
||||
});
|
||||
|
||||
expect(output).toEqual({
|
||||
authenticatedAccounts: ["user-1", "user-2"],
|
||||
"user_user-1_organizationManagementPreferences_autoConfirmFingerPrints": true,
|
||||
"user_user-2_organizationManagementPreferences_autoConfirmFingerPrints": false,
|
||||
"user-1": {
|
||||
settings: {
|
||||
extra: "data",
|
||||
},
|
||||
extra: "data",
|
||||
},
|
||||
"user-2": {
|
||||
settings: {
|
||||
extra: "data",
|
||||
},
|
||||
extra: "data",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
describe("rollback", () => {
|
||||
let helper: MockProxy<MigrationHelper>;
|
||||
let sut: AutoConfirmFingerPrintsMigrator;
|
||||
|
||||
const keyDefinitionLike = {
|
||||
key: "autoConfirmFingerPrints",
|
||||
stateDefinition: {
|
||||
name: "organizationManagementPreferences",
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(rollbackJSON(), 43);
|
||||
sut = new AutoConfirmFingerPrintsMigrator(42, 43);
|
||||
});
|
||||
|
||||
it("should null the autoConfirmFingerPrints user StorageKey for each account", async () => {
|
||||
await sut.rollback(helper);
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("user-1", keyDefinitionLike, null);
|
||||
});
|
||||
|
||||
it("should add the autoConfirmFingerPrints property back to the account settings object", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.set).toHaveBeenCalledWith("user-1", {
|
||||
settings: {
|
||||
autoConfirmFingerPrints: true,
|
||||
extra: "data",
|
||||
},
|
||||
extra: "data",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { KeyDefinitionLike, MigrationHelper, StateDefinitionLike } from "../migration-helper";
|
||||
import { Migrator } from "../migrator";
|
||||
|
||||
type ExpectedAccountState = {
|
||||
settings?: { autoConfirmFingerPrints?: boolean };
|
||||
};
|
||||
|
||||
const ORGANIZATION_MANAGEMENT_PREFERENCES: StateDefinitionLike = {
|
||||
name: "organizationManagementPreferences",
|
||||
};
|
||||
|
||||
const AUTO_CONFIRM_FINGERPRINTS: KeyDefinitionLike = {
|
||||
key: "autoConfirmFingerPrints",
|
||||
stateDefinition: ORGANIZATION_MANAGEMENT_PREFERENCES,
|
||||
};
|
||||
|
||||
export class AutoConfirmFingerPrintsMigrator extends Migrator<42, 43> {
|
||||
async migrate(helper: MigrationHelper): Promise<void> {
|
||||
const legacyAccounts = await helper.getAccounts<ExpectedAccountState>();
|
||||
|
||||
await Promise.all(
|
||||
legacyAccounts.map(async ({ userId, account }) => {
|
||||
if (account?.settings?.autoConfirmFingerPrints != null) {
|
||||
await helper.setToUser(
|
||||
userId,
|
||||
AUTO_CONFIRM_FINGERPRINTS,
|
||||
account.settings.autoConfirmFingerPrints,
|
||||
);
|
||||
delete account?.settings?.autoConfirmFingerPrints;
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async rollback(helper: MigrationHelper): Promise<void> {
|
||||
async function rollbackUser(userId: string, account: ExpectedAccountState) {
|
||||
let updatedAccount = false;
|
||||
const autoConfirmFingerPrints = await helper.getFromUser<boolean>(
|
||||
userId,
|
||||
AUTO_CONFIRM_FINGERPRINTS,
|
||||
);
|
||||
|
||||
if (autoConfirmFingerPrints) {
|
||||
if (!account) {
|
||||
account = {};
|
||||
}
|
||||
|
||||
updatedAccount = true;
|
||||
account.settings.autoConfirmFingerPrints = autoConfirmFingerPrints;
|
||||
await helper.setToUser(userId, AUTO_CONFIRM_FINGERPRINTS, null);
|
||||
}
|
||||
|
||||
if (updatedAccount) {
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
}
|
||||
|
||||
const accounts = await helper.getAccounts<ExpectedAccountState>();
|
||||
|
||||
await Promise.all(accounts.map(({ userId, account }) => rollbackUser(userId, account)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
import { any, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { MigrationHelper } from "../migration-helper";
|
||||
import { mockMigrationHelper } from "../migration-helper.spec";
|
||||
|
||||
import { UserDecryptionOptionsMigrator } from "./44-move-user-decryption-options-to-state-provider";
|
||||
|
||||
function exampleJSON() {
|
||||
return {
|
||||
global: {
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
authenticatedAccounts: ["FirstAccount", "SecondAccount", "ThirdAccount"],
|
||||
FirstAccount: {
|
||||
decryptionOptions: {
|
||||
hasMasterPassword: true,
|
||||
trustedDeviceOption: {
|
||||
hasAdminApproval: false,
|
||||
hasLoginApprovingDevice: false,
|
||||
hasManageResetPasswordPermission: true,
|
||||
},
|
||||
keyConnectorOption: {
|
||||
keyConnectorUrl: "https://keyconnector.bitwarden.com",
|
||||
},
|
||||
},
|
||||
profile: {
|
||||
otherStuff: "overStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
},
|
||||
SecondAccount: {
|
||||
decryptionOptions: {
|
||||
hasMasterPassword: false,
|
||||
trustedDeviceOption: {
|
||||
hasAdminApproval: true,
|
||||
hasLoginApprovingDevice: true,
|
||||
hasManageResetPasswordPermission: true,
|
||||
},
|
||||
keyConnectorOption: {
|
||||
keyConnectorUrl: "https://selfhosted.bitwarden.com",
|
||||
},
|
||||
},
|
||||
profile: {
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function rollbackJSON() {
|
||||
return {
|
||||
user_FirstAccount_decryptionOptions_userDecryptionOptions: {
|
||||
hasMasterPassword: true,
|
||||
trustedDeviceOption: {
|
||||
hasAdminApproval: false,
|
||||
hasLoginApprovingDevice: false,
|
||||
hasManageResetPasswordPermission: true,
|
||||
},
|
||||
keyConnectorOption: {
|
||||
keyConnectorUrl: "https://keyconnector.bitwarden.com",
|
||||
},
|
||||
},
|
||||
user_SecondAccount_decryptionOptions_userDecryptionOptions: {
|
||||
hasMasterPassword: false,
|
||||
trustedDeviceOption: {
|
||||
hasAdminApproval: true,
|
||||
hasLoginApprovingDevice: true,
|
||||
hasManageResetPasswordPermission: true,
|
||||
},
|
||||
keyConnectorOption: {
|
||||
keyConnectorUrl: "https://selfhosted.bitwarden.com",
|
||||
},
|
||||
},
|
||||
user_ThirdAccount_decryptionOptions_userDecryptionOptions: {},
|
||||
global: {
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
authenticatedAccounts: ["FirstAccount", "SecondAccount", "ThirdAccount"],
|
||||
FirstAccount: {
|
||||
decryptionOptions: {
|
||||
hasMasterPassword: true,
|
||||
trustedDeviceOption: {
|
||||
hasAdminApproval: false,
|
||||
hasLoginApprovingDevice: false,
|
||||
hasManageResetPasswordPermission: true,
|
||||
},
|
||||
keyConnectorOption: {
|
||||
keyConnectorUrl: "https://keyconnector.bitwarden.com",
|
||||
},
|
||||
},
|
||||
profile: {
|
||||
otherStuff: "overStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
},
|
||||
SecondAccount: {
|
||||
decryptionOptions: {
|
||||
hasMasterPassword: false,
|
||||
trustedDeviceOption: {
|
||||
hasAdminApproval: true,
|
||||
hasLoginApprovingDevice: true,
|
||||
hasManageResetPasswordPermission: true,
|
||||
},
|
||||
keyConnectorOption: {
|
||||
keyConnectorUrl: "https://selfhosted.bitwarden.com",
|
||||
},
|
||||
},
|
||||
profile: {
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("UserDecryptionOptionsMigrator", () => {
|
||||
let helper: MockProxy<MigrationHelper>;
|
||||
let sut: UserDecryptionOptionsMigrator;
|
||||
const keyDefinitionLike = {
|
||||
key: "decryptionOptions",
|
||||
stateDefinition: {
|
||||
name: "userDecryptionOptions",
|
||||
},
|
||||
};
|
||||
|
||||
describe("migrate", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(exampleJSON(), 43);
|
||||
sut = new UserDecryptionOptionsMigrator(43, 44);
|
||||
});
|
||||
|
||||
it("should remove decryptionOptions from all accounts", async () => {
|
||||
await sut.migrate(helper);
|
||||
expect(helper.set).toHaveBeenCalledWith("FirstAccount", {
|
||||
profile: {
|
||||
otherStuff: "overStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
});
|
||||
expect(helper.set).toHaveBeenCalledWith("SecondAccount", {
|
||||
profile: {
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
});
|
||||
});
|
||||
|
||||
it("should set decryptionOptions provider value for each account", async () => {
|
||||
await sut.migrate(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("FirstAccount", keyDefinitionLike, {
|
||||
hasMasterPassword: true,
|
||||
trustedDeviceOption: {
|
||||
hasAdminApproval: false,
|
||||
hasLoginApprovingDevice: false,
|
||||
hasManageResetPasswordPermission: true,
|
||||
},
|
||||
keyConnectorOption: {
|
||||
keyConnectorUrl: "https://keyconnector.bitwarden.com",
|
||||
},
|
||||
});
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("SecondAccount", keyDefinitionLike, {
|
||||
hasMasterPassword: false,
|
||||
trustedDeviceOption: {
|
||||
hasAdminApproval: true,
|
||||
hasLoginApprovingDevice: true,
|
||||
hasManageResetPasswordPermission: true,
|
||||
},
|
||||
keyConnectorOption: {
|
||||
keyConnectorUrl: "https://selfhosted.bitwarden.com",
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("rollback", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(rollbackJSON(), 44);
|
||||
sut = new UserDecryptionOptionsMigrator(43, 44);
|
||||
});
|
||||
|
||||
it.each(["FirstAccount", "SecondAccount", "ThirdAccount"])(
|
||||
"should null out new values",
|
||||
async (userId) => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(userId, keyDefinitionLike, null);
|
||||
},
|
||||
);
|
||||
|
||||
it("should add explicit value back to accounts", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.set).toHaveBeenCalledWith("FirstAccount", {
|
||||
decryptionOptions: {
|
||||
hasMasterPassword: true,
|
||||
trustedDeviceOption: {
|
||||
hasAdminApproval: false,
|
||||
hasLoginApprovingDevice: false,
|
||||
hasManageResetPasswordPermission: true,
|
||||
},
|
||||
keyConnectorOption: {
|
||||
keyConnectorUrl: "https://keyconnector.bitwarden.com",
|
||||
},
|
||||
},
|
||||
profile: {
|
||||
otherStuff: "overStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
});
|
||||
expect(helper.set).toHaveBeenCalledWith("SecondAccount", {
|
||||
decryptionOptions: {
|
||||
hasMasterPassword: false,
|
||||
trustedDeviceOption: {
|
||||
hasAdminApproval: true,
|
||||
hasLoginApprovingDevice: true,
|
||||
hasManageResetPasswordPermission: true,
|
||||
},
|
||||
keyConnectorOption: {
|
||||
keyConnectorUrl: "https://selfhosted.bitwarden.com",
|
||||
},
|
||||
},
|
||||
profile: {
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
});
|
||||
});
|
||||
|
||||
it("should not try to restore values to missing accounts", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.set).not.toHaveBeenCalledWith("ThirdAccount", any());
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
|
||||
import { Migrator } from "../migrator";
|
||||
|
||||
type DecryptionOptionsType = {
|
||||
hasMasterPassword: boolean;
|
||||
trustedDeviceOption?: {
|
||||
hasAdminApproval: boolean;
|
||||
hasLoginApprovingDevice: boolean;
|
||||
hasManageResetPasswordPermission: boolean;
|
||||
};
|
||||
keyConnectorOption?: {
|
||||
keyConnectorUrl: string;
|
||||
};
|
||||
};
|
||||
|
||||
type ExpectedAccountType = {
|
||||
decryptionOptions?: DecryptionOptionsType;
|
||||
};
|
||||
|
||||
const USER_DECRYPTION_OPTIONS: KeyDefinitionLike = {
|
||||
key: "decryptionOptions",
|
||||
stateDefinition: {
|
||||
name: "userDecryptionOptions",
|
||||
},
|
||||
};
|
||||
|
||||
export class UserDecryptionOptionsMigrator extends Migrator<43, 44> {
|
||||
async migrate(helper: MigrationHelper): Promise<void> {
|
||||
const accounts = await helper.getAccounts<ExpectedAccountType>();
|
||||
async function migrateAccount(userId: string, account: ExpectedAccountType): Promise<void> {
|
||||
const value = account?.decryptionOptions;
|
||||
if (value != null) {
|
||||
await helper.setToUser(userId, USER_DECRYPTION_OPTIONS, value);
|
||||
delete account.decryptionOptions;
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all([...accounts.map(({ userId, account }) => migrateAccount(userId, account))]);
|
||||
}
|
||||
async rollback(helper: MigrationHelper): Promise<void> {
|
||||
const accounts = await helper.getAccounts<ExpectedAccountType>();
|
||||
async function rollbackAccount(userId: string, account: ExpectedAccountType): Promise<void> {
|
||||
const value: DecryptionOptionsType = await helper.getFromUser(
|
||||
userId,
|
||||
USER_DECRYPTION_OPTIONS,
|
||||
);
|
||||
if (account) {
|
||||
account.decryptionOptions = Object.assign(account.decryptionOptions, value);
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
await helper.setToUser(userId, USER_DECRYPTION_OPTIONS, null);
|
||||
}
|
||||
|
||||
await Promise.all([...accounts.map(({ userId, account }) => rollbackAccount(userId, account))]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
import { runMigrator } from "../migration-helper.spec";
|
||||
|
||||
import { MergeEnvironmentState } from "./45-merge-environment-state";
|
||||
|
||||
describe("MergeEnvironmentState", () => {
|
||||
const migrator = new MergeEnvironmentState(44, 45);
|
||||
|
||||
it("can migrate all data", async () => {
|
||||
const output = await runMigrator(migrator, {
|
||||
authenticatedAccounts: ["user1", "user2"],
|
||||
global: {
|
||||
extra: "data",
|
||||
},
|
||||
global_environment_region: "US",
|
||||
global_environment_urls: {
|
||||
base: "example.com",
|
||||
},
|
||||
user1: {
|
||||
extra: "data",
|
||||
settings: {
|
||||
extra: "data",
|
||||
},
|
||||
},
|
||||
user2: {
|
||||
extra: "data",
|
||||
settings: {
|
||||
extra: "data",
|
||||
},
|
||||
},
|
||||
extra: "data",
|
||||
user_user1_environment_region: "US",
|
||||
user_user2_environment_region: "EU",
|
||||
user_user1_environment_urls: {
|
||||
base: "example.com",
|
||||
},
|
||||
user_user2_environment_urls: {
|
||||
base: "other.example.com",
|
||||
},
|
||||
});
|
||||
|
||||
expect(output).toEqual({
|
||||
authenticatedAccounts: ["user1", "user2"],
|
||||
global: {
|
||||
extra: "data",
|
||||
},
|
||||
global_environment_environment: {
|
||||
region: "US",
|
||||
urls: {
|
||||
base: "example.com",
|
||||
},
|
||||
},
|
||||
user1: {
|
||||
extra: "data",
|
||||
settings: {
|
||||
extra: "data",
|
||||
},
|
||||
},
|
||||
user2: {
|
||||
extra: "data",
|
||||
settings: {
|
||||
extra: "data",
|
||||
},
|
||||
},
|
||||
extra: "data",
|
||||
user_user1_environment_environment: {
|
||||
region: "US",
|
||||
urls: {
|
||||
base: "example.com",
|
||||
},
|
||||
},
|
||||
user_user2_environment_environment: {
|
||||
region: "EU",
|
||||
urls: {
|
||||
base: "other.example.com",
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("handles missing parts", async () => {
|
||||
const output = await runMigrator(migrator, {
|
||||
authenticatedAccounts: ["user1", "user2"],
|
||||
global: {
|
||||
extra: "data",
|
||||
},
|
||||
user1: {
|
||||
extra: "data",
|
||||
settings: {
|
||||
extra: "data",
|
||||
},
|
||||
},
|
||||
user2: null,
|
||||
});
|
||||
|
||||
expect(output).toEqual({
|
||||
authenticatedAccounts: ["user1", "user2"],
|
||||
global: {
|
||||
extra: "data",
|
||||
},
|
||||
user1: {
|
||||
extra: "data",
|
||||
settings: {
|
||||
extra: "data",
|
||||
},
|
||||
},
|
||||
user2: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("can migrate only global data", async () => {
|
||||
const output = await runMigrator(migrator, {
|
||||
authenticatedAccounts: [],
|
||||
global_environment_region: "Self-Hosted",
|
||||
global: {},
|
||||
});
|
||||
|
||||
expect(output).toEqual({
|
||||
authenticatedAccounts: [],
|
||||
global_environment_environment: {
|
||||
region: "Self-Hosted",
|
||||
urls: undefined,
|
||||
},
|
||||
global: {},
|
||||
});
|
||||
});
|
||||
|
||||
it("can migrate only user state", async () => {
|
||||
const output = await runMigrator(migrator, {
|
||||
authenticatedAccounts: ["user1"] as const,
|
||||
global: null,
|
||||
user1: { settings: {} },
|
||||
user_user1_environment_region: "Self-Hosted",
|
||||
user_user1_environment_urls: {
|
||||
base: "some-base-url",
|
||||
api: "some-api-url",
|
||||
identity: "some-identity-url",
|
||||
icons: "some-icons-url",
|
||||
notifications: "some-notifications-url",
|
||||
events: "some-events-url",
|
||||
webVault: "some-webVault-url",
|
||||
keyConnector: "some-keyConnector-url",
|
||||
},
|
||||
});
|
||||
|
||||
expect(output).toEqual({
|
||||
authenticatedAccounts: ["user1"] as const,
|
||||
global: null,
|
||||
user1: { settings: {} },
|
||||
user_user1_environment_environment: {
|
||||
region: "Self-Hosted",
|
||||
urls: {
|
||||
base: "some-base-url",
|
||||
api: "some-api-url",
|
||||
identity: "some-identity-url",
|
||||
icons: "some-icons-url",
|
||||
notifications: "some-notifications-url",
|
||||
events: "some-events-url",
|
||||
webVault: "some-webVault-url",
|
||||
keyConnector: "some-keyConnector-url",
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,83 @@
|
||||
import { KeyDefinitionLike, MigrationHelper, StateDefinitionLike } from "../migration-helper";
|
||||
import { Migrator } from "../migrator";
|
||||
|
||||
const ENVIRONMENT_STATE: StateDefinitionLike = { name: "environment" };
|
||||
|
||||
const ENVIRONMENT_REGION: KeyDefinitionLike = {
|
||||
key: "region",
|
||||
stateDefinition: ENVIRONMENT_STATE,
|
||||
};
|
||||
|
||||
const ENVIRONMENT_URLS: KeyDefinitionLike = {
|
||||
key: "urls",
|
||||
stateDefinition: ENVIRONMENT_STATE,
|
||||
};
|
||||
|
||||
const ENVIRONMENT_ENVIRONMENT: KeyDefinitionLike = {
|
||||
key: "environment",
|
||||
stateDefinition: ENVIRONMENT_STATE,
|
||||
};
|
||||
|
||||
export class MergeEnvironmentState extends Migrator<44, 45> {
|
||||
async migrate(helper: MigrationHelper): Promise<void> {
|
||||
const accounts = await helper.getAccounts<unknown>();
|
||||
|
||||
async function migrateAccount(userId: string, account: unknown): Promise<void> {
|
||||
const region = await helper.getFromUser(userId, ENVIRONMENT_REGION);
|
||||
const urls = await helper.getFromUser(userId, ENVIRONMENT_URLS);
|
||||
|
||||
if (region == null && urls == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
await helper.setToUser(userId, ENVIRONMENT_ENVIRONMENT, {
|
||||
region,
|
||||
urls,
|
||||
});
|
||||
await helper.removeFromUser(userId, ENVIRONMENT_REGION);
|
||||
await helper.removeFromUser(userId, ENVIRONMENT_URLS);
|
||||
}
|
||||
|
||||
await Promise.all([...accounts.map(({ userId, account }) => migrateAccount(userId, account))]);
|
||||
|
||||
const region = await helper.getFromGlobal(ENVIRONMENT_REGION);
|
||||
const urls = await helper.getFromGlobal(ENVIRONMENT_URLS);
|
||||
|
||||
if (region == null && urls == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
await helper.setToGlobal(ENVIRONMENT_ENVIRONMENT, {
|
||||
region,
|
||||
urls,
|
||||
});
|
||||
await helper.removeFromGlobal(ENVIRONMENT_REGION);
|
||||
await helper.removeFromGlobal(ENVIRONMENT_URLS);
|
||||
}
|
||||
|
||||
async rollback(helper: MigrationHelper): Promise<void> {
|
||||
const accounts = await helper.getAccounts<unknown>();
|
||||
|
||||
async function rollbackAccount(userId: string, account: unknown): Promise<void> {
|
||||
const state = (await helper.getFromUser(userId, ENVIRONMENT_ENVIRONMENT)) as {
|
||||
region: string;
|
||||
urls: string;
|
||||
} | null;
|
||||
|
||||
await helper.setToUser(userId, ENVIRONMENT_REGION, state?.region);
|
||||
await helper.setToUser(userId, ENVIRONMENT_URLS, state?.urls);
|
||||
await helper.removeFromUser(userId, ENVIRONMENT_ENVIRONMENT);
|
||||
}
|
||||
|
||||
await Promise.all([...accounts.map(({ userId, account }) => rollbackAccount(userId, account))]);
|
||||
|
||||
const state = (await helper.getFromGlobal(ENVIRONMENT_ENVIRONMENT)) as {
|
||||
region: string;
|
||||
urls: string;
|
||||
} | null;
|
||||
|
||||
await helper.setToGlobal(ENVIRONMENT_REGION, state?.region);
|
||||
await helper.setToGlobal(ENVIRONMENT_URLS, state?.urls);
|
||||
await helper.removeFromGlobal(ENVIRONMENT_ENVIRONMENT);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { runMigrator } from "../migration-helper.spec";
|
||||
import { IRREVERSIBLE } from "../migrator";
|
||||
|
||||
import { DeleteBiometricPromptCancelledData } from "./46-delete-orphaned-biometric-prompt-data";
|
||||
|
||||
describe("MoveThemeToStateProviders", () => {
|
||||
const sut = new DeleteBiometricPromptCancelledData(45, 46);
|
||||
|
||||
describe("migrate", () => {
|
||||
it("deletes promptCancelled from all users", async () => {
|
||||
const output = await runMigrator(sut, {
|
||||
authenticatedAccounts: ["user-1", "user-2"],
|
||||
"user_user-1_biometricSettings_promptCancelled": true,
|
||||
"user_user-2_biometricSettings_promptCancelled": false,
|
||||
});
|
||||
|
||||
expect(output).toEqual({
|
||||
authenticatedAccounts: ["user-1", "user-2"],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("rollback", () => {
|
||||
it("is irreversible", async () => {
|
||||
await expect(runMigrator(sut, {}, "rollback")).rejects.toThrow(IRREVERSIBLE);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
|
||||
import { IRREVERSIBLE, Migrator } from "../migrator";
|
||||
|
||||
export const PROMPT_CANCELLED: KeyDefinitionLike = {
|
||||
key: "promptCancelled",
|
||||
stateDefinition: { name: "biometricSettings" },
|
||||
};
|
||||
|
||||
export class DeleteBiometricPromptCancelledData extends Migrator<45, 46> {
|
||||
async migrate(helper: MigrationHelper): Promise<void> {
|
||||
await Promise.all(
|
||||
(await helper.getAccounts()).map(async ({ userId }) => {
|
||||
if (helper.getFromUser(userId, PROMPT_CANCELLED) != null) {
|
||||
await helper.removeFromUser(userId, PROMPT_CANCELLED);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async rollback(helper: MigrationHelper): Promise<void> {
|
||||
throw IRREVERSIBLE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
import { runMigrator } from "../migration-helper.spec";
|
||||
|
||||
import { MoveDesktopSettingsMigrator } from "./47-move-desktop-settings";
|
||||
|
||||
describe("MoveDesktopSettings", () => {
|
||||
const sut = new MoveDesktopSettingsMigrator(46, 47);
|
||||
|
||||
it("can migrate truthy values", async () => {
|
||||
const output = await runMigrator(sut, {
|
||||
authenticatedAccounts: ["user1"],
|
||||
global: {
|
||||
window: {
|
||||
width: 400,
|
||||
height: 400,
|
||||
displayBounds: {
|
||||
height: 200,
|
||||
width: 200,
|
||||
x: 200,
|
||||
y: 200,
|
||||
},
|
||||
},
|
||||
enableAlwaysOnTop: true,
|
||||
enableCloseToTray: true,
|
||||
enableMinimizeToTray: true,
|
||||
enableStartToTray: true,
|
||||
enableTray: true,
|
||||
openAtLogin: true,
|
||||
alwaysShowDock: true,
|
||||
},
|
||||
user1: {
|
||||
settings: {
|
||||
enableAlwaysOnTop: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(output).toEqual({
|
||||
authenticatedAccounts: ["user1"],
|
||||
global: {},
|
||||
global_desktopSettings_window: {
|
||||
width: 400,
|
||||
height: 400,
|
||||
displayBounds: {
|
||||
height: 200,
|
||||
width: 200,
|
||||
x: 200,
|
||||
y: 200,
|
||||
},
|
||||
},
|
||||
global_desktopSettings_closeToTray: true,
|
||||
global_desktopSettings_minimizeToTray: true,
|
||||
global_desktopSettings_startToTray: true,
|
||||
global_desktopSettings_trayEnabled: true,
|
||||
global_desktopSettings_openAtLogin: true,
|
||||
global_desktopSettings_alwaysShowDock: true,
|
||||
global_desktopSettings_alwaysOnTop: true,
|
||||
user1: {
|
||||
settings: {},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("can migrate falsey values", async () => {
|
||||
const output = await runMigrator(sut, {
|
||||
authenticatedAccounts: ["user1"],
|
||||
global: {
|
||||
window: null,
|
||||
enableCloseToTray: false,
|
||||
enableMinimizeToTray: false,
|
||||
enableStartToTray: false,
|
||||
enableTray: false,
|
||||
openAtLogin: false,
|
||||
alwaysShowDock: false,
|
||||
enableAlwaysOnTop: false,
|
||||
},
|
||||
user1: {
|
||||
settings: {
|
||||
enableAlwaysOnTop: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(output).toEqual({
|
||||
authenticatedAccounts: ["user1"],
|
||||
global: {},
|
||||
global_desktopSettings_window: null,
|
||||
global_desktopSettings_closeToTray: false,
|
||||
global_desktopSettings_minimizeToTray: false,
|
||||
global_desktopSettings_startToTray: false,
|
||||
global_desktopSettings_trayEnabled: false,
|
||||
global_desktopSettings_openAtLogin: false,
|
||||
global_desktopSettings_alwaysShowDock: false,
|
||||
global_desktopSettings_alwaysOnTop: false,
|
||||
user1: {
|
||||
settings: {},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("can migrate even if none of our values are found", async () => {
|
||||
//
|
||||
const output = await runMigrator(sut, {
|
||||
authenticatedAccounts: ["user1"] as const,
|
||||
global: {
|
||||
anotherSetting: "",
|
||||
},
|
||||
});
|
||||
|
||||
expect(output).toEqual({
|
||||
authenticatedAccounts: ["user1"] as const,
|
||||
global: {
|
||||
anotherSetting: "",
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,128 @@
|
||||
import { KeyDefinitionLike, MigrationHelper, StateDefinitionLike } from "../migration-helper";
|
||||
import { IRREVERSIBLE, Migrator } from "../migrator";
|
||||
|
||||
type ExpectedGlobalType = {
|
||||
window?: object;
|
||||
enableTray?: boolean;
|
||||
enableMinimizeToTray?: boolean;
|
||||
enableCloseToTray?: boolean;
|
||||
enableStartToTray?: boolean;
|
||||
openAtLogin?: boolean;
|
||||
alwaysShowDock?: boolean;
|
||||
enableAlwaysOnTop?: boolean;
|
||||
};
|
||||
|
||||
type ExpectedAccountType = {
|
||||
settings?: {
|
||||
enableAlwaysOnTop?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
const DESKTOP_SETTINGS_STATE: StateDefinitionLike = { name: "desktopSettings" };
|
||||
|
||||
const WINDOW_KEY: KeyDefinitionLike = { key: "window", stateDefinition: DESKTOP_SETTINGS_STATE };
|
||||
|
||||
const CLOSE_TO_TRAY_KEY: KeyDefinitionLike = {
|
||||
key: "closeToTray",
|
||||
stateDefinition: DESKTOP_SETTINGS_STATE,
|
||||
};
|
||||
const MINIMIZE_TO_TRAY_KEY: KeyDefinitionLike = {
|
||||
key: "minimizeToTray",
|
||||
stateDefinition: DESKTOP_SETTINGS_STATE,
|
||||
};
|
||||
const START_TO_TRAY_KEY: KeyDefinitionLike = {
|
||||
key: "startToTray",
|
||||
stateDefinition: DESKTOP_SETTINGS_STATE,
|
||||
};
|
||||
const TRAY_ENABLED_KEY: KeyDefinitionLike = {
|
||||
key: "trayEnabled",
|
||||
stateDefinition: DESKTOP_SETTINGS_STATE,
|
||||
};
|
||||
const OPEN_AT_LOGIN_KEY: KeyDefinitionLike = {
|
||||
key: "openAtLogin",
|
||||
stateDefinition: DESKTOP_SETTINGS_STATE,
|
||||
};
|
||||
const ALWAYS_SHOW_DOCK_KEY: KeyDefinitionLike = {
|
||||
key: "alwaysShowDock",
|
||||
stateDefinition: DESKTOP_SETTINGS_STATE,
|
||||
};
|
||||
|
||||
const ALWAYS_ON_TOP_KEY: KeyDefinitionLike = {
|
||||
key: "alwaysOnTop",
|
||||
stateDefinition: DESKTOP_SETTINGS_STATE,
|
||||
};
|
||||
|
||||
export class MoveDesktopSettingsMigrator extends Migrator<46, 47> {
|
||||
async migrate(helper: MigrationHelper): Promise<void> {
|
||||
const legacyGlobal = await helper.get<ExpectedGlobalType>("global");
|
||||
|
||||
let updatedGlobal = false;
|
||||
if (legacyGlobal?.window !== undefined) {
|
||||
await helper.setToGlobal(WINDOW_KEY, legacyGlobal.window);
|
||||
updatedGlobal = true;
|
||||
delete legacyGlobal.window;
|
||||
}
|
||||
|
||||
if (legacyGlobal?.enableCloseToTray != null) {
|
||||
await helper.setToGlobal(CLOSE_TO_TRAY_KEY, legacyGlobal.enableCloseToTray);
|
||||
updatedGlobal = true;
|
||||
delete legacyGlobal.enableCloseToTray;
|
||||
}
|
||||
|
||||
if (legacyGlobal?.enableMinimizeToTray != null) {
|
||||
await helper.setToGlobal(MINIMIZE_TO_TRAY_KEY, legacyGlobal.enableMinimizeToTray);
|
||||
updatedGlobal = true;
|
||||
delete legacyGlobal.enableMinimizeToTray;
|
||||
}
|
||||
|
||||
if (legacyGlobal?.enableStartToTray != null) {
|
||||
await helper.setToGlobal(START_TO_TRAY_KEY, legacyGlobal.enableStartToTray);
|
||||
updatedGlobal = true;
|
||||
delete legacyGlobal.enableStartToTray;
|
||||
}
|
||||
|
||||
if (legacyGlobal?.enableTray != null) {
|
||||
await helper.setToGlobal(TRAY_ENABLED_KEY, legacyGlobal.enableTray);
|
||||
updatedGlobal = true;
|
||||
delete legacyGlobal.enableTray;
|
||||
}
|
||||
|
||||
if (legacyGlobal?.openAtLogin != null) {
|
||||
await helper.setToGlobal(OPEN_AT_LOGIN_KEY, legacyGlobal.openAtLogin);
|
||||
updatedGlobal = true;
|
||||
delete legacyGlobal.openAtLogin;
|
||||
}
|
||||
|
||||
if (legacyGlobal?.alwaysShowDock != null) {
|
||||
await helper.setToGlobal(ALWAYS_SHOW_DOCK_KEY, legacyGlobal.alwaysShowDock);
|
||||
updatedGlobal = true;
|
||||
delete legacyGlobal.alwaysShowDock;
|
||||
}
|
||||
|
||||
if (legacyGlobal?.enableAlwaysOnTop != null) {
|
||||
await helper.setToGlobal(ALWAYS_ON_TOP_KEY, legacyGlobal.enableAlwaysOnTop);
|
||||
updatedGlobal = true;
|
||||
delete legacyGlobal.enableAlwaysOnTop;
|
||||
}
|
||||
|
||||
if (updatedGlobal) {
|
||||
await helper.set("global", legacyGlobal);
|
||||
}
|
||||
|
||||
async function migrateAccount(userId: string, account: ExpectedAccountType) {
|
||||
// We only migrate the global setting for this, if we find it on the account object
|
||||
// just delete it.
|
||||
if (account?.settings?.enableAlwaysOnTop != null) {
|
||||
delete account.settings.enableAlwaysOnTop;
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
}
|
||||
|
||||
const accounts = await helper.getAccounts<ExpectedAccountType>();
|
||||
await Promise.all(accounts.map(({ userId, account }) => migrateAccount(userId, account)));
|
||||
}
|
||||
|
||||
rollback(helper: MigrationHelper): Promise<void> {
|
||||
throw IRREVERSIBLE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { runMigrator } from "../migration-helper.spec";
|
||||
|
||||
import { MoveDdgToStateProviderMigrator } from "./48-move-ddg-to-state-provider";
|
||||
|
||||
describe("MoveDdgToStateProviderMigrator", () => {
|
||||
const migrator = new MoveDdgToStateProviderMigrator(47, 48);
|
||||
|
||||
it("migrate", async () => {
|
||||
const output = await runMigrator(migrator, {
|
||||
global: {
|
||||
enableDuckDuckGoBrowserIntegration: true,
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
otherStuff: "otherStuff2",
|
||||
});
|
||||
|
||||
expect(output).toEqual({
|
||||
global_autofillSettings_enableDuckDuckGoBrowserIntegration: true,
|
||||
global: {
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
otherStuff: "otherStuff2",
|
||||
});
|
||||
});
|
||||
|
||||
it("rollback", async () => {
|
||||
const output = await runMigrator(
|
||||
migrator,
|
||||
{
|
||||
global_autofillSettings_enableDuckDuckGoBrowserIntegration: true,
|
||||
global: {
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
"rollback",
|
||||
);
|
||||
|
||||
expect(output).toEqual({
|
||||
global: {
|
||||
enableDuckDuckGoBrowserIntegration: true,
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
otherStuff: "otherStuff2",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
|
||||
import { Migrator } from "../migrator";
|
||||
|
||||
type ExpectedGlobal = {
|
||||
enableDuckDuckGoBrowserIntegration?: boolean;
|
||||
};
|
||||
|
||||
export const DDG_KEY: KeyDefinitionLike = {
|
||||
key: "enableDuckDuckGoBrowserIntegration",
|
||||
stateDefinition: {
|
||||
name: "autofillSettings",
|
||||
},
|
||||
};
|
||||
|
||||
export class MoveDdgToStateProviderMigrator extends Migrator<47, 48> {
|
||||
async migrate(helper: MigrationHelper): Promise<void> {
|
||||
// global state
|
||||
const global = await helper.get<ExpectedGlobal>("global");
|
||||
if (global?.enableDuckDuckGoBrowserIntegration == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
await helper.setToGlobal(DDG_KEY, global.enableDuckDuckGoBrowserIntegration);
|
||||
delete global.enableDuckDuckGoBrowserIntegration;
|
||||
await helper.set("global", global);
|
||||
}
|
||||
|
||||
async rollback(helper: MigrationHelper): Promise<void> {
|
||||
const enableDdg = await helper.getFromGlobal<boolean>(DDG_KEY);
|
||||
|
||||
if (!enableDdg) {
|
||||
return;
|
||||
}
|
||||
|
||||
const global = (await helper.get<ExpectedGlobal>("global")) ?? {};
|
||||
global.enableDuckDuckGoBrowserIntegration = enableDdg;
|
||||
await helper.set("global", global);
|
||||
await helper.removeFromGlobal(DDG_KEY);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import { runMigrator } from "../migration-helper.spec";
|
||||
|
||||
import { AccountServerConfigMigrator } from "./49-move-account-server-configs";
|
||||
|
||||
describe("AccountServerConfigMigrator", () => {
|
||||
const migrator = new AccountServerConfigMigrator(48, 49);
|
||||
|
||||
describe("all data", () => {
|
||||
function toMigrate() {
|
||||
return {
|
||||
authenticatedAccounts: ["user1", "user2"],
|
||||
user1: {
|
||||
settings: {
|
||||
serverConfig: {
|
||||
config: "user1 server config",
|
||||
},
|
||||
},
|
||||
},
|
||||
user2: {
|
||||
settings: {
|
||||
serverConfig: {
|
||||
config: "user2 server config",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function migrated() {
|
||||
return {
|
||||
authenticatedAccounts: ["user1", "user2"],
|
||||
|
||||
user1: {
|
||||
settings: {},
|
||||
},
|
||||
user2: {
|
||||
settings: {},
|
||||
},
|
||||
user_user1_config_serverConfig: {
|
||||
config: "user1 server config",
|
||||
},
|
||||
user_user2_config_serverConfig: {
|
||||
config: "user2 server config",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function rolledBack(previous: object) {
|
||||
return {
|
||||
...previous,
|
||||
user_user1_config_serverConfig: null as unknown,
|
||||
user_user2_config_serverConfig: null as unknown,
|
||||
};
|
||||
}
|
||||
|
||||
it("migrates", async () => {
|
||||
const output = await runMigrator(migrator, toMigrate(), "migrate");
|
||||
expect(output).toEqual(migrated());
|
||||
});
|
||||
|
||||
it("rolls back", async () => {
|
||||
const output = await runMigrator(migrator, migrated(), "rollback");
|
||||
expect(output).toEqual(rolledBack(toMigrate()));
|
||||
});
|
||||
});
|
||||
|
||||
describe("missing parts", () => {
|
||||
function toMigrate() {
|
||||
return {
|
||||
authenticatedAccounts: ["user1", "user2"],
|
||||
user1: {
|
||||
settings: {
|
||||
serverConfig: {
|
||||
config: "user1 server config",
|
||||
},
|
||||
},
|
||||
},
|
||||
user2: null as unknown,
|
||||
};
|
||||
}
|
||||
|
||||
function migrated() {
|
||||
return {
|
||||
authenticatedAccounts: ["user1", "user2"],
|
||||
user1: {
|
||||
settings: {},
|
||||
},
|
||||
user2: null as unknown,
|
||||
user_user1_config_serverConfig: {
|
||||
config: "user1 server config",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function rollback(previous: object) {
|
||||
return {
|
||||
...previous,
|
||||
user_user1_config_serverConfig: null as unknown,
|
||||
};
|
||||
}
|
||||
|
||||
it("migrates", async () => {
|
||||
const output = await runMigrator(migrator, toMigrate(), "migrate");
|
||||
expect(output).toEqual(migrated());
|
||||
});
|
||||
|
||||
it("rolls back", async () => {
|
||||
const output = await runMigrator(migrator, migrated(), "rollback");
|
||||
expect(output).toEqual(rollback(toMigrate()));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
import { KeyDefinitionLike, MigrationHelper, StateDefinitionLike } from "../migration-helper";
|
||||
import { Migrator } from "../migrator";
|
||||
|
||||
const CONFIG_DISK: StateDefinitionLike = { name: "config" };
|
||||
export const USER_SERVER_CONFIG: KeyDefinitionLike = {
|
||||
stateDefinition: CONFIG_DISK,
|
||||
key: "serverConfig",
|
||||
};
|
||||
|
||||
// Note: no need to migrate global configs, they don't currently exist
|
||||
|
||||
type ExpectedAccountType = {
|
||||
settings?: {
|
||||
serverConfig?: unknown;
|
||||
};
|
||||
};
|
||||
|
||||
export class AccountServerConfigMigrator extends Migrator<48, 49> {
|
||||
async migrate(helper: MigrationHelper): Promise<void> {
|
||||
const accounts = await helper.getAccounts<ExpectedAccountType>();
|
||||
|
||||
async function migrateAccount(userId: string, account: ExpectedAccountType): Promise<void> {
|
||||
if (account?.settings?.serverConfig != null) {
|
||||
await helper.setToUser(userId, USER_SERVER_CONFIG, account.settings.serverConfig);
|
||||
delete account.settings.serverConfig;
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all([...accounts.map(({ userId, account }) => migrateAccount(userId, account))]);
|
||||
}
|
||||
|
||||
async rollback(helper: MigrationHelper): Promise<void> {
|
||||
const accounts = await helper.getAccounts<ExpectedAccountType>();
|
||||
|
||||
async function rollbackAccount(userId: string, account: ExpectedAccountType): Promise<void> {
|
||||
const serverConfig = await helper.getFromUser(userId, USER_SERVER_CONFIG);
|
||||
|
||||
if (serverConfig) {
|
||||
account ??= {};
|
||||
account.settings ??= {};
|
||||
|
||||
account.settings.serverConfig = serverConfig;
|
||||
await helper.setToUser(userId, USER_SERVER_CONFIG, null);
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all([...accounts.map(({ userId, account }) => rollbackAccount(userId, account))]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
import { MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { MigrationHelper } from "../migration-helper";
|
||||
import { mockMigrationHelper } from "../migration-helper.spec";
|
||||
|
||||
import { AddKeyTypeToOrgKeysMigrator } from "./5-add-key-type-to-org-keys";
|
||||
|
||||
function migrateExampleJSON() {
|
||||
return {
|
||||
global: {
|
||||
stateVersion: 4,
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
authenticatedAccounts: [
|
||||
"c493ed01-4e08-4e88-abc7-332f380ca760",
|
||||
"23e61a5f-2ece-4f5e-b499-f0bc489482a9",
|
||||
],
|
||||
"c493ed01-4e08-4e88-abc7-332f380ca760": {
|
||||
keys: {
|
||||
organizationKeys: {
|
||||
encrypted: {
|
||||
orgOneId: "orgOneEncKey",
|
||||
orgTwoId: "orgTwoEncKey",
|
||||
},
|
||||
},
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function rollbackExampleJSON() {
|
||||
return {
|
||||
global: {
|
||||
stateVersion: 5,
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
authenticatedAccounts: [
|
||||
"c493ed01-4e08-4e88-abc7-332f380ca760",
|
||||
"23e61a5f-2ece-4f5e-b499-f0bc489482a9",
|
||||
],
|
||||
"c493ed01-4e08-4e88-abc7-332f380ca760": {
|
||||
keys: {
|
||||
organizationKeys: {
|
||||
encrypted: {
|
||||
orgOneId: {
|
||||
type: "organization",
|
||||
key: "orgOneEncKey",
|
||||
},
|
||||
orgTwoId: {
|
||||
type: "organization",
|
||||
key: "orgTwoEncKey",
|
||||
},
|
||||
},
|
||||
},
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("AddKeyTypeToOrgKeysMigrator", () => {
|
||||
let helper: MockProxy<MigrationHelper>;
|
||||
let sut: AddKeyTypeToOrgKeysMigrator;
|
||||
|
||||
describe("migrate", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(migrateExampleJSON());
|
||||
sut = new AddKeyTypeToOrgKeysMigrator(4, 5);
|
||||
});
|
||||
|
||||
it("should add organization type to organization keys", async () => {
|
||||
await sut.migrate(helper);
|
||||
|
||||
expect(helper.set).toHaveBeenCalledWith("c493ed01-4e08-4e88-abc7-332f380ca760", {
|
||||
keys: {
|
||||
organizationKeys: {
|
||||
encrypted: {
|
||||
orgOneId: {
|
||||
type: "organization",
|
||||
key: "orgOneEncKey",
|
||||
},
|
||||
orgTwoId: {
|
||||
type: "organization",
|
||||
key: "orgTwoEncKey",
|
||||
},
|
||||
},
|
||||
},
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
});
|
||||
});
|
||||
|
||||
it("should update version", async () => {
|
||||
await sut.updateVersion(helper, "up");
|
||||
|
||||
expect(helper.set).toHaveBeenCalledTimes(1);
|
||||
expect(helper.set).toHaveBeenCalledWith("global", {
|
||||
stateVersion: 5,
|
||||
otherStuff: "otherStuff1",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("rollback", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(rollbackExampleJSON());
|
||||
sut = new AddKeyTypeToOrgKeysMigrator(4, 5);
|
||||
});
|
||||
|
||||
it("should remove type from orgainzation keys", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.set).toHaveBeenCalledWith("c493ed01-4e08-4e88-abc7-332f380ca760", {
|
||||
keys: {
|
||||
organizationKeys: {
|
||||
encrypted: {
|
||||
orgOneId: "orgOneEncKey",
|
||||
orgTwoId: "orgTwoEncKey",
|
||||
},
|
||||
},
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
});
|
||||
});
|
||||
|
||||
it("should update version down", async () => {
|
||||
await sut.updateVersion(helper, "down");
|
||||
|
||||
expect(helper.set).toHaveBeenCalledTimes(1);
|
||||
expect(helper.set).toHaveBeenCalledWith("global", {
|
||||
stateVersion: 4,
|
||||
otherStuff: "otherStuff1",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,73 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { MigrationHelper } from "../migration-helper";
|
||||
import { Direction, Migrator } from "../migrator";
|
||||
|
||||
type ExpectedAccountType = { keys?: { organizationKeys?: { encrypted: Record<string, string> } } };
|
||||
type NewAccountType = {
|
||||
keys?: {
|
||||
organizationKeys?: { encrypted: Record<string, { type: "organization"; key: string }> };
|
||||
};
|
||||
};
|
||||
|
||||
export class AddKeyTypeToOrgKeysMigrator extends Migrator<4, 5> {
|
||||
async migrate(helper: MigrationHelper): Promise<void> {
|
||||
const accounts = await helper.getAccounts();
|
||||
|
||||
async function updateOrgKey(userId: string, account: ExpectedAccountType) {
|
||||
const encryptedOrgKeys = account?.keys?.organizationKeys?.encrypted;
|
||||
if (encryptedOrgKeys == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newOrgKeys: Record<string, { type: "organization"; key: string }> = {};
|
||||
|
||||
Object.entries(encryptedOrgKeys).forEach(([orgId, encKey]) => {
|
||||
newOrgKeys[orgId] = {
|
||||
type: "organization",
|
||||
key: encKey,
|
||||
};
|
||||
});
|
||||
(account as any).keys.organizationKeys.encrypted = newOrgKeys;
|
||||
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
Promise.all(accounts.map(({ userId, account }) => updateOrgKey(userId, account)));
|
||||
}
|
||||
|
||||
async rollback(helper: MigrationHelper): Promise<void> {
|
||||
const accounts = await helper.getAccounts();
|
||||
|
||||
async function updateOrgKey(userId: string, account: NewAccountType) {
|
||||
const encryptedOrgKeys = account?.keys?.organizationKeys?.encrypted;
|
||||
if (encryptedOrgKeys == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newOrgKeys: Record<string, string> = {};
|
||||
|
||||
Object.entries(encryptedOrgKeys).forEach(([orgId, encKey]) => {
|
||||
newOrgKeys[orgId] = encKey.key;
|
||||
});
|
||||
(account as any).keys.organizationKeys.encrypted = newOrgKeys;
|
||||
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
Promise.all(accounts.map(async ({ userId, account }) => updateOrgKey(userId, account)));
|
||||
}
|
||||
|
||||
// Override is necessary because default implementation assumes `stateVersion` at the root, but for this version
|
||||
// it is nested inside a global object.
|
||||
override async updateVersion(helper: MigrationHelper, direction: Direction): Promise<void> {
|
||||
const endVersion = direction === "up" ? this.toVersion : this.fromVersion;
|
||||
helper.currentVersion = endVersion;
|
||||
const global: { stateVersion: number } = (await helper.get("global")) || ({} as any);
|
||||
await helper.set("global", { ...global, stateVersion: endVersion });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
import { MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
|
||||
import { mockMigrationHelper } from "../migration-helper.spec";
|
||||
|
||||
import { KeyConnectorMigrator } from "./50-move-key-connector-to-state-provider";
|
||||
|
||||
function exampleJSON() {
|
||||
return {
|
||||
global: {
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
authenticatedAccounts: ["FirstAccount", "SecondAccount", "ThirdAccount"],
|
||||
FirstAccount: {
|
||||
profile: {
|
||||
usesKeyConnector: true,
|
||||
convertAccountToKeyConnector: false,
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
},
|
||||
SecondAccount: {
|
||||
profile: {
|
||||
usesKeyConnector: true,
|
||||
convertAccountToKeyConnector: true,
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function rollbackJSON() {
|
||||
return {
|
||||
user_FirstAccount_keyConnector_usesKeyConnector: true,
|
||||
user_FirstAccount_keyConnector_convertAccountToKeyConnector: false,
|
||||
user_SecondAccount_keyConnector_usesKeyConnector: true,
|
||||
user_SecondAccount_keyConnector_convertAccountToKeyConnector: true,
|
||||
global: {
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
authenticatedAccounts: ["FirstAccount", "SecondAccount", "ThirdAccount"],
|
||||
FirstAccount: {
|
||||
profile: {
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
},
|
||||
SecondAccount: {
|
||||
profile: {
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const usesKeyConnectorKeyDefinition: KeyDefinitionLike = {
|
||||
key: "usesKeyConnector",
|
||||
stateDefinition: {
|
||||
name: "keyConnector",
|
||||
},
|
||||
};
|
||||
|
||||
const convertAccountToKeyConnectorKeyDefinition: KeyDefinitionLike = {
|
||||
key: "convertAccountToKeyConnector",
|
||||
stateDefinition: {
|
||||
name: "keyConnector",
|
||||
},
|
||||
};
|
||||
|
||||
describe("KeyConnectorMigrator", () => {
|
||||
let helper: MockProxy<MigrationHelper>;
|
||||
let sut: KeyConnectorMigrator;
|
||||
|
||||
describe("migrate", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(exampleJSON(), 50);
|
||||
sut = new KeyConnectorMigrator(49, 50);
|
||||
});
|
||||
|
||||
it("should remove usesKeyConnector and convertAccountToKeyConnector from Profile", async () => {
|
||||
await sut.migrate(helper);
|
||||
|
||||
// Set is called 2 times even though there are 3 accounts. Since the target properties don't exist in ThirdAccount, they are not set.
|
||||
expect(helper.set).toHaveBeenCalledTimes(2);
|
||||
expect(helper.set).toHaveBeenCalledWith("FirstAccount", {
|
||||
profile: {
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
});
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(
|
||||
"FirstAccount",
|
||||
usesKeyConnectorKeyDefinition,
|
||||
true,
|
||||
);
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(
|
||||
"FirstAccount",
|
||||
convertAccountToKeyConnectorKeyDefinition,
|
||||
false,
|
||||
);
|
||||
expect(helper.set).toHaveBeenCalledWith("SecondAccount", {
|
||||
profile: {
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
});
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(
|
||||
"SecondAccount",
|
||||
usesKeyConnectorKeyDefinition,
|
||||
true,
|
||||
);
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(
|
||||
"SecondAccount",
|
||||
convertAccountToKeyConnectorKeyDefinition,
|
||||
true,
|
||||
);
|
||||
expect(helper.setToUser).not.toHaveBeenCalledWith("ThirdAccount");
|
||||
});
|
||||
});
|
||||
|
||||
describe("rollback", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(rollbackJSON(), 50);
|
||||
sut = new KeyConnectorMigrator(49, 50);
|
||||
});
|
||||
|
||||
it("should null out new usesKeyConnector global value", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledTimes(4);
|
||||
expect(helper.set).toHaveBeenCalledTimes(2);
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(
|
||||
"FirstAccount",
|
||||
usesKeyConnectorKeyDefinition,
|
||||
null,
|
||||
);
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(
|
||||
"FirstAccount",
|
||||
convertAccountToKeyConnectorKeyDefinition,
|
||||
null,
|
||||
);
|
||||
expect(helper.set).toHaveBeenCalledWith("FirstAccount", {
|
||||
profile: {
|
||||
usesKeyConnector: true,
|
||||
convertAccountToKeyConnector: false,
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
});
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(
|
||||
"SecondAccount",
|
||||
usesKeyConnectorKeyDefinition,
|
||||
null,
|
||||
);
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(
|
||||
"SecondAccount",
|
||||
convertAccountToKeyConnectorKeyDefinition,
|
||||
null,
|
||||
);
|
||||
expect(helper.set).toHaveBeenCalledWith("SecondAccount", {
|
||||
profile: {
|
||||
usesKeyConnector: true,
|
||||
convertAccountToKeyConnector: true,
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
});
|
||||
expect(helper.setToUser).not.toHaveBeenCalledWith("ThirdAccount");
|
||||
expect(helper.set).not.toHaveBeenCalledWith("ThirdAccount");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,80 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
|
||||
import { Migrator } from "../migrator";
|
||||
|
||||
type ExpectedAccountType = {
|
||||
profile?: {
|
||||
usesKeyConnector?: boolean;
|
||||
convertAccountToKeyConnector?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
const usesKeyConnectorKeyDefinition: KeyDefinitionLike = {
|
||||
key: "usesKeyConnector",
|
||||
stateDefinition: {
|
||||
name: "keyConnector",
|
||||
},
|
||||
};
|
||||
|
||||
const convertAccountToKeyConnectorKeyDefinition: KeyDefinitionLike = {
|
||||
key: "convertAccountToKeyConnector",
|
||||
stateDefinition: {
|
||||
name: "keyConnector",
|
||||
},
|
||||
};
|
||||
|
||||
export class KeyConnectorMigrator extends Migrator<49, 50> {
|
||||
async migrate(helper: MigrationHelper): Promise<void> {
|
||||
const accounts = await helper.getAccounts<ExpectedAccountType>();
|
||||
async function migrateAccount(userId: string, account: ExpectedAccountType): Promise<void> {
|
||||
const usesKeyConnector = account?.profile?.usesKeyConnector;
|
||||
const convertAccountToKeyConnector = account?.profile?.convertAccountToKeyConnector;
|
||||
if (usesKeyConnector == null && convertAccountToKeyConnector == null) {
|
||||
return;
|
||||
}
|
||||
if (usesKeyConnector != null) {
|
||||
await helper.setToUser(userId, usesKeyConnectorKeyDefinition, usesKeyConnector);
|
||||
delete account.profile.usesKeyConnector;
|
||||
}
|
||||
if (convertAccountToKeyConnector != null) {
|
||||
await helper.setToUser(
|
||||
userId,
|
||||
convertAccountToKeyConnectorKeyDefinition,
|
||||
convertAccountToKeyConnector,
|
||||
);
|
||||
delete account.profile.convertAccountToKeyConnector;
|
||||
}
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
await Promise.all([...accounts.map(({ userId, account }) => migrateAccount(userId, account))]);
|
||||
}
|
||||
|
||||
async rollback(helper: MigrationHelper): Promise<void> {
|
||||
const accounts = await helper.getAccounts<ExpectedAccountType>();
|
||||
async function rollbackAccount(userId: string, account: ExpectedAccountType): Promise<void> {
|
||||
const usesKeyConnector: boolean = await helper.getFromUser(
|
||||
userId,
|
||||
usesKeyConnectorKeyDefinition,
|
||||
);
|
||||
const convertAccountToKeyConnector: boolean = await helper.getFromUser(
|
||||
userId,
|
||||
convertAccountToKeyConnectorKeyDefinition,
|
||||
);
|
||||
if (usesKeyConnector == null && convertAccountToKeyConnector == null) {
|
||||
return;
|
||||
}
|
||||
if (usesKeyConnector != null) {
|
||||
account.profile.usesKeyConnector = usesKeyConnector;
|
||||
await helper.setToUser(userId, usesKeyConnectorKeyDefinition, null);
|
||||
}
|
||||
if (convertAccountToKeyConnector != null) {
|
||||
account.profile.convertAccountToKeyConnector = convertAccountToKeyConnector;
|
||||
await helper.setToUser(userId, convertAccountToKeyConnectorKeyDefinition, null);
|
||||
}
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
|
||||
await Promise.all([...accounts.map(({ userId, account }) => rollbackAccount(userId, account))]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import { MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { MigrationHelper } from "../migration-helper";
|
||||
import { mockMigrationHelper, runMigrator } from "../migration-helper.spec";
|
||||
|
||||
import { RememberedEmailMigrator } from "./51-move-remembered-email-to-state-providers";
|
||||
|
||||
function rollbackJSON() {
|
||||
return {
|
||||
global: {
|
||||
extra: "data",
|
||||
},
|
||||
global_loginEmail_storedEmail: "user@example.com",
|
||||
};
|
||||
}
|
||||
|
||||
describe("RememberedEmailMigrator", () => {
|
||||
const migrator = new RememberedEmailMigrator(50, 51);
|
||||
|
||||
describe("migrate", () => {
|
||||
it("should migrate the rememberedEmail property from the legacy global object to a global StorageKey as 'global_loginEmail_storedEmail'", async () => {
|
||||
const output = await runMigrator(migrator, {
|
||||
global: {
|
||||
rememberedEmail: "user@example.com",
|
||||
extra: "data", // Represents a global property that should persist after migration
|
||||
},
|
||||
});
|
||||
|
||||
expect(output).toEqual({
|
||||
global: {
|
||||
extra: "data",
|
||||
},
|
||||
global_loginEmail_storedEmail: "user@example.com",
|
||||
});
|
||||
});
|
||||
|
||||
it("should remove the rememberedEmail property from the legacy global object", async () => {
|
||||
const output = await runMigrator(migrator, {
|
||||
global: {
|
||||
rememberedEmail: "user@example.com",
|
||||
},
|
||||
});
|
||||
|
||||
expect(output.global).not.toHaveProperty("rememberedEmail");
|
||||
});
|
||||
});
|
||||
|
||||
describe("rollback", () => {
|
||||
let helper: MockProxy<MigrationHelper>;
|
||||
let sut: RememberedEmailMigrator;
|
||||
|
||||
const keyDefinitionLike = {
|
||||
key: "storedEmail",
|
||||
stateDefinition: {
|
||||
name: "loginEmail",
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(rollbackJSON(), 51);
|
||||
sut = new RememberedEmailMigrator(50, 51);
|
||||
});
|
||||
|
||||
it("should null out the storedEmail global StorageKey", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.setToGlobal).toHaveBeenCalledTimes(1);
|
||||
expect(helper.setToGlobal).toHaveBeenCalledWith(keyDefinitionLike, null);
|
||||
});
|
||||
|
||||
it("should add the rememberedEmail property back to legacy global object", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.set).toHaveBeenCalledTimes(1);
|
||||
expect(helper.set).toHaveBeenCalledWith("global", {
|
||||
rememberedEmail: "user@example.com",
|
||||
extra: "data",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
import { KeyDefinitionLike, MigrationHelper, StateDefinitionLike } from "../migration-helper";
|
||||
import { Migrator } from "../migrator";
|
||||
|
||||
type ExpectedGlobalState = { rememberedEmail?: string };
|
||||
|
||||
const LOGIN_EMAIL_STATE: StateDefinitionLike = { name: "loginEmail" };
|
||||
|
||||
const STORED_EMAIL: KeyDefinitionLike = {
|
||||
key: "storedEmail",
|
||||
stateDefinition: LOGIN_EMAIL_STATE,
|
||||
};
|
||||
|
||||
export class RememberedEmailMigrator extends Migrator<50, 51> {
|
||||
async migrate(helper: MigrationHelper): Promise<void> {
|
||||
const legacyGlobal = await helper.get<ExpectedGlobalState>("global");
|
||||
|
||||
// Move global data
|
||||
if (legacyGlobal?.rememberedEmail != null) {
|
||||
await helper.setToGlobal(STORED_EMAIL, legacyGlobal.rememberedEmail);
|
||||
}
|
||||
|
||||
// Delete legacy global data
|
||||
delete legacyGlobal?.rememberedEmail;
|
||||
await helper.set("global", legacyGlobal);
|
||||
}
|
||||
|
||||
async rollback(helper: MigrationHelper): Promise<void> {
|
||||
let legacyGlobal = await helper.get<ExpectedGlobalState>("global");
|
||||
let updatedLegacyGlobal = false;
|
||||
const globalStoredEmail = await helper.getFromGlobal<string>(STORED_EMAIL);
|
||||
|
||||
if (globalStoredEmail) {
|
||||
if (!legacyGlobal) {
|
||||
legacyGlobal = {};
|
||||
}
|
||||
|
||||
updatedLegacyGlobal = true;
|
||||
legacyGlobal.rememberedEmail = globalStoredEmail;
|
||||
await helper.setToGlobal(STORED_EMAIL, null);
|
||||
}
|
||||
|
||||
if (updatedLegacyGlobal) {
|
||||
await helper.set("global", legacyGlobal);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { runMigrator } from "../migration-helper.spec";
|
||||
|
||||
import { DeleteInstalledVersion } from "./52-delete-installed-version";
|
||||
|
||||
describe("DeleteInstalledVersion", () => {
|
||||
const sut = new DeleteInstalledVersion(51, 52);
|
||||
|
||||
describe("migrate", () => {
|
||||
it("can delete data if there", async () => {
|
||||
const output = await runMigrator(sut, {
|
||||
authenticatedAccounts: ["user1"],
|
||||
global: {
|
||||
installedVersion: "2024.1.1",
|
||||
},
|
||||
});
|
||||
|
||||
expect(output).toEqual({
|
||||
authenticatedAccounts: ["user1"],
|
||||
global: {},
|
||||
});
|
||||
});
|
||||
|
||||
it("will run if installed version is not there", async () => {
|
||||
const output = await runMigrator(sut, {
|
||||
authenticatedAccounts: ["user1"],
|
||||
global: {},
|
||||
});
|
||||
|
||||
expect(output).toEqual({
|
||||
authenticatedAccounts: ["user1"],
|
||||
global: {},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
import { MigrationHelper } from "../migration-helper";
|
||||
import { IRREVERSIBLE, Migrator } from "../migrator";
|
||||
|
||||
type ExpectedGlobal = {
|
||||
installedVersion?: string;
|
||||
};
|
||||
|
||||
export class DeleteInstalledVersion extends Migrator<51, 52> {
|
||||
async migrate(helper: MigrationHelper): Promise<void> {
|
||||
const legacyGlobal = await helper.get<ExpectedGlobal>("global");
|
||||
if (legacyGlobal?.installedVersion != null) {
|
||||
delete legacyGlobal.installedVersion;
|
||||
await helper.set("global", legacyGlobal);
|
||||
}
|
||||
}
|
||||
rollback(helper: MigrationHelper): Promise<void> {
|
||||
throw IRREVERSIBLE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
import { MockProxy, any } from "jest-mock-extended";
|
||||
|
||||
import { MigrationHelper } from "../migration-helper";
|
||||
import { mockMigrationHelper } from "../migration-helper.spec";
|
||||
|
||||
import {
|
||||
DEVICE_KEY,
|
||||
DeviceTrustServiceStateProviderMigrator,
|
||||
SHOULD_TRUST_DEVICE,
|
||||
} from "./53-migrate-device-trust-svc-to-state-providers";
|
||||
|
||||
// Represents data in state service pre-migration
|
||||
function preMigrationJson() {
|
||||
return {
|
||||
global: {
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
authenticatedAccounts: ["user1", "user2", "user3"],
|
||||
user1: {
|
||||
keys: {
|
||||
deviceKey: {
|
||||
keyB64: "user1_deviceKey",
|
||||
},
|
||||
otherStuff: "overStuff2",
|
||||
},
|
||||
settings: {
|
||||
trustDeviceChoiceForDecryption: true,
|
||||
otherStuff: "overStuff3",
|
||||
},
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
user2: {
|
||||
keys: {
|
||||
// no device key
|
||||
otherStuff: "otherStuff5",
|
||||
},
|
||||
settings: {
|
||||
// no trust device choice
|
||||
otherStuff: "overStuff6",
|
||||
},
|
||||
otherStuff: "otherStuff7",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function rollbackJSON() {
|
||||
return {
|
||||
// use pattern user_{userId}_{stateDefinitionName}_{keyDefinitionKey} for each user
|
||||
// User1 migrated data
|
||||
user_user1_deviceTrust_deviceKey: {
|
||||
keyB64: "user1_deviceKey",
|
||||
},
|
||||
user_user1_deviceTrust_shouldTrustDevice: true,
|
||||
|
||||
// User2 does not have migrated data
|
||||
|
||||
global: {
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
authenticatedAccounts: ["user1", "user2", "user3"],
|
||||
user1: {
|
||||
keys: {
|
||||
otherStuff: "overStuff2",
|
||||
},
|
||||
settings: {
|
||||
otherStuff: "overStuff3",
|
||||
},
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
user2: {
|
||||
keys: {
|
||||
otherStuff: "otherStuff5",
|
||||
},
|
||||
settings: {
|
||||
otherStuff: "overStuff6",
|
||||
},
|
||||
otherStuff: "otherStuff6",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("DeviceTrustServiceStateProviderMigrator", () => {
|
||||
let helper: MockProxy<MigrationHelper>;
|
||||
let sut: DeviceTrustServiceStateProviderMigrator;
|
||||
|
||||
describe("migrate", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(preMigrationJson(), 52);
|
||||
sut = new DeviceTrustServiceStateProviderMigrator(52, 53);
|
||||
});
|
||||
|
||||
// it should remove deviceKey and trustDeviceChoiceForDecryption from all accounts
|
||||
it("should remove deviceKey and trustDeviceChoiceForDecryption from all accounts that have it", async () => {
|
||||
await sut.migrate(helper);
|
||||
expect(helper.set).toHaveBeenCalledWith("user1", {
|
||||
keys: {
|
||||
otherStuff: "overStuff2",
|
||||
},
|
||||
settings: {
|
||||
otherStuff: "overStuff3",
|
||||
},
|
||||
otherStuff: "otherStuff4",
|
||||
});
|
||||
|
||||
expect(helper.set).toHaveBeenCalledTimes(1);
|
||||
expect(helper.set).not.toHaveBeenCalledWith("user2", any());
|
||||
expect(helper.set).not.toHaveBeenCalledWith("user3", any());
|
||||
});
|
||||
|
||||
it("should migrate deviceKey and trustDeviceChoiceForDecryption to state providers for accounts that have the data", async () => {
|
||||
await sut.migrate(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("user1", DEVICE_KEY, {
|
||||
keyB64: "user1_deviceKey",
|
||||
});
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("user1", SHOULD_TRUST_DEVICE, true);
|
||||
|
||||
expect(helper.setToUser).not.toHaveBeenCalledWith("user2", DEVICE_KEY, any());
|
||||
expect(helper.setToUser).not.toHaveBeenCalledWith("user2", SHOULD_TRUST_DEVICE, any());
|
||||
|
||||
expect(helper.setToUser).not.toHaveBeenCalledWith("user3", DEVICE_KEY, any());
|
||||
expect(helper.setToUser).not.toHaveBeenCalledWith("user3", SHOULD_TRUST_DEVICE, any());
|
||||
});
|
||||
});
|
||||
|
||||
describe("rollback", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(rollbackJSON(), 53);
|
||||
sut = new DeviceTrustServiceStateProviderMigrator(52, 53);
|
||||
});
|
||||
|
||||
it("should null out newly migrated entries in state provider framework", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("user1", DEVICE_KEY, null);
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("user1", SHOULD_TRUST_DEVICE, null);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("user2", DEVICE_KEY, null);
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("user2", SHOULD_TRUST_DEVICE, null);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("user3", DEVICE_KEY, null);
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("user3", SHOULD_TRUST_DEVICE, null);
|
||||
});
|
||||
|
||||
it("should add back deviceKey and trustDeviceChoiceForDecryption to all accounts", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.set).toHaveBeenCalledWith("user1", {
|
||||
keys: {
|
||||
deviceKey: {
|
||||
keyB64: "user1_deviceKey",
|
||||
},
|
||||
otherStuff: "overStuff2",
|
||||
},
|
||||
settings: {
|
||||
trustDeviceChoiceForDecryption: true,
|
||||
otherStuff: "overStuff3",
|
||||
},
|
||||
otherStuff: "otherStuff4",
|
||||
});
|
||||
});
|
||||
|
||||
it("should not add data back if data wasn't migrated or acct doesn't exist", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
// no data to add back for user2 (acct exists but no migrated data) and user3 (no acct)
|
||||
expect(helper.set).not.toHaveBeenCalledWith("user2", any());
|
||||
expect(helper.set).not.toHaveBeenCalledWith("user3", any());
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,97 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
|
||||
import { Migrator } from "../migrator";
|
||||
|
||||
// Types to represent data as it is stored in JSON
|
||||
type DeviceKeyJsonType = {
|
||||
keyB64: string;
|
||||
};
|
||||
|
||||
type ExpectedAccountType = {
|
||||
keys?: {
|
||||
deviceKey?: DeviceKeyJsonType;
|
||||
};
|
||||
settings?: {
|
||||
trustDeviceChoiceForDecryption?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export const DEVICE_KEY: KeyDefinitionLike = {
|
||||
key: "deviceKey", // matches KeyDefinition.key in DeviceTrustService
|
||||
stateDefinition: {
|
||||
name: "deviceTrust", // matches StateDefinition.name in StateDefinitions
|
||||
},
|
||||
};
|
||||
|
||||
export const SHOULD_TRUST_DEVICE: KeyDefinitionLike = {
|
||||
key: "shouldTrustDevice",
|
||||
stateDefinition: {
|
||||
name: "deviceTrust",
|
||||
},
|
||||
};
|
||||
|
||||
export class DeviceTrustServiceStateProviderMigrator extends Migrator<52, 53> {
|
||||
async migrate(helper: MigrationHelper): Promise<void> {
|
||||
const accounts = await helper.getAccounts<ExpectedAccountType>();
|
||||
async function migrateAccount(userId: string, account: ExpectedAccountType): Promise<void> {
|
||||
let updatedAccount = false;
|
||||
|
||||
// Migrate deviceKey
|
||||
const existingDeviceKey = account?.keys?.deviceKey;
|
||||
|
||||
if (existingDeviceKey != null) {
|
||||
// Only migrate data that exists
|
||||
await helper.setToUser(userId, DEVICE_KEY, existingDeviceKey);
|
||||
delete account.keys.deviceKey;
|
||||
updatedAccount = true;
|
||||
}
|
||||
|
||||
// Migrate shouldTrustDevice
|
||||
const existingShouldTrustDevice = account?.settings?.trustDeviceChoiceForDecryption;
|
||||
|
||||
if (existingShouldTrustDevice != null) {
|
||||
await helper.setToUser(userId, SHOULD_TRUST_DEVICE, existingShouldTrustDevice);
|
||||
delete account.settings.trustDeviceChoiceForDecryption;
|
||||
updatedAccount = true;
|
||||
}
|
||||
|
||||
if (updatedAccount) {
|
||||
// Save the migrated account
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all([...accounts.map(({ userId, account }) => migrateAccount(userId, account))]);
|
||||
}
|
||||
|
||||
async rollback(helper: MigrationHelper): Promise<void> {
|
||||
const accounts = await helper.getAccounts<ExpectedAccountType>();
|
||||
async function rollbackAccount(userId: string, account: ExpectedAccountType): Promise<void> {
|
||||
// Rollback deviceKey
|
||||
const migratedDeviceKey: DeviceKeyJsonType = await helper.getFromUser(userId, DEVICE_KEY);
|
||||
|
||||
if (account?.keys && migratedDeviceKey != null) {
|
||||
account.keys.deviceKey = migratedDeviceKey;
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
|
||||
await helper.setToUser(userId, DEVICE_KEY, null);
|
||||
|
||||
// Rollback shouldTrustDevice
|
||||
const migratedShouldTrustDevice = await helper.getFromUser<boolean>(
|
||||
userId,
|
||||
SHOULD_TRUST_DEVICE,
|
||||
);
|
||||
|
||||
if (account?.settings && migratedShouldTrustDevice != null) {
|
||||
account.settings.trustDeviceChoiceForDecryption = migratedShouldTrustDevice;
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
|
||||
await helper.setToUser(userId, SHOULD_TRUST_DEVICE, null);
|
||||
}
|
||||
|
||||
await Promise.all([...accounts.map(({ userId, account }) => rollbackAccount(userId, account))]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
import { MockProxy, any } from "jest-mock-extended";
|
||||
|
||||
import { MigrationHelper } from "../migration-helper";
|
||||
import { mockMigrationHelper } from "../migration-helper.spec";
|
||||
|
||||
import { SendMigrator } from "./54-move-encrypted-sends";
|
||||
|
||||
function exampleJSON() {
|
||||
return {
|
||||
global: {
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
authenticatedAccounts: ["user-1", "user-2"],
|
||||
"user-1": {
|
||||
data: {
|
||||
sends: {
|
||||
encrypted: {
|
||||
"2ebadc23-e101-471b-bf2d-b125015337a0": {
|
||||
id: "2ebadc23-e101-471b-bf2d-b125015337a0",
|
||||
accessId: "I9y6LgHhG0e_LbElAVM3oA",
|
||||
deletionDate: "2024-03-07T20:35:03Z",
|
||||
disabled: false,
|
||||
hideEmail: false,
|
||||
key: "2.sR07sf4f18Rw6YQH9R/fPw==|DlLIYdTlFBktHVEJixqrOZmW/dTDGmZ+9iVftYkRh4s=|2mXH2fKgtItEMi8rcP1ykkVwRbxztw5MGboBwRl/kKM=",
|
||||
name: "2.A0wIvDbyzuh6AjgFtv2gqQ==|D0FymzfCdYJQcAk5MARfjg==|2g52y7e/33A7Bafaaoy3Yvae7vxbIxoABZdZeoZuyg4=",
|
||||
text: {
|
||||
hidden: false,
|
||||
text: "2.MkcPiJUnNfpcyETsoH3b8g==|/oHZ5g6pmcerXAJidP9sXg==|JDhd1Blsxm/ubp2AAggHZr6gZhyW4UYwZkF5rxlO6X0=",
|
||||
},
|
||||
type: 0,
|
||||
},
|
||||
"3b31c20d-b783-4912-9170-b12501555398": {
|
||||
id: "3b31c20d-b783-4912-9170-b12501555398",
|
||||
accessId: "DcIxO4O3EkmRcLElAVVTmA",
|
||||
deletionDate: "2024-03-07T20:42:43Z",
|
||||
disabled: false,
|
||||
hideEmail: false,
|
||||
key: "2.366XwLCi7RJnXuAvpsEVNw==|XfLoSsdOIYsHfcSMmv+7VJY97bKfS3fjpbq3ez+KCdk=|iTJxf4Pc3ub6hTFXGeU8NpUV3KxnuxzaHuNoFo/I6Vs=",
|
||||
name: "2.uJ2FoouFJr/SR9gv3jYY/Q==|ksVre4/YqwY/XOtPyIfIJw==|/LVT842LJgyAchl7NffogXkrmCFwOEHX9NFd0zgLqKo=",
|
||||
text: {
|
||||
hidden: false,
|
||||
text: "2.zBeOzMKtjnP5YI5lJWQTWA==|vxrGt4GKtydhrqaW35b/jw==|36Jtg172awn9YsgfzNs4pJ/OpA59NBnUkLNt6lg7Zw8=",
|
||||
},
|
||||
type: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
},
|
||||
"user-2": {
|
||||
data: {
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function rollbackJSON() {
|
||||
return {
|
||||
"user_user-1_send_sends": {
|
||||
"2ebadc23-e101-471b-bf2d-b125015337a0": {
|
||||
id: "2ebadc23-e101-471b-bf2d-b125015337a0",
|
||||
accessId: "I9y6LgHhG0e_LbElAVM3oA",
|
||||
deletionDate: "2024-03-07T20:35:03Z",
|
||||
disabled: false,
|
||||
hideEmail: false,
|
||||
key: "2.sR07sf4f18Rw6YQH9R/fPw==|DlLIYdTlFBktHVEJixqrOZmW/dTDGmZ+9iVftYkRh4s=|2mXH2fKgtItEMi8rcP1ykkVwRbxztw5MGboBwRl/kKM=",
|
||||
name: "2.A0wIvDbyzuh6AjgFtv2gqQ==|D0FymzfCdYJQcAk5MARfjg==|2g52y7e/33A7Bafaaoy3Yvae7vxbIxoABZdZeoZuyg4=",
|
||||
text: {
|
||||
hidden: false,
|
||||
text: "2.MkcPiJUnNfpcyETsoH3b8g==|/oHZ5g6pmcerXAJidP9sXg==|JDhd1Blsxm/ubp2AAggHZr6gZhyW4UYwZkF5rxlO6X0=",
|
||||
},
|
||||
type: 0,
|
||||
},
|
||||
"3b31c20d-b783-4912-9170-b12501555398": {
|
||||
id: "3b31c20d-b783-4912-9170-b12501555398",
|
||||
accessId: "DcIxO4O3EkmRcLElAVVTmA",
|
||||
deletionDate: "2024-03-07T20:42:43Z",
|
||||
disabled: false,
|
||||
hideEmail: false,
|
||||
key: "2.366XwLCi7RJnXuAvpsEVNw==|XfLoSsdOIYsHfcSMmv+7VJY97bKfS3fjpbq3ez+KCdk=|iTJxf4Pc3ub6hTFXGeU8NpUV3KxnuxzaHuNoFo/I6Vs=",
|
||||
name: "2.uJ2FoouFJr/SR9gv3jYY/Q==|ksVre4/YqwY/XOtPyIfIJw==|/LVT842LJgyAchl7NffogXkrmCFwOEHX9NFd0zgLqKo=",
|
||||
text: {
|
||||
hidden: false,
|
||||
text: "2.zBeOzMKtjnP5YI5lJWQTWA==|vxrGt4GKtydhrqaW35b/jw==|36Jtg172awn9YsgfzNs4pJ/OpA59NBnUkLNt6lg7Zw8=",
|
||||
},
|
||||
type: 0,
|
||||
},
|
||||
},
|
||||
"user_user-2_send_data": null as any,
|
||||
global: {
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
authenticatedAccounts: ["user-1", "user-2"],
|
||||
"user-1": {
|
||||
data: {
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
},
|
||||
"user-2": {
|
||||
data: {
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("SendMigrator", () => {
|
||||
let helper: MockProxy<MigrationHelper>;
|
||||
let sut: SendMigrator;
|
||||
const keyDefinitionLike = {
|
||||
stateDefinition: {
|
||||
name: "send",
|
||||
},
|
||||
key: "sends",
|
||||
};
|
||||
|
||||
describe("migrate", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(exampleJSON(), 53);
|
||||
sut = new SendMigrator(53, 54);
|
||||
});
|
||||
|
||||
it("should remove encrypted sends from all accounts", async () => {
|
||||
await sut.migrate(helper);
|
||||
expect(helper.set).toHaveBeenCalledWith("user-1", {
|
||||
data: {
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
});
|
||||
});
|
||||
|
||||
it("should set encrypted sends for each account", async () => {
|
||||
await sut.migrate(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("user-1", keyDefinitionLike, {
|
||||
"2ebadc23-e101-471b-bf2d-b125015337a0": {
|
||||
id: "2ebadc23-e101-471b-bf2d-b125015337a0",
|
||||
accessId: "I9y6LgHhG0e_LbElAVM3oA",
|
||||
deletionDate: "2024-03-07T20:35:03Z",
|
||||
disabled: false,
|
||||
hideEmail: false,
|
||||
key: "2.sR07sf4f18Rw6YQH9R/fPw==|DlLIYdTlFBktHVEJixqrOZmW/dTDGmZ+9iVftYkRh4s=|2mXH2fKgtItEMi8rcP1ykkVwRbxztw5MGboBwRl/kKM=",
|
||||
name: "2.A0wIvDbyzuh6AjgFtv2gqQ==|D0FymzfCdYJQcAk5MARfjg==|2g52y7e/33A7Bafaaoy3Yvae7vxbIxoABZdZeoZuyg4=",
|
||||
text: {
|
||||
hidden: false,
|
||||
text: "2.MkcPiJUnNfpcyETsoH3b8g==|/oHZ5g6pmcerXAJidP9sXg==|JDhd1Blsxm/ubp2AAggHZr6gZhyW4UYwZkF5rxlO6X0=",
|
||||
},
|
||||
type: 0,
|
||||
},
|
||||
"3b31c20d-b783-4912-9170-b12501555398": {
|
||||
id: "3b31c20d-b783-4912-9170-b12501555398",
|
||||
accessId: "DcIxO4O3EkmRcLElAVVTmA",
|
||||
deletionDate: "2024-03-07T20:42:43Z",
|
||||
disabled: false,
|
||||
hideEmail: false,
|
||||
key: "2.366XwLCi7RJnXuAvpsEVNw==|XfLoSsdOIYsHfcSMmv+7VJY97bKfS3fjpbq3ez+KCdk=|iTJxf4Pc3ub6hTFXGeU8NpUV3KxnuxzaHuNoFo/I6Vs=",
|
||||
name: "2.uJ2FoouFJr/SR9gv3jYY/Q==|ksVre4/YqwY/XOtPyIfIJw==|/LVT842LJgyAchl7NffogXkrmCFwOEHX9NFd0zgLqKo=",
|
||||
text: {
|
||||
hidden: false,
|
||||
text: "2.zBeOzMKtjnP5YI5lJWQTWA==|vxrGt4GKtydhrqaW35b/jw==|36Jtg172awn9YsgfzNs4pJ/OpA59NBnUkLNt6lg7Zw8=",
|
||||
},
|
||||
type: 0,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("rollback", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(rollbackJSON(), 54);
|
||||
sut = new SendMigrator(53, 54);
|
||||
});
|
||||
|
||||
it.each(["user-1", "user-2"])("should null out new values", async (userId) => {
|
||||
await sut.rollback(helper);
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(userId, keyDefinitionLike, null);
|
||||
});
|
||||
|
||||
it("should add encrypted send values back to accounts", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.set).toHaveBeenCalled();
|
||||
expect(helper.set).toHaveBeenCalledWith("user-1", {
|
||||
data: {
|
||||
sends: {
|
||||
encrypted: {
|
||||
"2ebadc23-e101-471b-bf2d-b125015337a0": {
|
||||
id: "2ebadc23-e101-471b-bf2d-b125015337a0",
|
||||
accessId: "I9y6LgHhG0e_LbElAVM3oA",
|
||||
deletionDate: "2024-03-07T20:35:03Z",
|
||||
disabled: false,
|
||||
hideEmail: false,
|
||||
key: "2.sR07sf4f18Rw6YQH9R/fPw==|DlLIYdTlFBktHVEJixqrOZmW/dTDGmZ+9iVftYkRh4s=|2mXH2fKgtItEMi8rcP1ykkVwRbxztw5MGboBwRl/kKM=",
|
||||
name: "2.A0wIvDbyzuh6AjgFtv2gqQ==|D0FymzfCdYJQcAk5MARfjg==|2g52y7e/33A7Bafaaoy3Yvae7vxbIxoABZdZeoZuyg4=",
|
||||
text: {
|
||||
hidden: false,
|
||||
text: "2.MkcPiJUnNfpcyETsoH3b8g==|/oHZ5g6pmcerXAJidP9sXg==|JDhd1Blsxm/ubp2AAggHZr6gZhyW4UYwZkF5rxlO6X0=",
|
||||
},
|
||||
type: 0,
|
||||
},
|
||||
"3b31c20d-b783-4912-9170-b12501555398": {
|
||||
id: "3b31c20d-b783-4912-9170-b12501555398",
|
||||
accessId: "DcIxO4O3EkmRcLElAVVTmA",
|
||||
deletionDate: "2024-03-07T20:42:43Z",
|
||||
disabled: false,
|
||||
hideEmail: false,
|
||||
key: "2.366XwLCi7RJnXuAvpsEVNw==|XfLoSsdOIYsHfcSMmv+7VJY97bKfS3fjpbq3ez+KCdk=|iTJxf4Pc3ub6hTFXGeU8NpUV3KxnuxzaHuNoFo/I6Vs=",
|
||||
name: "2.uJ2FoouFJr/SR9gv3jYY/Q==|ksVre4/YqwY/XOtPyIfIJw==|/LVT842LJgyAchl7NffogXkrmCFwOEHX9NFd0zgLqKo=",
|
||||
text: {
|
||||
hidden: false,
|
||||
text: "2.zBeOzMKtjnP5YI5lJWQTWA==|vxrGt4GKtydhrqaW35b/jw==|36Jtg172awn9YsgfzNs4pJ/OpA59NBnUkLNt6lg7Zw8=",
|
||||
},
|
||||
type: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
});
|
||||
});
|
||||
|
||||
it("should not try to restore values to missing accounts", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.set).not.toHaveBeenCalledWith("user-3", any());
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,71 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
|
||||
import { Migrator } from "../migrator";
|
||||
|
||||
// FIXME: update to use a const object instead of a typescript enum
|
||||
// eslint-disable-next-line @bitwarden/platform/no-enums
|
||||
export enum SendType {
|
||||
Text = 0,
|
||||
File = 1,
|
||||
}
|
||||
|
||||
type SendData = {
|
||||
id: string;
|
||||
accessId: string;
|
||||
};
|
||||
|
||||
type ExpectedSendState = {
|
||||
data?: {
|
||||
sends?: {
|
||||
encrypted?: Record<string, SendData>;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
const ENCRYPTED_SENDS: KeyDefinitionLike = {
|
||||
stateDefinition: {
|
||||
name: "send",
|
||||
},
|
||||
key: "sends",
|
||||
};
|
||||
|
||||
/**
|
||||
* Only encrypted sends are stored on disk. Only the encrypted items need to be
|
||||
* migrated from the previous sends state data.
|
||||
*/
|
||||
export class SendMigrator extends Migrator<53, 54> {
|
||||
async migrate(helper: MigrationHelper): Promise<void> {
|
||||
const accounts = await helper.getAccounts<ExpectedSendState>();
|
||||
|
||||
async function migrateAccount(userId: string, account: ExpectedSendState): Promise<void> {
|
||||
const value = account?.data?.sends?.encrypted;
|
||||
if (value != null) {
|
||||
await helper.setToUser(userId, ENCRYPTED_SENDS, value);
|
||||
delete account.data.sends;
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all([...accounts.map(({ userId, account }) => migrateAccount(userId, account))]);
|
||||
}
|
||||
|
||||
async rollback(helper: MigrationHelper): Promise<void> {
|
||||
const accounts = await helper.getAccounts<ExpectedSendState>();
|
||||
|
||||
async function rollbackAccount(userId: string, account: ExpectedSendState): Promise<void> {
|
||||
const value = await helper.getFromUser(userId, ENCRYPTED_SENDS);
|
||||
if (account) {
|
||||
account.data = Object.assign(account.data ?? {}, {
|
||||
sends: {
|
||||
encrypted: value,
|
||||
},
|
||||
});
|
||||
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
await helper.setToUser(userId, ENCRYPTED_SENDS, null);
|
||||
}
|
||||
await Promise.all([...accounts.map(({ userId, account }) => rollbackAccount(userId, account))]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
import { any, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { MigrationHelper } from "../migration-helper";
|
||||
import { mockMigrationHelper } from "../migration-helper.spec";
|
||||
|
||||
import {
|
||||
FORCE_SET_PASSWORD_REASON_DEFINITION,
|
||||
MASTER_KEY_ENCRYPTED_USER_KEY_DEFINITION,
|
||||
MASTER_KEY_HASH_DEFINITION,
|
||||
MoveMasterKeyStateToProviderMigrator,
|
||||
} from "./55-move-master-key-state-to-provider";
|
||||
|
||||
function preMigrationState() {
|
||||
return {
|
||||
global: {
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
authenticatedAccounts: ["FirstAccount", "SecondAccount", "ThirdAccount"],
|
||||
// prettier-ignore
|
||||
"FirstAccount": {
|
||||
profile: {
|
||||
forceSetPasswordReason: "FirstAccount_forceSetPasswordReason",
|
||||
keyHash: "FirstAccount_keyHash",
|
||||
otherStuff: "overStuff2",
|
||||
},
|
||||
keys: {
|
||||
masterKeyEncryptedUserKey: "FirstAccount_masterKeyEncryptedUserKey",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
},
|
||||
// prettier-ignore
|
||||
"SecondAccount": {
|
||||
profile: {
|
||||
forceSetPasswordReason: "SecondAccount_forceSetPasswordReason",
|
||||
keyHash: "SecondAccount_keyHash",
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
keys: {
|
||||
masterKeyEncryptedUserKey: "SecondAccount_masterKeyEncryptedUserKey",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
},
|
||||
// prettier-ignore
|
||||
"ThirdAccount": {
|
||||
profile: {
|
||||
otherStuff: "otherStuff6",
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function postMigrationState() {
|
||||
return {
|
||||
user_FirstAccount_masterPassword_forceSetPasswordReason: "FirstAccount_forceSetPasswordReason",
|
||||
user_FirstAccount_masterPassword_masterKeyHash: "FirstAccount_keyHash",
|
||||
user_FirstAccount_masterPassword_masterKeyEncryptedUserKey:
|
||||
"FirstAccount_masterKeyEncryptedUserKey",
|
||||
user_SecondAccount_masterPassword_forceSetPasswordReason:
|
||||
"SecondAccount_forceSetPasswordReason",
|
||||
user_SecondAccount_masterPassword_masterKeyHash: "SecondAccount_keyHash",
|
||||
user_SecondAccount_masterPassword_masterKeyEncryptedUserKey:
|
||||
"SecondAccount_masterKeyEncryptedUserKey",
|
||||
global: {
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
authenticatedAccounts: ["FirstAccount", "SecondAccount"],
|
||||
// prettier-ignore
|
||||
"FirstAccount": {
|
||||
profile: {
|
||||
otherStuff: "overStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
},
|
||||
// prettier-ignore
|
||||
"SecondAccount": {
|
||||
profile: {
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
},
|
||||
// prettier-ignore
|
||||
"ThirdAccount": {
|
||||
profile: {
|
||||
otherStuff: "otherStuff6",
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("MoveForceSetPasswordReasonToStateProviderMigrator", () => {
|
||||
let helper: MockProxy<MigrationHelper>;
|
||||
let sut: MoveMasterKeyStateToProviderMigrator;
|
||||
|
||||
describe("migrate", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(preMigrationState(), 54);
|
||||
sut = new MoveMasterKeyStateToProviderMigrator(54, 55);
|
||||
});
|
||||
|
||||
it("should remove properties from existing accounts", async () => {
|
||||
await sut.migrate(helper);
|
||||
expect(helper.set).toHaveBeenCalledWith("FirstAccount", {
|
||||
profile: {
|
||||
otherStuff: "overStuff2",
|
||||
},
|
||||
keys: {},
|
||||
otherStuff: "otherStuff3",
|
||||
});
|
||||
expect(helper.set).toHaveBeenCalledWith("SecondAccount", {
|
||||
profile: {
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
keys: {},
|
||||
otherStuff: "otherStuff5",
|
||||
});
|
||||
});
|
||||
|
||||
it("should set properties for each account", async () => {
|
||||
await sut.migrate(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(
|
||||
"FirstAccount",
|
||||
FORCE_SET_PASSWORD_REASON_DEFINITION,
|
||||
"FirstAccount_forceSetPasswordReason",
|
||||
);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(
|
||||
"FirstAccount",
|
||||
MASTER_KEY_HASH_DEFINITION,
|
||||
"FirstAccount_keyHash",
|
||||
);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(
|
||||
"FirstAccount",
|
||||
MASTER_KEY_ENCRYPTED_USER_KEY_DEFINITION,
|
||||
"FirstAccount_masterKeyEncryptedUserKey",
|
||||
);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(
|
||||
"SecondAccount",
|
||||
FORCE_SET_PASSWORD_REASON_DEFINITION,
|
||||
"SecondAccount_forceSetPasswordReason",
|
||||
);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(
|
||||
"SecondAccount",
|
||||
MASTER_KEY_HASH_DEFINITION,
|
||||
"SecondAccount_keyHash",
|
||||
);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(
|
||||
"SecondAccount",
|
||||
MASTER_KEY_ENCRYPTED_USER_KEY_DEFINITION,
|
||||
"SecondAccount_masterKeyEncryptedUserKey",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("rollback", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(postMigrationState(), 55);
|
||||
sut = new MoveMasterKeyStateToProviderMigrator(54, 55);
|
||||
});
|
||||
|
||||
it.each(["FirstAccount", "SecondAccount"])("should null out new values", async (userId) => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(
|
||||
userId,
|
||||
FORCE_SET_PASSWORD_REASON_DEFINITION,
|
||||
null,
|
||||
);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(userId, MASTER_KEY_HASH_DEFINITION, null);
|
||||
});
|
||||
|
||||
it("should add explicit value back to accounts", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.set).toHaveBeenCalledWith("FirstAccount", {
|
||||
profile: {
|
||||
forceSetPasswordReason: "FirstAccount_forceSetPasswordReason",
|
||||
keyHash: "FirstAccount_keyHash",
|
||||
otherStuff: "overStuff2",
|
||||
},
|
||||
keys: {
|
||||
masterKeyEncryptedUserKey: "FirstAccount_masterKeyEncryptedUserKey",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
});
|
||||
expect(helper.set).toHaveBeenCalledWith("SecondAccount", {
|
||||
profile: {
|
||||
forceSetPasswordReason: "SecondAccount_forceSetPasswordReason",
|
||||
keyHash: "SecondAccount_keyHash",
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
keys: {
|
||||
masterKeyEncryptedUserKey: "SecondAccount_masterKeyEncryptedUserKey",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
});
|
||||
});
|
||||
|
||||
it("should not try to restore values to missing accounts", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.set).not.toHaveBeenCalledWith("ThirdAccount", any());
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,113 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
|
||||
import { Migrator } from "../migrator";
|
||||
|
||||
type ExpectedAccountType = {
|
||||
keys?: {
|
||||
masterKeyEncryptedUserKey?: string;
|
||||
};
|
||||
profile?: {
|
||||
forceSetPasswordReason?: number;
|
||||
keyHash?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export const FORCE_SET_PASSWORD_REASON_DEFINITION: KeyDefinitionLike = {
|
||||
key: "forceSetPasswordReason",
|
||||
stateDefinition: {
|
||||
name: "masterPassword",
|
||||
},
|
||||
};
|
||||
|
||||
export const MASTER_KEY_HASH_DEFINITION: KeyDefinitionLike = {
|
||||
key: "masterKeyHash",
|
||||
stateDefinition: {
|
||||
name: "masterPassword",
|
||||
},
|
||||
};
|
||||
|
||||
export const MASTER_KEY_ENCRYPTED_USER_KEY_DEFINITION: KeyDefinitionLike = {
|
||||
key: "masterKeyEncryptedUserKey",
|
||||
stateDefinition: {
|
||||
name: "masterPassword",
|
||||
},
|
||||
};
|
||||
|
||||
export class MoveMasterKeyStateToProviderMigrator extends Migrator<54, 55> {
|
||||
async migrate(helper: MigrationHelper): Promise<void> {
|
||||
const accounts = await helper.getAccounts<ExpectedAccountType>();
|
||||
async function migrateAccount(userId: string, account: ExpectedAccountType): Promise<void> {
|
||||
const forceSetPasswordReason = account?.profile?.forceSetPasswordReason;
|
||||
if (forceSetPasswordReason != null) {
|
||||
await helper.setToUser(
|
||||
userId,
|
||||
FORCE_SET_PASSWORD_REASON_DEFINITION,
|
||||
forceSetPasswordReason,
|
||||
);
|
||||
|
||||
delete account.profile.forceSetPasswordReason;
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
|
||||
const masterKeyHash = account?.profile?.keyHash;
|
||||
if (masterKeyHash != null) {
|
||||
await helper.setToUser(userId, MASTER_KEY_HASH_DEFINITION, masterKeyHash);
|
||||
|
||||
delete account.profile.keyHash;
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
|
||||
const masterKeyEncryptedUserKey = account?.keys?.masterKeyEncryptedUserKey;
|
||||
if (masterKeyEncryptedUserKey != null) {
|
||||
await helper.setToUser(
|
||||
userId,
|
||||
MASTER_KEY_ENCRYPTED_USER_KEY_DEFINITION,
|
||||
masterKeyEncryptedUserKey,
|
||||
);
|
||||
|
||||
delete account.keys.masterKeyEncryptedUserKey;
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all([...accounts.map(({ userId, account }) => migrateAccount(userId, account))]);
|
||||
}
|
||||
async rollback(helper: MigrationHelper): Promise<void> {
|
||||
const accounts = await helper.getAccounts<ExpectedAccountType>();
|
||||
async function rollbackAccount(userId: string, account: ExpectedAccountType): Promise<void> {
|
||||
const forceSetPasswordReason = await helper.getFromUser(
|
||||
userId,
|
||||
FORCE_SET_PASSWORD_REASON_DEFINITION,
|
||||
);
|
||||
const masterKeyHash = await helper.getFromUser(userId, MASTER_KEY_HASH_DEFINITION);
|
||||
const masterKeyEncryptedUserKey = await helper.getFromUser(
|
||||
userId,
|
||||
MASTER_KEY_ENCRYPTED_USER_KEY_DEFINITION,
|
||||
);
|
||||
if (account != null) {
|
||||
if (forceSetPasswordReason != null) {
|
||||
account.profile = Object.assign(account.profile ?? {}, {
|
||||
forceSetPasswordReason,
|
||||
});
|
||||
}
|
||||
if (masterKeyHash != null) {
|
||||
account.profile = Object.assign(account.profile ?? {}, {
|
||||
keyHash: masterKeyHash,
|
||||
});
|
||||
}
|
||||
if (masterKeyEncryptedUserKey != null) {
|
||||
account.keys = Object.assign(account.keys ?? {}, {
|
||||
masterKeyEncryptedUserKey,
|
||||
});
|
||||
}
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
|
||||
await helper.setToUser(userId, FORCE_SET_PASSWORD_REASON_DEFINITION, null);
|
||||
await helper.setToUser(userId, MASTER_KEY_HASH_DEFINITION, null);
|
||||
}
|
||||
|
||||
await Promise.all([...accounts.map(({ userId, account }) => rollbackAccount(userId, account))]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
import { MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
|
||||
import { mockMigrationHelper } from "../migration-helper.spec";
|
||||
|
||||
import { AuthRequestMigrator } from "./56-move-auth-requests";
|
||||
|
||||
function exampleJSON() {
|
||||
return {
|
||||
global: {
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
authenticatedAccounts: ["FirstAccount", "SecondAccount"],
|
||||
FirstAccount: {
|
||||
settings: {
|
||||
otherStuff: "otherStuff2",
|
||||
approveLoginRequests: true,
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
adminAuthRequest: {
|
||||
id: "id1",
|
||||
privateKey: "privateKey1",
|
||||
},
|
||||
},
|
||||
SecondAccount: {
|
||||
settings: {
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function rollbackJSON() {
|
||||
return {
|
||||
user_FirstAccount_authRequestLocal_adminAuthRequest: {
|
||||
id: "id1",
|
||||
privateKey: "privateKey1",
|
||||
},
|
||||
user_FirstAccount_authRequestLocal_acceptAuthRequests: true,
|
||||
global: {
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
authenticatedAccounts: ["FirstAccount", "SecondAccount"],
|
||||
FirstAccount: {
|
||||
settings: {
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
},
|
||||
SecondAccount: {
|
||||
settings: {
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const ADMIN_AUTH_REQUEST_KEY: KeyDefinitionLike = {
|
||||
stateDefinition: {
|
||||
name: "authRequestLocal",
|
||||
},
|
||||
key: "adminAuthRequest",
|
||||
};
|
||||
|
||||
const ACCEPT_AUTH_REQUESTS_KEY: KeyDefinitionLike = {
|
||||
stateDefinition: {
|
||||
name: "authRequestLocal",
|
||||
},
|
||||
key: "acceptAuthRequests",
|
||||
};
|
||||
|
||||
describe("AuthRequestMigrator", () => {
|
||||
let helper: MockProxy<MigrationHelper>;
|
||||
let sut: AuthRequestMigrator;
|
||||
|
||||
describe("migrate", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(exampleJSON(), 55);
|
||||
sut = new AuthRequestMigrator(55, 56);
|
||||
});
|
||||
|
||||
it("removes the existing adminAuthRequest and approveLoginRequests", async () => {
|
||||
await sut.migrate(helper);
|
||||
|
||||
expect(helper.set).toHaveBeenCalledWith("FirstAccount", {
|
||||
settings: {
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
});
|
||||
expect(helper.set).not.toHaveBeenCalledWith("SecondAccount");
|
||||
});
|
||||
|
||||
it("sets the adminAuthRequest and approveLoginRequests under the new key definitions", async () => {
|
||||
await sut.migrate(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("FirstAccount", ADMIN_AUTH_REQUEST_KEY, {
|
||||
id: "id1",
|
||||
privateKey: "privateKey1",
|
||||
});
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("FirstAccount", ACCEPT_AUTH_REQUESTS_KEY, true);
|
||||
expect(helper.setToUser).not.toHaveBeenCalledWith("SecondAccount");
|
||||
});
|
||||
});
|
||||
|
||||
describe("rollback", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(rollbackJSON(), 56);
|
||||
sut = new AuthRequestMigrator(55, 56);
|
||||
});
|
||||
|
||||
it("nulls the new adminAuthRequest and acceptAuthRequests values", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("FirstAccount", ADMIN_AUTH_REQUEST_KEY, null);
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("FirstAccount", ACCEPT_AUTH_REQUESTS_KEY, null);
|
||||
});
|
||||
|
||||
it("sets back the adminAuthRequest and approveLoginRequests under old account object", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.set).toHaveBeenCalledWith("FirstAccount", {
|
||||
adminAuthRequest: {
|
||||
id: "id1",
|
||||
privateKey: "privateKey1",
|
||||
},
|
||||
settings: {
|
||||
otherStuff: "otherStuff2",
|
||||
approveLoginRequests: true,
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,106 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
|
||||
import { Migrator } from "../migrator";
|
||||
|
||||
type AdminAuthRequestStorable = {
|
||||
id: string;
|
||||
privateKey: string;
|
||||
};
|
||||
|
||||
type ExpectedAccountType = {
|
||||
adminAuthRequest?: AdminAuthRequestStorable;
|
||||
settings?: {
|
||||
approveLoginRequests?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
const ADMIN_AUTH_REQUEST_KEY: KeyDefinitionLike = {
|
||||
stateDefinition: {
|
||||
name: "authRequestLocal",
|
||||
},
|
||||
key: "adminAuthRequest",
|
||||
};
|
||||
|
||||
const ACCEPT_AUTH_REQUESTS_KEY: KeyDefinitionLike = {
|
||||
stateDefinition: {
|
||||
name: "authRequestLocal",
|
||||
},
|
||||
key: "acceptAuthRequests",
|
||||
};
|
||||
|
||||
export class AuthRequestMigrator extends Migrator<55, 56> {
|
||||
async migrate(helper: MigrationHelper): Promise<void> {
|
||||
const accounts = await helper.getAccounts<ExpectedAccountType>();
|
||||
|
||||
async function migrateAccount(userId: string, account: ExpectedAccountType): Promise<void> {
|
||||
let updatedAccount = false;
|
||||
|
||||
// Migrate admin auth request
|
||||
const existingAdminAuthRequest = account?.adminAuthRequest;
|
||||
|
||||
if (existingAdminAuthRequest != null) {
|
||||
await helper.setToUser(userId, ADMIN_AUTH_REQUEST_KEY, existingAdminAuthRequest);
|
||||
delete account.adminAuthRequest;
|
||||
updatedAccount = true;
|
||||
}
|
||||
|
||||
// Migrate approve login requests
|
||||
const existingApproveLoginRequests = account?.settings?.approveLoginRequests;
|
||||
|
||||
if (existingApproveLoginRequests != null) {
|
||||
await helper.setToUser(userId, ACCEPT_AUTH_REQUESTS_KEY, existingApproveLoginRequests);
|
||||
delete account.settings.approveLoginRequests;
|
||||
updatedAccount = true;
|
||||
}
|
||||
|
||||
if (updatedAccount) {
|
||||
// Save the migrated account
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all([...accounts.map(({ userId, account }) => migrateAccount(userId, account))]);
|
||||
}
|
||||
|
||||
async rollback(helper: MigrationHelper): Promise<void> {
|
||||
const accounts = await helper.getAccounts<ExpectedAccountType>();
|
||||
|
||||
async function rollbackAccount(userId: string, account: ExpectedAccountType): Promise<void> {
|
||||
let updatedAccount = false;
|
||||
// Rollback admin auth request
|
||||
const migratedAdminAuthRequest: AdminAuthRequestStorable = await helper.getFromUser(
|
||||
userId,
|
||||
ADMIN_AUTH_REQUEST_KEY,
|
||||
);
|
||||
|
||||
if (migratedAdminAuthRequest != null) {
|
||||
account.adminAuthRequest = migratedAdminAuthRequest;
|
||||
updatedAccount = true;
|
||||
}
|
||||
|
||||
await helper.setToUser(userId, ADMIN_AUTH_REQUEST_KEY, null);
|
||||
|
||||
// Rollback approve login requests
|
||||
const migratedAcceptAuthRequest: boolean = await helper.getFromUser(
|
||||
userId,
|
||||
ACCEPT_AUTH_REQUESTS_KEY,
|
||||
);
|
||||
|
||||
if (migratedAcceptAuthRequest != null) {
|
||||
account.settings = Object.assign(account.settings ?? {}, {
|
||||
approveLoginRequests: migratedAcceptAuthRequest,
|
||||
});
|
||||
updatedAccount = true;
|
||||
}
|
||||
|
||||
await helper.setToUser(userId, ACCEPT_AUTH_REQUESTS_KEY, null);
|
||||
|
||||
if (updatedAccount) {
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all([...accounts.map(({ userId, account }) => rollbackAccount(userId, account))]);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user