mirror of
https://github.com/bitwarden/browser
synced 2025-12-17 08:43:33 +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:
@@ -1 +1,2 @@
|
||||
export { createMigrationBuilder, waitForMigrations, CURRENT_VERSION } from "./migrate";
|
||||
// Compatibility re-export for @bitwarden/common/state-migrations
|
||||
export * from "@bitwarden/state";
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
|
||||
// eslint-disable-next-line import/no-restricted-paths -- Needed to print log messages
|
||||
import { LogService } from "../platform/abstractions/log.service";
|
||||
// eslint-disable-next-line import/no-restricted-paths -- Needed to interface with storage locations
|
||||
import { AbstractStorageService } from "../platform/abstractions/storage.service";
|
||||
|
||||
import { currentVersion } from "./migrate";
|
||||
|
||||
describe("currentVersion", () => {
|
||||
let storage: MockProxy<AbstractStorageService>;
|
||||
let logService: MockProxy<LogService>;
|
||||
|
||||
beforeEach(() => {
|
||||
storage = mock();
|
||||
logService = mock();
|
||||
});
|
||||
|
||||
it("should return -1 if no version", async () => {
|
||||
storage.get.mockReturnValueOnce(null);
|
||||
expect(await currentVersion(storage, logService)).toEqual(-1);
|
||||
});
|
||||
|
||||
it("should return version", async () => {
|
||||
storage.get.calledWith("stateVersion").mockReturnValueOnce(1 as any);
|
||||
expect(await currentVersion(storage, logService)).toEqual(1);
|
||||
});
|
||||
|
||||
it("should return version from global", async () => {
|
||||
storage.get.calledWith("stateVersion").mockReturnValueOnce(null);
|
||||
storage.get.calledWith("global").mockReturnValueOnce({ stateVersion: 1 } as any);
|
||||
expect(await currentVersion(storage, logService)).toEqual(1);
|
||||
});
|
||||
|
||||
it("should prefer root version to global", async () => {
|
||||
storage.get.calledWith("stateVersion").mockReturnValue(1 as any);
|
||||
storage.get.calledWith("global").mockReturnValue({ stateVersion: 2 } as any);
|
||||
expect(await currentVersion(storage, logService)).toEqual(1);
|
||||
});
|
||||
});
|
||||
@@ -1,218 +0,0 @@
|
||||
// eslint-disable-next-line import/no-restricted-paths -- Needed to print log messages
|
||||
import { LogService } from "../platform/abstractions/log.service";
|
||||
// eslint-disable-next-line import/no-restricted-paths -- Needed to interface with storage locations
|
||||
import { AbstractStorageService } from "../platform/abstractions/storage.service";
|
||||
|
||||
import { MigrationBuilder } from "./migration-builder";
|
||||
import { EverHadUserKeyMigrator } from "./migrations/10-move-ever-had-user-key-to-state-providers";
|
||||
import { OrganizationKeyMigrator } from "./migrations/11-move-org-keys-to-state-providers";
|
||||
import { MoveEnvironmentStateToProviders } from "./migrations/12-move-environment-state-to-providers";
|
||||
import { ProviderKeyMigrator } from "./migrations/13-move-provider-keys-to-state-providers";
|
||||
import { MoveBiometricClientKeyHalfToStateProviders } from "./migrations/14-move-biometric-client-key-half-state-to-providers";
|
||||
import { FolderMigrator } from "./migrations/15-move-folder-state-to-state-provider";
|
||||
import { LastSyncMigrator } from "./migrations/16-move-last-sync-to-state-provider";
|
||||
import { EnablePasskeysMigrator } from "./migrations/17-move-enable-passkeys-to-state-providers";
|
||||
import { AutofillSettingsKeyMigrator } from "./migrations/18-move-autofill-settings-to-state-providers";
|
||||
import { RequirePasswordOnStartMigrator } from "./migrations/19-migrate-require-password-on-start";
|
||||
import { PrivateKeyMigrator } from "./migrations/20-move-private-key-to-state-providers";
|
||||
import { CollectionMigrator } from "./migrations/21-move-collections-state-to-state-provider";
|
||||
import { CollapsedGroupingsMigrator } from "./migrations/22-move-collapsed-groupings-to-state-provider";
|
||||
import { MoveBiometricPromptsToStateProviders } from "./migrations/23-move-biometric-prompts-to-state-providers";
|
||||
import { SmOnboardingTasksMigrator } from "./migrations/24-move-sm-onboarding-key-to-state-providers";
|
||||
import { ClearClipboardDelayMigrator } from "./migrations/25-move-clear-clipboard-to-autofill-settings-state-provider";
|
||||
import { RevertLastSyncMigrator } from "./migrations/26-revert-move-last-sync-to-state-provider";
|
||||
import { BadgeSettingsMigrator } from "./migrations/27-move-badge-settings-to-state-providers";
|
||||
import { MoveBiometricUnlockToStateProviders } from "./migrations/28-move-biometric-unlock-to-state-providers";
|
||||
import { UserNotificationSettingsKeyMigrator } from "./migrations/29-move-user-notification-settings-to-state-provider";
|
||||
import { PolicyMigrator } from "./migrations/30-move-policy-state-to-state-provider";
|
||||
import { EnableContextMenuMigrator } from "./migrations/31-move-enable-context-menu-to-autofill-settings-state-provider";
|
||||
import { PreferredLanguageMigrator } from "./migrations/32-move-preferred-language";
|
||||
import { AppIdMigrator } from "./migrations/33-move-app-id-to-state-providers";
|
||||
import { DomainSettingsMigrator } from "./migrations/34-move-domain-settings-to-state-providers";
|
||||
import { MoveThemeToStateProviderMigrator } from "./migrations/35-move-theme-to-state-providers";
|
||||
import { VaultSettingsKeyMigrator } from "./migrations/36-move-show-card-and-identity-to-state-provider";
|
||||
import { AvatarColorMigrator } from "./migrations/37-move-avatar-color-to-state-providers";
|
||||
import { TokenServiceStateProviderMigrator } from "./migrations/38-migrate-token-svc-to-state-provider";
|
||||
import { MoveBillingAccountProfileMigrator } from "./migrations/39-move-billing-account-profile-to-state-providers";
|
||||
import { RemoveEverBeenUnlockedMigrator } from "./migrations/4-remove-ever-been-unlocked";
|
||||
import { OrganizationMigrator } from "./migrations/40-move-organization-state-to-state-provider";
|
||||
import { EventCollectionMigrator } from "./migrations/41-move-event-collection-to-state-provider";
|
||||
import { EnableFaviconMigrator } from "./migrations/42-move-enable-favicon-to-domain-settings-state-provider";
|
||||
import { AutoConfirmFingerPrintsMigrator } from "./migrations/43-move-auto-confirm-finger-prints-to-state-provider";
|
||||
import { UserDecryptionOptionsMigrator } from "./migrations/44-move-user-decryption-options-to-state-provider";
|
||||
import { MergeEnvironmentState } from "./migrations/45-merge-environment-state";
|
||||
import { DeleteBiometricPromptCancelledData } from "./migrations/46-delete-orphaned-biometric-prompt-data";
|
||||
import { MoveDesktopSettingsMigrator } from "./migrations/47-move-desktop-settings";
|
||||
import { MoveDdgToStateProviderMigrator } from "./migrations/48-move-ddg-to-state-provider";
|
||||
import { AccountServerConfigMigrator } from "./migrations/49-move-account-server-configs";
|
||||
import { AddKeyTypeToOrgKeysMigrator } from "./migrations/5-add-key-type-to-org-keys";
|
||||
import { KeyConnectorMigrator } from "./migrations/50-move-key-connector-to-state-provider";
|
||||
import { RememberedEmailMigrator } from "./migrations/51-move-remembered-email-to-state-providers";
|
||||
import { DeleteInstalledVersion } from "./migrations/52-delete-installed-version";
|
||||
import { DeviceTrustServiceStateProviderMigrator } from "./migrations/53-migrate-device-trust-svc-to-state-providers";
|
||||
import { SendMigrator } from "./migrations/54-move-encrypted-sends";
|
||||
import { MoveMasterKeyStateToProviderMigrator } from "./migrations/55-move-master-key-state-to-provider";
|
||||
import { AuthRequestMigrator } from "./migrations/56-move-auth-requests";
|
||||
import { CipherServiceMigrator } from "./migrations/57-move-cipher-service-to-state-provider";
|
||||
import { RemoveRefreshTokenMigratedFlagMigrator } from "./migrations/58-remove-refresh-token-migrated-state-provider-flag";
|
||||
import { KdfConfigMigrator } from "./migrations/59-move-kdf-config-to-state-provider";
|
||||
import { RemoveLegacyEtmKeyMigrator } from "./migrations/6-remove-legacy-etm-key";
|
||||
import { KnownAccountsMigrator } from "./migrations/60-known-accounts";
|
||||
import { PinStateMigrator } from "./migrations/61-move-pin-state-to-providers";
|
||||
import { VaultTimeoutSettingsServiceStateProviderMigrator } from "./migrations/62-migrate-vault-timeout-settings-svc-to-state-provider";
|
||||
import { PasswordOptionsMigrator } from "./migrations/63-migrate-password-settings";
|
||||
import { GeneratorHistoryMigrator } from "./migrations/64-migrate-generator-history";
|
||||
import { ForwarderOptionsMigrator } from "./migrations/65-migrate-forwarder-settings";
|
||||
import { MoveFinalDesktopSettingsMigrator } from "./migrations/66-move-final-desktop-settings";
|
||||
import { RemoveUnassignedItemsBannerDismissed } from "./migrations/67-remove-unassigned-items-banner-dismissed";
|
||||
import { MoveLastSyncDate } from "./migrations/68-move-last-sync-date";
|
||||
import { MigrateIncorrectFolderKey } from "./migrations/69-migrate-incorrect-folder-key";
|
||||
import { MoveBiometricAutoPromptToAccount } from "./migrations/7-move-biometric-auto-prompt-to-account";
|
||||
import { RemoveAcBannersDismissed } from "./migrations/70-remove-ac-banner-dismissed";
|
||||
import { RemoveNewCustomizationOptionsCalloutDismissed } from "./migrations/71-remove-new-customization-options-callout-dismissed";
|
||||
import { RemoveAccountDeprovisioningBannerDismissed } from "./migrations/72-remove-account-deprovisioning-banner-dismissed";
|
||||
import { MoveStateVersionMigrator } from "./migrations/8-move-state-version";
|
||||
import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-settings-to-global";
|
||||
import { MinVersionMigrator } from "./migrations/min-version";
|
||||
|
||||
export const MIN_VERSION = 3;
|
||||
export const CURRENT_VERSION = 72;
|
||||
export type MinVersion = typeof MIN_VERSION;
|
||||
|
||||
export function createMigrationBuilder() {
|
||||
return MigrationBuilder.create()
|
||||
.with(MinVersionMigrator)
|
||||
.with(RemoveEverBeenUnlockedMigrator, 3, 4)
|
||||
.with(AddKeyTypeToOrgKeysMigrator, 4, 5)
|
||||
.with(RemoveLegacyEtmKeyMigrator, 5, 6)
|
||||
.with(MoveBiometricAutoPromptToAccount, 6, 7)
|
||||
.with(MoveStateVersionMigrator, 7, 8)
|
||||
.with(MoveBrowserSettingsToGlobal, 8, 9)
|
||||
.with(EverHadUserKeyMigrator, 9, 10)
|
||||
.with(OrganizationKeyMigrator, 10, 11)
|
||||
.with(MoveEnvironmentStateToProviders, 11, 12)
|
||||
.with(ProviderKeyMigrator, 12, 13)
|
||||
.with(MoveBiometricClientKeyHalfToStateProviders, 13, 14)
|
||||
.with(FolderMigrator, 14, 15)
|
||||
.with(LastSyncMigrator, 15, 16)
|
||||
.with(EnablePasskeysMigrator, 16, 17)
|
||||
.with(AutofillSettingsKeyMigrator, 17, 18)
|
||||
.with(RequirePasswordOnStartMigrator, 18, 19)
|
||||
.with(PrivateKeyMigrator, 19, 20)
|
||||
.with(CollectionMigrator, 20, 21)
|
||||
.with(CollapsedGroupingsMigrator, 21, 22)
|
||||
.with(MoveBiometricPromptsToStateProviders, 22, 23)
|
||||
.with(SmOnboardingTasksMigrator, 23, 24)
|
||||
.with(ClearClipboardDelayMigrator, 24, 25)
|
||||
.with(RevertLastSyncMigrator, 25, 26)
|
||||
.with(BadgeSettingsMigrator, 26, 27)
|
||||
.with(MoveBiometricUnlockToStateProviders, 27, 28)
|
||||
.with(UserNotificationSettingsKeyMigrator, 28, 29)
|
||||
.with(PolicyMigrator, 29, 30)
|
||||
.with(EnableContextMenuMigrator, 30, 31)
|
||||
.with(PreferredLanguageMigrator, 31, 32)
|
||||
.with(AppIdMigrator, 32, 33)
|
||||
.with(DomainSettingsMigrator, 33, 34)
|
||||
.with(MoveThemeToStateProviderMigrator, 34, 35)
|
||||
.with(VaultSettingsKeyMigrator, 35, 36)
|
||||
.with(AvatarColorMigrator, 36, 37)
|
||||
.with(TokenServiceStateProviderMigrator, 37, 38)
|
||||
.with(MoveBillingAccountProfileMigrator, 38, 39)
|
||||
.with(OrganizationMigrator, 39, 40)
|
||||
.with(EventCollectionMigrator, 40, 41)
|
||||
.with(EnableFaviconMigrator, 41, 42)
|
||||
.with(AutoConfirmFingerPrintsMigrator, 42, 43)
|
||||
.with(UserDecryptionOptionsMigrator, 43, 44)
|
||||
.with(MergeEnvironmentState, 44, 45)
|
||||
.with(DeleteBiometricPromptCancelledData, 45, 46)
|
||||
.with(MoveDesktopSettingsMigrator, 46, 47)
|
||||
.with(MoveDdgToStateProviderMigrator, 47, 48)
|
||||
.with(AccountServerConfigMigrator, 48, 49)
|
||||
.with(KeyConnectorMigrator, 49, 50)
|
||||
.with(RememberedEmailMigrator, 50, 51)
|
||||
.with(DeleteInstalledVersion, 51, 52)
|
||||
.with(DeviceTrustServiceStateProviderMigrator, 52, 53)
|
||||
.with(SendMigrator, 53, 54)
|
||||
.with(MoveMasterKeyStateToProviderMigrator, 54, 55)
|
||||
.with(AuthRequestMigrator, 55, 56)
|
||||
.with(CipherServiceMigrator, 56, 57)
|
||||
.with(RemoveRefreshTokenMigratedFlagMigrator, 57, 58)
|
||||
.with(KdfConfigMigrator, 58, 59)
|
||||
.with(KnownAccountsMigrator, 59, 60)
|
||||
.with(PinStateMigrator, 60, 61)
|
||||
.with(VaultTimeoutSettingsServiceStateProviderMigrator, 61, 62)
|
||||
.with(PasswordOptionsMigrator, 62, 63)
|
||||
.with(GeneratorHistoryMigrator, 63, 64)
|
||||
.with(ForwarderOptionsMigrator, 64, 65)
|
||||
.with(MoveFinalDesktopSettingsMigrator, 65, 66)
|
||||
.with(RemoveUnassignedItemsBannerDismissed, 66, 67)
|
||||
.with(MoveLastSyncDate, 67, 68)
|
||||
.with(MigrateIncorrectFolderKey, 68, 69)
|
||||
.with(RemoveAcBannersDismissed, 69, 70)
|
||||
.with(RemoveNewCustomizationOptionsCalloutDismissed, 70, 71)
|
||||
.with(RemoveAccountDeprovisioningBannerDismissed, 71, CURRENT_VERSION);
|
||||
}
|
||||
|
||||
export async function currentVersion(
|
||||
storageService: AbstractStorageService,
|
||||
logService: LogService,
|
||||
) {
|
||||
let state = await storageService.get<number>("stateVersion");
|
||||
if (state == null) {
|
||||
// Pre v8
|
||||
state = (await storageService.get<{ stateVersion: number }>("global"))?.stateVersion;
|
||||
}
|
||||
if (state == null) {
|
||||
logService.info("No state version found, assuming empty state.");
|
||||
return -1;
|
||||
}
|
||||
logService.info(`State version: ${state}`);
|
||||
return state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for migrations to have a chance to run and will resolve the promise once they are.
|
||||
*
|
||||
* @param storageService Disk storage where the `stateVersion` will or is already saved in.
|
||||
* @param logService Log service
|
||||
*/
|
||||
export async function waitForMigrations(
|
||||
storageService: AbstractStorageService,
|
||||
logService: LogService,
|
||||
) {
|
||||
const isReady = async () => {
|
||||
const version = await currentVersion(storageService, logService);
|
||||
// The saved version is what we consider the latest
|
||||
// migrations should be complete, the state version
|
||||
// shouldn't become larger than `CURRENT_VERSION` in
|
||||
// any normal usage of the application but it is common
|
||||
// enough in dev scenarios where we want to consider that
|
||||
// ready as well and return true in that scenario.
|
||||
return version >= CURRENT_VERSION;
|
||||
};
|
||||
|
||||
const wait = async (time: number) => {
|
||||
// Wait exponentially
|
||||
const nextTime = time * 2;
|
||||
if (nextTime > 8192) {
|
||||
// Don't wait longer than ~8 seconds in a single wait,
|
||||
// if the migrations still haven't happened. They aren't
|
||||
// likely to.
|
||||
return;
|
||||
}
|
||||
return new Promise<void>((resolve) => {
|
||||
setTimeout(async () => {
|
||||
if (!(await isReady())) {
|
||||
logService.info(`Waiting for migrations to finish, waiting for ${nextTime}ms`);
|
||||
await wait(nextTime);
|
||||
}
|
||||
resolve();
|
||||
}, time);
|
||||
});
|
||||
};
|
||||
|
||||
if (!(await isReady())) {
|
||||
// Wait for 2ms to start with
|
||||
await wait(2);
|
||||
}
|
||||
}
|
||||
@@ -1,143 +0,0 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
// eslint-disable-next-line import/no-restricted-paths
|
||||
import { ClientType } from "../enums";
|
||||
|
||||
import { MigrationBuilder } from "./migration-builder";
|
||||
import { MigrationHelper } from "./migration-helper";
|
||||
import { Migrator } from "./migrator";
|
||||
|
||||
describe("MigrationBuilder", () => {
|
||||
class TestMigrator extends Migrator<0, 1> {
|
||||
async migrate(helper: MigrationHelper): Promise<void> {
|
||||
await helper.set("test", "test");
|
||||
return;
|
||||
}
|
||||
|
||||
async rollback(helper: MigrationHelper): Promise<void> {
|
||||
await helper.set("test", "rollback");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
class TestMigratorWithInstanceMethod extends Migrator<0, 1> {
|
||||
private async instanceMethod(helper: MigrationHelper, value: string) {
|
||||
await helper.set("test", value);
|
||||
}
|
||||
|
||||
async migrate(helper: MigrationHelper): Promise<void> {
|
||||
await this.instanceMethod(helper, "migrate");
|
||||
}
|
||||
|
||||
async rollback(helper: MigrationHelper): Promise<void> {
|
||||
await this.instanceMethod(helper, "rollback");
|
||||
}
|
||||
}
|
||||
|
||||
let sut: MigrationBuilder<number>;
|
||||
|
||||
beforeEach(() => {
|
||||
sut = MigrationBuilder.create();
|
||||
});
|
||||
|
||||
class TestBadMigrator extends Migrator<1, 0> {
|
||||
async migrate(helper: MigrationHelper): Promise<void> {
|
||||
await helper.set("test", "test");
|
||||
}
|
||||
|
||||
async rollback(helper: MigrationHelper): Promise<void> {
|
||||
await helper.set("test", "rollback");
|
||||
}
|
||||
}
|
||||
|
||||
it("should throw if instantiated incorrectly", () => {
|
||||
expect(() => MigrationBuilder.create().with(TestMigrator, null, null)).toThrow();
|
||||
expect(() =>
|
||||
MigrationBuilder.create().with(TestMigrator, 0, 1).with(TestBadMigrator, 1, 0),
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
it("should be able to create a new MigrationBuilder", () => {
|
||||
expect(sut).toBeInstanceOf(MigrationBuilder);
|
||||
});
|
||||
|
||||
it("should be able to add a migrator", () => {
|
||||
const newBuilder = sut.with(TestMigrator, 0, 1);
|
||||
const migrations = newBuilder["migrations"];
|
||||
expect(migrations.length).toBe(1);
|
||||
expect(migrations[0]).toMatchObject({ migrator: expect.any(TestMigrator), direction: "up" });
|
||||
});
|
||||
|
||||
it("should be able to add a rollback", () => {
|
||||
const newBuilder = sut.with(TestMigrator, 0, 1).rollback(TestMigrator, 1, 0);
|
||||
const migrations = newBuilder["migrations"];
|
||||
expect(migrations.length).toBe(2);
|
||||
expect(migrations[1]).toMatchObject({ migrator: expect.any(TestMigrator), direction: "down" });
|
||||
});
|
||||
|
||||
const clientTypes = Object.values(ClientType);
|
||||
|
||||
describe.each(clientTypes)("for client %s", (clientType) => {
|
||||
describe("migrate", () => {
|
||||
let migrator: TestMigrator;
|
||||
let rollback_migrator: TestMigrator;
|
||||
|
||||
beforeEach(() => {
|
||||
sut = sut.with(TestMigrator, 0, 1).rollback(TestMigrator, 1, 0);
|
||||
migrator = (sut as any).migrations[0].migrator;
|
||||
rollback_migrator = (sut as any).migrations[1].migrator;
|
||||
});
|
||||
|
||||
it("should migrate", async () => {
|
||||
const helper = new MigrationHelper(0, mock(), mock(), "general", clientType);
|
||||
const spy = jest.spyOn(migrator, "migrate");
|
||||
await sut.migrate(helper);
|
||||
expect(spy).toBeCalledWith(helper);
|
||||
});
|
||||
|
||||
it("should rollback", async () => {
|
||||
const helper = new MigrationHelper(1, mock(), mock(), "general", clientType);
|
||||
const spy = jest.spyOn(rollback_migrator, "rollback");
|
||||
await sut.migrate(helper);
|
||||
expect(spy).toBeCalledWith(helper);
|
||||
});
|
||||
|
||||
it("should update version on migrate", async () => {
|
||||
const helper = new MigrationHelper(0, mock(), mock(), "general", clientType);
|
||||
const spy = jest.spyOn(migrator, "updateVersion");
|
||||
await sut.migrate(helper);
|
||||
expect(spy).toBeCalledWith(helper, "up");
|
||||
});
|
||||
|
||||
it("should update version on rollback", async () => {
|
||||
const helper = new MigrationHelper(1, mock(), mock(), "general", clientType);
|
||||
const spy = jest.spyOn(rollback_migrator, "updateVersion");
|
||||
await sut.migrate(helper);
|
||||
expect(spy).toBeCalledWith(helper, "down");
|
||||
});
|
||||
|
||||
it("should not run the migrator if the current version does not match the from version", async () => {
|
||||
const helper = new MigrationHelper(3, mock(), mock(), "general", clientType);
|
||||
const migrate = jest.spyOn(migrator, "migrate");
|
||||
const rollback = jest.spyOn(rollback_migrator, "rollback");
|
||||
await sut.migrate(helper);
|
||||
expect(migrate).not.toBeCalled();
|
||||
expect(rollback).not.toBeCalled();
|
||||
});
|
||||
|
||||
it("should not update version if the current version does not match the from version", async () => {
|
||||
const helper = new MigrationHelper(3, mock(), mock(), "general", clientType);
|
||||
const migrate = jest.spyOn(migrator, "updateVersion");
|
||||
const rollback = jest.spyOn(rollback_migrator, "updateVersion");
|
||||
await sut.migrate(helper);
|
||||
expect(migrate).not.toBeCalled();
|
||||
expect(rollback).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it("should be able to call instance methods", async () => {
|
||||
const helper = new MigrationHelper(0, mock(), mock(), "general", clientType);
|
||||
await sut.with(TestMigratorWithInstanceMethod, 0, 1).migrate(helper);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,106 +1 @@
|
||||
import { MigrationHelper } from "./migration-helper";
|
||||
import { Direction, Migrator, VersionFrom, VersionTo } from "./migrator";
|
||||
|
||||
export class MigrationBuilder<TCurrent extends number = 0> {
|
||||
/** Create a new MigrationBuilder with an empty buffer of migrations to perform.
|
||||
*
|
||||
* Add migrations to the buffer with {@link with} and {@link rollback}.
|
||||
* @returns A new MigrationBuilder.
|
||||
*/
|
||||
static create(): MigrationBuilder<0> {
|
||||
return new MigrationBuilder([]);
|
||||
}
|
||||
|
||||
private constructor(
|
||||
private migrations: readonly { migrator: Migrator<number, number>; direction: Direction }[],
|
||||
) {}
|
||||
|
||||
/** Add a migrator to the MigrationBuilder. Types are updated such that the chained MigrationBuilder must currently be
|
||||
* at state version equal to the from version of the migrator. Return as MigrationBuilder<TTo> where TTo is the to
|
||||
* version of the migrator, so that the next migrator can be chained.
|
||||
*
|
||||
* @param migrate A migrator class or a tuple of a migrator class, the from version, and the to version. A tuple is
|
||||
* required to instantiate version numbers unless a default constructor is defined.
|
||||
* @returns A new MigrationBuilder with the to version of the migrator as the current version.
|
||||
*/
|
||||
with<
|
||||
TMigrator extends Migrator<number, number>,
|
||||
TFrom extends VersionFrom<TMigrator> & TCurrent,
|
||||
TTo extends VersionTo<TMigrator>,
|
||||
>(
|
||||
...migrate: [new () => TMigrator] | [new (from: TFrom, to: TTo) => TMigrator, TFrom, TTo]
|
||||
): MigrationBuilder<TTo> {
|
||||
return this.addMigrator(migrate, "up");
|
||||
}
|
||||
|
||||
/** Add a migrator to rollback on the MigrationBuilder's list of migrations. As with {@link with}, types of
|
||||
* MigrationBuilder and Migrator must align. However, this time the migration is reversed so TCurrent of the
|
||||
* MigrationBuilder must be equal to the to version of the migrator. Return as MigrationBuilder<TFrom> where TFrom
|
||||
* is the from version of the migrator, so that the next migrator can be chained.
|
||||
*
|
||||
* @param migrate A migrator class or a tuple of a migrator class, the from version, and the to version. A tuple is
|
||||
* required to instantiate version numbers unless a default constructor is defined.
|
||||
* @returns A new MigrationBuilder with the from version of the migrator as the current version.
|
||||
*/
|
||||
rollback<
|
||||
TMigrator extends Migrator<number, number>,
|
||||
TFrom extends VersionFrom<TMigrator>,
|
||||
TTo extends VersionTo<TMigrator> & TCurrent,
|
||||
>(
|
||||
...migrate: [new () => TMigrator] | [new (from: TFrom, to: TTo) => TMigrator, TTo, TFrom]
|
||||
): MigrationBuilder<TFrom> {
|
||||
if (migrate.length === 3) {
|
||||
migrate = [migrate[0], migrate[2], migrate[1]];
|
||||
}
|
||||
return this.addMigrator(migrate, "down");
|
||||
}
|
||||
|
||||
/** Execute the migrations as defined in the MigrationBuilder's migrator buffer */
|
||||
migrate(helper: MigrationHelper): Promise<void> {
|
||||
return this.migrations.reduce(
|
||||
(promise, migrator) =>
|
||||
promise.then(async () => {
|
||||
await this.runMigrator(migrator.migrator, helper, migrator.direction);
|
||||
}),
|
||||
Promise.resolve(),
|
||||
);
|
||||
}
|
||||
|
||||
private addMigrator<
|
||||
TMigrator extends Migrator<number, number>,
|
||||
TFrom extends VersionFrom<TMigrator> & TCurrent,
|
||||
TTo extends VersionTo<TMigrator>,
|
||||
>(
|
||||
migrate: [new () => TMigrator] | [new (from: TFrom, to: TTo) => TMigrator, TFrom, TTo],
|
||||
direction: Direction = "up",
|
||||
) {
|
||||
const newMigration =
|
||||
migrate.length === 1
|
||||
? { migrator: new migrate[0](), direction }
|
||||
: { migrator: new migrate[0](migrate[1], migrate[2]), direction };
|
||||
|
||||
return new MigrationBuilder<TTo>([...this.migrations, newMigration]);
|
||||
}
|
||||
|
||||
private async runMigrator(
|
||||
migrator: Migrator<number, number>,
|
||||
helper: MigrationHelper,
|
||||
direction: Direction,
|
||||
): Promise<void> {
|
||||
const shouldMigrate = await migrator.shouldMigrate(helper, direction);
|
||||
helper.info(
|
||||
`Migrator ${migrator.constructor.name} (to version ${migrator.toVersion}) should migrate: ${shouldMigrate} - ${direction}`,
|
||||
);
|
||||
if (shouldMigrate) {
|
||||
const method = direction === "up" ? migrator.migrate : migrator.rollback;
|
||||
await method.bind(migrator)(helper);
|
||||
helper.info(
|
||||
`Migrator ${migrator.constructor.name} (to version ${migrator.toVersion}) migrated - ${direction}`,
|
||||
);
|
||||
await migrator.updateVersion(helper, direction);
|
||||
helper.info(
|
||||
`Migrator ${migrator.constructor.name} (to version ${migrator.toVersion}) updated version - ${direction}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
export { MigrationBuilder } from "@bitwarden/state";
|
||||
|
||||
@@ -1,385 +0,0 @@
|
||||
import { MockProxy, mock } from "jest-mock-extended";
|
||||
|
||||
import { FakeStorageService } from "../../spec/fake-storage.service";
|
||||
// eslint-disable-next-line import/no-restricted-paths -- Needed client type enum
|
||||
import { ClientType } from "../enums";
|
||||
// eslint-disable-next-line import/no-restricted-paths -- Needed to print log messages
|
||||
import { LogService } from "../platform/abstractions/log.service";
|
||||
// eslint-disable-next-line import/no-restricted-paths -- Needed to interface with storage locations
|
||||
import { AbstractStorageService } from "../platform/abstractions/storage.service";
|
||||
// eslint-disable-next-line import/no-restricted-paths -- Needed to generate unique strings for injection
|
||||
import { Utils } from "../platform/misc/utils";
|
||||
|
||||
import { MigrationHelper, MigrationHelperType } from "./migration-helper";
|
||||
import { Migrator } from "./migrator";
|
||||
|
||||
const exampleJSON = {
|
||||
authenticatedAccounts: [
|
||||
"c493ed01-4e08-4e88-abc7-332f380ca760",
|
||||
"23e61a5f-2ece-4f5e-b499-f0bc489482a9",
|
||||
],
|
||||
"c493ed01-4e08-4e88-abc7-332f380ca760": {
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
"23e61a5f-2ece-4f5e-b499-f0bc489482a9": {
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
global_serviceName_key: "global_serviceName_key",
|
||||
user_userId_serviceName_key: "user_userId_serviceName_key",
|
||||
global_account_accounts: {
|
||||
"c493ed01-4e08-4e88-abc7-332f380ca760": {
|
||||
otherStuff: "otherStuff3",
|
||||
},
|
||||
"23e61a5f-2ece-4f5e-b499-f0bc489482a9": {
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe("RemoveLegacyEtmKeyMigrator", () => {
|
||||
let storage: MockProxy<AbstractStorageService>;
|
||||
let logService: MockProxy<LogService>;
|
||||
let sut: MigrationHelper;
|
||||
|
||||
const clientTypes = Object.values(ClientType);
|
||||
|
||||
describe.each(clientTypes)("for client %s", (clientType) => {
|
||||
beforeEach(() => {
|
||||
logService = mock();
|
||||
storage = mock();
|
||||
storage.get.mockImplementation((key) => (exampleJSON as any)[key]);
|
||||
|
||||
sut = new MigrationHelper(0, storage, logService, "general", clientType);
|
||||
});
|
||||
|
||||
describe("get", () => {
|
||||
it("should delegate to storage.get", async () => {
|
||||
await sut.get("key");
|
||||
expect(storage.get).toHaveBeenCalledWith("key");
|
||||
});
|
||||
});
|
||||
|
||||
describe("set", () => {
|
||||
it("should delegate to storage.save", async () => {
|
||||
await sut.set("key", "value");
|
||||
expect(storage.save).toHaveBeenCalledWith("key", "value");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getAccounts", () => {
|
||||
it("should return all accounts", async () => {
|
||||
const accounts = await sut.getAccounts();
|
||||
expect(accounts).toEqual([
|
||||
{
|
||||
userId: "c493ed01-4e08-4e88-abc7-332f380ca760",
|
||||
account: { otherStuff: "otherStuff1" },
|
||||
},
|
||||
{
|
||||
userId: "23e61a5f-2ece-4f5e-b499-f0bc489482a9",
|
||||
account: { otherStuff: "otherStuff2" },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("should handle missing authenticatedAccounts", async () => {
|
||||
storage.get.mockImplementation((key) =>
|
||||
key === "authenticatedAccounts" ? undefined : (exampleJSON as any)[key],
|
||||
);
|
||||
const accounts = await sut.getAccounts();
|
||||
expect(accounts).toEqual([]);
|
||||
});
|
||||
|
||||
it("handles global scoped known accounts for version 60 and after", async () => {
|
||||
sut.currentVersion = 60;
|
||||
const accounts = await sut.getAccounts();
|
||||
expect(accounts).toEqual([
|
||||
// Note, still gets values stored in state service objects, just grabs user ids from global
|
||||
{
|
||||
userId: "c493ed01-4e08-4e88-abc7-332f380ca760",
|
||||
account: { otherStuff: "otherStuff1" },
|
||||
},
|
||||
{
|
||||
userId: "23e61a5f-2ece-4f5e-b499-f0bc489482a9",
|
||||
account: { otherStuff: "otherStuff2" },
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getKnownUserIds", () => {
|
||||
it("returns all user ids", async () => {
|
||||
const userIds = await sut.getKnownUserIds();
|
||||
expect(userIds).toEqual([
|
||||
"c493ed01-4e08-4e88-abc7-332f380ca760",
|
||||
"23e61a5f-2ece-4f5e-b499-f0bc489482a9",
|
||||
]);
|
||||
});
|
||||
|
||||
it("returns all user ids when version is 60 or greater", async () => {
|
||||
sut.currentVersion = 60;
|
||||
const userIds = await sut.getKnownUserIds();
|
||||
expect(userIds).toEqual([
|
||||
"c493ed01-4e08-4e88-abc7-332f380ca760",
|
||||
"23e61a5f-2ece-4f5e-b499-f0bc489482a9",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getFromGlobal", () => {
|
||||
it("should return the correct value", async () => {
|
||||
sut.currentVersion = 9;
|
||||
const value = await sut.getFromGlobal({
|
||||
stateDefinition: { name: "serviceName" },
|
||||
key: "key",
|
||||
});
|
||||
expect(value).toEqual("global_serviceName_key");
|
||||
});
|
||||
|
||||
it("should throw if the current version is less than 9", () => {
|
||||
expect(() =>
|
||||
sut.getFromGlobal({ stateDefinition: { name: "serviceName" }, key: "key" }),
|
||||
).toThrowError("No key builder should be used for versions prior to 9.");
|
||||
});
|
||||
});
|
||||
|
||||
describe("setToGlobal", () => {
|
||||
it("should set the correct value", async () => {
|
||||
sut.currentVersion = 9;
|
||||
await sut.setToGlobal(
|
||||
{ stateDefinition: { name: "serviceName" }, key: "key" },
|
||||
"new_value",
|
||||
);
|
||||
expect(storage.save).toHaveBeenCalledWith("global_serviceName_key", "new_value");
|
||||
});
|
||||
|
||||
it("should throw if the current version is less than 9", () => {
|
||||
expect(() =>
|
||||
sut.setToGlobal(
|
||||
{ stateDefinition: { name: "serviceName" }, key: "key" },
|
||||
"global_serviceName_key",
|
||||
),
|
||||
).toThrowError("No key builder should be used for versions prior to 9.");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getFromUser", () => {
|
||||
it("should return the correct value", async () => {
|
||||
sut.currentVersion = 9;
|
||||
const value = await sut.getFromUser("userId", {
|
||||
stateDefinition: { name: "serviceName" },
|
||||
key: "key",
|
||||
});
|
||||
expect(value).toEqual("user_userId_serviceName_key");
|
||||
});
|
||||
|
||||
it("should throw if the current version is less than 9", () => {
|
||||
expect(() =>
|
||||
sut.getFromUser("userId", { stateDefinition: { name: "serviceName" }, key: "key" }),
|
||||
).toThrowError("No key builder should be used for versions prior to 9.");
|
||||
});
|
||||
});
|
||||
|
||||
describe("setToUser", () => {
|
||||
it("should set the correct value", async () => {
|
||||
sut.currentVersion = 9;
|
||||
await sut.setToUser(
|
||||
"userId",
|
||||
{ stateDefinition: { name: "serviceName" }, key: "key" },
|
||||
"new_value",
|
||||
);
|
||||
expect(storage.save).toHaveBeenCalledWith("user_userId_serviceName_key", "new_value");
|
||||
});
|
||||
|
||||
it("should throw if the current version is less than 9", () => {
|
||||
expect(() =>
|
||||
sut.setToUser(
|
||||
"userId",
|
||||
{ stateDefinition: { name: "serviceName" }, key: "key" },
|
||||
"new_value",
|
||||
),
|
||||
).toThrowError("No key builder should be used for versions prior to 9.");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/** Helper to create well-mocked migration helpers in migration tests */
|
||||
export function mockMigrationHelper(
|
||||
storageJson: any,
|
||||
stateVersion = 0,
|
||||
type: MigrationHelperType = "general",
|
||||
clientType: ClientType = ClientType.Web,
|
||||
): MockProxy<MigrationHelper> {
|
||||
const logService: MockProxy<LogService> = mock();
|
||||
const storage: MockProxy<AbstractStorageService> = mock();
|
||||
storage.get.mockImplementation((key) => (storageJson as any)[key]);
|
||||
storage.save.mockImplementation(async (key, value) => {
|
||||
(storageJson as any)[key] = value;
|
||||
});
|
||||
const helper = new MigrationHelper(stateVersion, storage, logService, type, clientType);
|
||||
|
||||
const mockHelper = mock<MigrationHelper>();
|
||||
mockHelper.get.mockImplementation((key) => helper.get(key));
|
||||
mockHelper.set.mockImplementation((key, value) => helper.set(key, value));
|
||||
mockHelper.getFromGlobal.mockImplementation((keyDefinition) =>
|
||||
helper.getFromGlobal(keyDefinition),
|
||||
);
|
||||
mockHelper.setToGlobal.mockImplementation((keyDefinition, value) =>
|
||||
helper.setToGlobal(keyDefinition, value),
|
||||
);
|
||||
mockHelper.getFromUser.mockImplementation((userId, keyDefinition) =>
|
||||
helper.getFromUser(userId, keyDefinition),
|
||||
);
|
||||
mockHelper.setToUser.mockImplementation((userId, keyDefinition, value) =>
|
||||
helper.setToUser(userId, keyDefinition, value),
|
||||
);
|
||||
mockHelper.getAccounts.mockImplementation(() => helper.getAccounts());
|
||||
mockHelper.getKnownUserIds.mockImplementation(() => helper.getKnownUserIds());
|
||||
mockHelper.removeFromGlobal.mockImplementation((keyDefinition) =>
|
||||
helper.removeFromGlobal(keyDefinition),
|
||||
);
|
||||
mockHelper.remove.mockImplementation((key) => helper.remove(key));
|
||||
|
||||
mockHelper.type = helper.type;
|
||||
mockHelper.clientType = helper.clientType;
|
||||
|
||||
return mockHelper;
|
||||
}
|
||||
|
||||
export type InitialDataHint<TUsers extends readonly string[]> = {
|
||||
/**
|
||||
* A string array of the users id who are authenticated
|
||||
*/
|
||||
authenticatedAccounts?: TUsers;
|
||||
/**
|
||||
* Global data
|
||||
*/
|
||||
global?: unknown;
|
||||
/**
|
||||
* Other top level data
|
||||
*/
|
||||
[key: string]: unknown;
|
||||
} & {
|
||||
/**
|
||||
* A users data
|
||||
*/
|
||||
[userData in TUsers[number]]?: unknown;
|
||||
};
|
||||
|
||||
type InjectedData = {
|
||||
propertyName: string;
|
||||
propertyValue: string;
|
||||
originalPath: string[];
|
||||
};
|
||||
|
||||
// This is a slight lie, technically the type is `Record<string | symbol, unknown>
|
||||
// but for the purposes of things in the migrations this is enough.
|
||||
function isStringRecord(object: unknown | undefined): object is Record<string, unknown> {
|
||||
return object && typeof object === "object" && !Array.isArray(object);
|
||||
}
|
||||
|
||||
function injectData(data: Record<string, unknown>, path: string[]): InjectedData[] {
|
||||
if (!data) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const injectedData: InjectedData[] = [];
|
||||
|
||||
// Traverse keys for other objects
|
||||
const keys = Object.keys(data);
|
||||
for (const key of keys) {
|
||||
const currentProperty = data[key];
|
||||
if (isStringRecord(currentProperty)) {
|
||||
injectedData.push(...injectData(currentProperty, [...path, key]));
|
||||
}
|
||||
}
|
||||
|
||||
const propertyName = `__injectedProperty__${Utils.newGuid()}`;
|
||||
const propertyValue = `__injectedValue__${Utils.newGuid()}`;
|
||||
|
||||
injectedData.push({
|
||||
propertyName: propertyName,
|
||||
propertyValue: propertyValue,
|
||||
// Track the path it was originally injected in just for a better error
|
||||
originalPath: path,
|
||||
});
|
||||
data[propertyName] = propertyValue;
|
||||
return injectedData;
|
||||
}
|
||||
|
||||
function expectInjectedData(
|
||||
data: Record<string, unknown>,
|
||||
injectedData: InjectedData[],
|
||||
): [data: Record<string, unknown>, leftoverInjectedData: InjectedData[]] {
|
||||
const keys = Object.keys(data);
|
||||
for (const key of keys) {
|
||||
const propertyValue = data[key];
|
||||
// Injected data does not have to be found exactly where it was injected,
|
||||
// just that it exists at all.
|
||||
const injectedIndex = injectedData.findIndex(
|
||||
(d) =>
|
||||
d.propertyName === key &&
|
||||
typeof propertyValue === "string" &&
|
||||
propertyValue === d.propertyValue,
|
||||
);
|
||||
|
||||
if (injectedIndex !== -1) {
|
||||
// We found something we injected, remove it
|
||||
injectedData.splice(injectedIndex, 1);
|
||||
delete data[key];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isStringRecord(propertyValue)) {
|
||||
const [updatedData, leftoverInjectedData] = expectInjectedData(propertyValue, injectedData);
|
||||
data[key] = updatedData;
|
||||
injectedData = leftoverInjectedData;
|
||||
}
|
||||
}
|
||||
|
||||
return [data, injectedData];
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the {@link Migrator.migrate} method of your migrator. You may pass in your test data and get back the data after the migration.
|
||||
* This also injects extra properties at every level of your state and makes sure that it can be found.
|
||||
* @param migrator Your migrator to use to do the migration
|
||||
* @param initalData The data to start with
|
||||
* @returns State after your migration has ran.
|
||||
*/
|
||||
export async function runMigrator<
|
||||
TMigrator extends Migrator<number, number>,
|
||||
const TUsers extends readonly string[],
|
||||
>(
|
||||
migrator: TMigrator,
|
||||
initalData?: InitialDataHint<TUsers>,
|
||||
direction: "migrate" | "rollback" = "migrate",
|
||||
): Promise<Record<string, unknown>> {
|
||||
const clonedData = JSON.parse(JSON.stringify(initalData ?? {}));
|
||||
|
||||
// Inject fake data at every level of the object
|
||||
const allInjectedData = injectData(clonedData, []);
|
||||
|
||||
const fakeStorageService = new FakeStorageService(clonedData);
|
||||
const helper = new MigrationHelper(
|
||||
migrator.fromVersion,
|
||||
fakeStorageService,
|
||||
mock(),
|
||||
"general",
|
||||
ClientType.Web,
|
||||
);
|
||||
|
||||
// Run their migrations
|
||||
if (direction === "rollback") {
|
||||
await migrator.rollback(helper);
|
||||
} else {
|
||||
await migrator.migrate(helper);
|
||||
}
|
||||
const [data, leftoverInjectedData] = expectInjectedData(
|
||||
fakeStorageService.internalStore,
|
||||
allInjectedData,
|
||||
);
|
||||
expect(leftoverInjectedData).toHaveLength(0);
|
||||
|
||||
return data;
|
||||
}
|
||||
@@ -1,261 +1 @@
|
||||
// eslint-disable-next-line import/no-restricted-paths -- Needed to provide client type to migrations
|
||||
import { ClientType } from "../enums";
|
||||
// eslint-disable-next-line import/no-restricted-paths -- Needed to print log messages
|
||||
import { LogService } from "../platform/abstractions/log.service";
|
||||
// eslint-disable-next-line import/no-restricted-paths -- Needed to interface with storage locations
|
||||
import { AbstractStorageService } from "../platform/abstractions/storage.service";
|
||||
|
||||
export type StateDefinitionLike = { name: string };
|
||||
export type KeyDefinitionLike = {
|
||||
stateDefinition: StateDefinitionLike;
|
||||
key: string;
|
||||
};
|
||||
|
||||
export type MigrationHelperType = "general" | "web-disk-local";
|
||||
|
||||
export class MigrationHelper {
|
||||
constructor(
|
||||
public currentVersion: number,
|
||||
private storageService: AbstractStorageService,
|
||||
public logService: LogService,
|
||||
type: MigrationHelperType,
|
||||
public clientType: ClientType,
|
||||
) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
/**
|
||||
* On some clients, migrations are ran multiple times without direct action from the migration writer.
|
||||
*
|
||||
* All clients will run through migrations at least once, this run is referred to as `"general"`. If a migration is
|
||||
* ran more than that single time, they will get a unique name if that the write can make conditional logic based on which
|
||||
* migration run this is.
|
||||
*
|
||||
* @remarks The preferrable way of writing migrations is ALWAYS to be defensive and reflect on the data you are given back. This
|
||||
* should really only be used when reflecting on the data given isn't enough.
|
||||
*/
|
||||
type: MigrationHelperType;
|
||||
|
||||
/**
|
||||
* Gets a value from the storage service at the given key.
|
||||
*
|
||||
* This is a brute force method to just get a value from the storage service. If you can use {@link getFromGlobal} or {@link getFromUser}, you should.
|
||||
* @param key location
|
||||
* @returns the value at the location
|
||||
*/
|
||||
get<T>(key: string): Promise<T> {
|
||||
return this.storageService.get<T>(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a value in the storage service at the given key.
|
||||
*
|
||||
* This is a brute force method to just set a value in the storage service. If you can use {@link setToGlobal} or {@link setToUser}, you should.
|
||||
* @param key location
|
||||
* @param value the value to set
|
||||
* @returns
|
||||
*/
|
||||
set<T>(key: string, value: T): Promise<void> {
|
||||
this.logService.info(`Setting ${key}`);
|
||||
return this.storageService.save(key, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a value in the storage service at the given key.
|
||||
*
|
||||
* This is a brute force method to just remove a value in the storage service. If you can use {@link removeFromGlobal} or {@link removeFromUser}, you should.
|
||||
* @param key location
|
||||
* @returns void
|
||||
*/
|
||||
remove(key: string): Promise<void> {
|
||||
this.logService.info(`Removing ${key}`);
|
||||
return this.storageService.remove(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a globally scoped value from a location derived through the key definition
|
||||
*
|
||||
* This is for use with the state providers framework, DO NOT use for values stored with {@link StateService},
|
||||
* use {@link get} for those.
|
||||
* @param keyDefinition unique key definition
|
||||
* @returns value from store
|
||||
*/
|
||||
getFromGlobal<T>(keyDefinition: KeyDefinitionLike): Promise<T> {
|
||||
return this.get<T>(this.getGlobalKey(keyDefinition));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a globally scoped value to a location derived through the key definition
|
||||
*
|
||||
* This is for use with the state providers framework, DO NOT use for values stored with {@link StateService},
|
||||
* use {@link set} for those.
|
||||
* @param keyDefinition unique key definition
|
||||
* @param value value to store
|
||||
* @returns void
|
||||
*/
|
||||
setToGlobal<T>(keyDefinition: KeyDefinitionLike, value: T): Promise<void> {
|
||||
return this.set(this.getGlobalKey(keyDefinition), value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a globally scoped location derived through the key definition
|
||||
*
|
||||
* This is for use with the state providers framework, DO NOT use for values stored with {@link StateService},
|
||||
* use {@link remove} for those.
|
||||
* @param keyDefinition unique key definition
|
||||
* @returns void
|
||||
*/
|
||||
removeFromGlobal(keyDefinition: KeyDefinitionLike): Promise<void> {
|
||||
return this.remove(this.getGlobalKey(keyDefinition));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a user scoped value from a location derived through the user id and key definition
|
||||
*
|
||||
* This is for use with the state providers framework, DO NOT use for values stored with {@link StateService},
|
||||
* use {@link get} for those.
|
||||
* @param userId userId to use in the key
|
||||
* @param keyDefinition unique key definition
|
||||
* @returns value from store
|
||||
*/
|
||||
getFromUser<T>(userId: string, keyDefinition: KeyDefinitionLike): Promise<T> {
|
||||
return this.get<T>(this.getUserKey(userId, keyDefinition));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a user scoped value to a location derived through the user id and key definition
|
||||
*
|
||||
* This is for use with the state providers framework, DO NOT use for values stored with {@link StateService},
|
||||
* use {@link set} for those.
|
||||
* @param userId userId to use in the key
|
||||
* @param keyDefinition unique key definition
|
||||
* @param value value to store
|
||||
* @returns void
|
||||
*/
|
||||
setToUser<T>(userId: string, keyDefinition: KeyDefinitionLike, value: T): Promise<void> {
|
||||
return this.set(this.getUserKey(userId, keyDefinition), value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a user scoped location derived through the key definition
|
||||
*
|
||||
* This is for use with the state providers framework, DO NOT use for values stored with {@link StateService},
|
||||
* use {@link remove} for those.
|
||||
* @param keyDefinition unique key definition
|
||||
* @returns void
|
||||
*/
|
||||
removeFromUser(userId: string, keyDefinition: KeyDefinitionLike): Promise<void> {
|
||||
return this.remove(this.getUserKey(userId, keyDefinition));
|
||||
}
|
||||
|
||||
info(message: string): void {
|
||||
this.logService.info(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to read all Account objects stored by the State Service.
|
||||
*
|
||||
* This is useful from creating migrations off of this paradigm, but should not be used once a value is migrated to a state provider.
|
||||
*
|
||||
* @returns a list of all accounts that have been authenticated with state service, cast the expected type.
|
||||
*/
|
||||
async getAccounts<ExpectedAccountType>(): Promise<
|
||||
{ userId: string; account: ExpectedAccountType }[]
|
||||
> {
|
||||
const userIds = await this.getKnownUserIds();
|
||||
return Promise.all(
|
||||
userIds.map(async (userId) => ({
|
||||
userId,
|
||||
account: await this.get<ExpectedAccountType>(userId),
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to read known users ids.
|
||||
*/
|
||||
async getKnownUserIds(): Promise<string[]> {
|
||||
if (this.currentVersion < 60) {
|
||||
return knownAccountUserIdsBuilderPre60(this.storageService);
|
||||
} else {
|
||||
return knownAccountUserIdsBuilder(this.storageService);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a user storage key appropriate for the current version.
|
||||
*
|
||||
* @param userId userId to use in the key
|
||||
* @param keyDefinition state and key to use in the key
|
||||
* @returns
|
||||
*/
|
||||
private getUserKey(userId: string, keyDefinition: KeyDefinitionLike): string {
|
||||
if (this.currentVersion < 9) {
|
||||
return userKeyBuilderPre9();
|
||||
} else {
|
||||
return userKeyBuilder(userId, keyDefinition);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a global storage key appropriate for the current version.
|
||||
*
|
||||
* @param keyDefinition state and key to use in the key
|
||||
* @returns
|
||||
*/
|
||||
private getGlobalKey(keyDefinition: KeyDefinitionLike): string {
|
||||
if (this.currentVersion < 9) {
|
||||
return globalKeyBuilderPre9();
|
||||
} else {
|
||||
return globalKeyBuilder(keyDefinition);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When this is updated, rename this function to `userKeyBuilderXToY` where `X` is the version number it
|
||||
* became relevant, and `Y` prior to the version it was updated.
|
||||
*
|
||||
* Be sure to update the map in `MigrationHelper` to point to the appropriate function for the current version.
|
||||
* @param userId The userId of the user you want the key to be for.
|
||||
* @param keyDefinition the key definition of which data the key should point to.
|
||||
* @returns
|
||||
*/
|
||||
function userKeyBuilder(userId: string, keyDefinition: KeyDefinitionLike): string {
|
||||
return `user_${userId}_${keyDefinition.stateDefinition.name}_${keyDefinition.key}`;
|
||||
}
|
||||
|
||||
function userKeyBuilderPre9(): string {
|
||||
throw Error("No key builder should be used for versions prior to 9.");
|
||||
}
|
||||
|
||||
/**
|
||||
* When this is updated, rename this function to `globalKeyBuilderXToY` where `X` is the version number
|
||||
* it became relevant, and `Y` prior to the version it was updated.
|
||||
*
|
||||
* Be sure to update the map in `MigrationHelper` to point to the appropriate function for the current version.
|
||||
* @param keyDefinition the key definition of which data the key should point to.
|
||||
* @returns
|
||||
*/
|
||||
function globalKeyBuilder(keyDefinition: KeyDefinitionLike): string {
|
||||
return `global_${keyDefinition.stateDefinition.name}_${keyDefinition.key}`;
|
||||
}
|
||||
|
||||
function globalKeyBuilderPre9(): string {
|
||||
throw Error("No key builder should be used for versions prior to 9.");
|
||||
}
|
||||
|
||||
async function knownAccountUserIdsBuilderPre60(
|
||||
storageService: AbstractStorageService,
|
||||
): Promise<string[]> {
|
||||
return (await storageService.get<string[]>("authenticatedAccounts")) ?? [];
|
||||
}
|
||||
|
||||
async function knownAccountUserIdsBuilder(
|
||||
storageService: AbstractStorageService,
|
||||
): Promise<string[]> {
|
||||
const accounts = await storageService.get<Record<string, unknown>>(
|
||||
globalKeyBuilder({ stateDefinition: { name: "account" }, key: "accounts" }),
|
||||
);
|
||||
return Object.keys(accounts ?? {});
|
||||
}
|
||||
export { MigrationHelper } from "@bitwarden/state";
|
||||
|
||||
@@ -1,161 +0,0 @@
|
||||
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());
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,48 +0,0 @@
|
||||
// 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))]);
|
||||
}
|
||||
}
|
||||
@@ -1,163 +0,0 @@
|
||||
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());
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,61 +0,0 @@
|
||||
// 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))]);
|
||||
}
|
||||
}
|
||||
@@ -1,157 +0,0 @@
|
||||
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",
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,132 +0,0 @@
|
||||
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)));
|
||||
}
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
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());
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,55 +0,0 @@
|
||||
// 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))]);
|
||||
}
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
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());
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,65 +0,0 @@
|
||||
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)));
|
||||
}
|
||||
}
|
||||
@@ -1,163 +0,0 @@
|
||||
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());
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,59 +0,0 @@
|
||||
// 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))]);
|
||||
}
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
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());
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,49 +0,0 @@
|
||||
// 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))]);
|
||||
}
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
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",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,36 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,228 +0,0 @@
|
||||
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());
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,274 +0,0 @@
|
||||
// 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
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());
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,56 +0,0 @@
|
||||
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)));
|
||||
}
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
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());
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,55 +0,0 @@
|
||||
// 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))]);
|
||||
}
|
||||
}
|
||||
@@ -1,196 +0,0 @@
|
||||
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());
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,65 +0,0 @@
|
||||
// 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))]);
|
||||
}
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
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());
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,49 +0,0 @@
|
||||
// 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))]);
|
||||
}
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
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());
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,99 +0,0 @@
|
||||
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)));
|
||||
}
|
||||
}
|
||||
@@ -1,200 +0,0 @@
|
||||
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());
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,53 +0,0 @@
|
||||
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)));
|
||||
}
|
||||
}
|
||||
@@ -1,177 +0,0 @@
|
||||
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());
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,92 +0,0 @@
|
||||
// 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
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());
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,49 +0,0 @@
|
||||
// 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))]);
|
||||
}
|
||||
}
|
||||
@@ -1,166 +0,0 @@
|
||||
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());
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,73 +0,0 @@
|
||||
// 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
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());
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,58 +0,0 @@
|
||||
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)));
|
||||
}
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
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());
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,77 +0,0 @@
|
||||
// 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)));
|
||||
}
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
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",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,105 +0,0 @@
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,192 +0,0 @@
|
||||
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());
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,80 +0,0 @@
|
||||
// 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)));
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
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());
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,46 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
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",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,39 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,213 +0,0 @@
|
||||
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());
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,46 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,255 +0,0 @@
|
||||
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());
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,171 +0,0 @@
|
||||
// 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
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({});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,31 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,142 +0,0 @@
|
||||
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());
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,107 +0,0 @@
|
||||
// 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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,143 +0,0 @@
|
||||
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",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,59 +0,0 @@
|
||||
// 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)));
|
||||
}
|
||||
}
|
||||
@@ -1,300 +0,0 @@
|
||||
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());
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,245 +0,0 @@
|
||||
// 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))]);
|
||||
}
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
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());
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,67 +0,0 @@
|
||||
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))]);
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
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",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,34 +0,0 @@
|
||||
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 });
|
||||
}
|
||||
}
|
||||
@@ -1,183 +0,0 @@
|
||||
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());
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,158 +0,0 @@
|
||||
// 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)));
|
||||
}
|
||||
}
|
||||
@@ -1,168 +0,0 @@
|
||||
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());
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,51 +0,0 @@
|
||||
// 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))]);
|
||||
}
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
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",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,45 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
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",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,65 +0,0 @@
|
||||
// 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)));
|
||||
}
|
||||
}
|
||||
@@ -1,238 +0,0 @@
|
||||
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());
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,59 +0,0 @@
|
||||
// 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))]);
|
||||
}
|
||||
}
|
||||
@@ -1,164 +0,0 @@
|
||||
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",
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,83 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,23 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
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: "",
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,128 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
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",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,40 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
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()));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,51 +0,0 @@
|
||||
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))]);
|
||||
}
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
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",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,73 +0,0 @@
|
||||
// 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 });
|
||||
}
|
||||
}
|
||||
@@ -1,174 +0,0 @@
|
||||
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");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,80 +0,0 @@
|
||||
// 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))]);
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
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",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,46 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
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: {},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,19 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,171 +0,0 @@
|
||||
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());
|
||||
});
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user