1
0
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:
Addison Beck
2025-08-04 11:01:28 -04:00
committed by GitHub
parent 5833ed459b
commit 361f7e3447
306 changed files with 4491 additions and 2702 deletions

View File

@@ -1 +1,2 @@
export { createMigrationBuilder, waitForMigrations, CURRENT_VERSION } from "./migrate";
// Compatibility re-export for @bitwarden/common/state-migrations
export * from "@bitwarden/state";

View File

@@ -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);
});
});

View File

@@ -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);
}
}

View File

@@ -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);
});
});
});

View File

@@ -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";

View File

@@ -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;
}

View File

@@ -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";

View File

@@ -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());
});
});
});

View File

@@ -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))]);
}
}

View File

@@ -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());
});
});
});

View File

@@ -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))]);
}
}

View File

@@ -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",
},
});
});
});

View File

@@ -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)));
}
}

View File

@@ -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());
});
});
});

View File

@@ -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))]);
}
}

View File

@@ -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());
},
);
});
});

View File

@@ -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)));
}
}

View File

@@ -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());
});
});
});

View File

@@ -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))]);
}
}

View File

@@ -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());
});
});
});

View File

@@ -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))]);
}
}

View File

@@ -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",
});
});
});
});

View File

@@ -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);
}
}
}

View File

@@ -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());
});
});
});

View File

@@ -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,
});
}
}
}
}

View File

@@ -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());
},
);
});
});

View File

@@ -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)));
}
}

View File

@@ -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());
});
});
});

View File

@@ -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))]);
}
}

View File

@@ -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());
});
});
});

View File

@@ -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))]);
}
}

View File

@@ -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());
});
});
});

View File

@@ -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))]);
}
}

View File

@@ -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());
},
);
});
});

View File

@@ -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)));
}
}

View File

@@ -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());
});
});
});

View File

@@ -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)));
}
}

View File

@@ -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());
});
});
});

View File

@@ -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,
});
}
}
}
}

View File

@@ -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());
});
});
});

View File

@@ -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))]);
}
}

View File

@@ -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());
});
});
});

View File

@@ -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,
});
}
}
}
}

View File

@@ -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());
},
);
});
});

View File

@@ -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)));
}
}

View File

@@ -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());
});
});
});

View File

@@ -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)));
}
}

View File

@@ -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",
});
});
});
});

View File

@@ -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,
);
}
}
}

View File

@@ -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());
});
});
});

View File

@@ -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)));
}
}

View File

@@ -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());
});
});
});

View File

@@ -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);
}
}
}

View File

@@ -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",
});
});
});
});

View File

@@ -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);
}
}

View File

@@ -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());
});
});
});

View File

@@ -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);
}
}
}

View File

@@ -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());
});
});
});

View File

@@ -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,
});
}
}
}
}

View File

@@ -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({});
});
});
});

View File

@@ -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);
}
}
}

View File

@@ -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());
});
});
});

View File

@@ -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 });
}
}
}
}

View File

@@ -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",
});
});
});
});

View File

@@ -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)));
}
}

View File

@@ -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());
});
});
});

View File

@@ -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))]);
}
}

View File

@@ -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());
},
);
});
});

View File

@@ -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))]);
}
}

View File

@@ -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",
});
});
});
});

View File

@@ -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 });
}
}

View File

@@ -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());
});
});
});

View File

@@ -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)));
}
}

View File

@@ -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());
});
});
});

View File

@@ -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))]);
}
}

View File

@@ -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",
});
});
});
});

View File

@@ -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);
}
}
}

View File

@@ -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",
});
});
});
});

View File

@@ -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)));
}
}

View File

@@ -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());
});
});
});

View File

@@ -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))]);
}
}

View File

@@ -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",
},
},
});
});
});

View File

@@ -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);
}
}

View File

@@ -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);
});
});
});

View File

@@ -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;
}
}

View File

@@ -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: "",
},
});
});
});

View File

@@ -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;
}
}

View File

@@ -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",
});
});
});

View File

@@ -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);
}
}

View File

@@ -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()));
});
});
});

View File

@@ -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))]);
}
}

View File

@@ -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",
});
});
});
});

View File

@@ -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 });
}
}

View File

@@ -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");
});
});
});

View File

@@ -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))]);
}
}

View File

@@ -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",
});
});
});
});

View File

@@ -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);
}
}
}

View File

@@ -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: {},
});
});
});
});

View File

@@ -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;
}
}

View File

@@ -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