1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-11 22:03:36 +00:00

[PM-5535] Migrate Environment Service to StateProvider (#7621)

* Migrate EnvironmentService

* Move Migration Test Helper

* Claim StateDefinition

* Add State Migration

* Update StateServices

* Update EnvironmentService Abstraction

* Update DI

* Update Browser Instantiation

* Fix BrowserEnvironmentService

* Update Desktop & CLI Instantiation

* Update Usage

* Create isStringRecord helper

* Fix Old Tests

* Use Existing AccountService

* Don't Rely on Parameter Mutation

* Fix Conflicts
This commit is contained in:
Justin Baur
2024-01-24 14:21:50 -05:00
committed by GitHub
parent 842fa5153b
commit c1d5351075
26 changed files with 648 additions and 232 deletions

View File

@@ -344,6 +344,11 @@ export default class MainBackground {
this.globalStateProvider, this.globalStateProvider,
this.derivedStateProvider, this.derivedStateProvider,
); );
this.environmentService = new BrowserEnvironmentService(
this.logService,
this.stateProvider,
this.accountService,
);
this.stateService = new BrowserStateService( this.stateService = new BrowserStateService(
this.storageService, this.storageService,
this.secureStorageService, this.secureStorageService,
@@ -351,6 +356,7 @@ export default class MainBackground {
this.logService, this.logService,
new StateFactory(GlobalState, Account), new StateFactory(GlobalState, Account),
this.accountService, this.accountService,
this.environmentService,
); );
this.platformUtilsService = new BrowserPlatformUtilsService( this.platformUtilsService = new BrowserPlatformUtilsService(
this.messagingService, this.messagingService,
@@ -386,7 +392,6 @@ export default class MainBackground {
); );
this.tokenService = new TokenService(this.stateService); this.tokenService = new TokenService(this.stateService);
this.appIdService = new AppIdService(this.storageService); this.appIdService = new AppIdService(this.storageService);
this.environmentService = new BrowserEnvironmentService(this.stateService, this.logService);
this.apiService = new ApiService( this.apiService = new ApiService(
this.tokenService, this.tokenService,
this.platformUtilsService, this.platformUtilsService,

View File

@@ -1,16 +1,18 @@
import {
accountServiceFactory,
AccountServiceInitOptions,
} from "../../../auth/background/service-factories/account-service.factory";
import { BrowserEnvironmentService } from "../../services/browser-environment.service"; import { BrowserEnvironmentService } from "../../services/browser-environment.service";
import { CachedServices, factory, FactoryOptions } from "./factory-options"; import { CachedServices, factory, FactoryOptions } from "./factory-options";
import { logServiceFactory, LogServiceInitOptions } from "./log-service.factory"; import { logServiceFactory, LogServiceInitOptions } from "./log-service.factory";
import { import { stateProviderFactory, StateProviderInitOptions } from "./state-provider.factory";
stateServiceFactory as stateServiceFactory,
StateServiceInitOptions,
} from "./state-service.factory";
type EnvironmentServiceFactoryOptions = FactoryOptions; type EnvironmentServiceFactoryOptions = FactoryOptions;
export type EnvironmentServiceInitOptions = EnvironmentServiceFactoryOptions & export type EnvironmentServiceInitOptions = EnvironmentServiceFactoryOptions &
StateServiceInitOptions & StateProviderInitOptions &
AccountServiceInitOptions &
LogServiceInitOptions; LogServiceInitOptions;
export function environmentServiceFactory( export function environmentServiceFactory(
@@ -23,8 +25,9 @@ export function environmentServiceFactory(
opts, opts,
async () => async () =>
new BrowserEnvironmentService( new BrowserEnvironmentService(
await stateServiceFactory(cache, opts),
await logServiceFactory(cache, opts), await logServiceFactory(cache, opts),
await stateProviderFactory(cache, opts),
await accountServiceFactory(cache, opts),
), ),
); );
} }

View File

@@ -8,6 +8,10 @@ import {
import { Account } from "../../../models/account"; import { Account } from "../../../models/account";
import { BrowserStateService } from "../../services/browser-state.service"; import { BrowserStateService } from "../../services/browser-state.service";
import {
environmentServiceFactory,
EnvironmentServiceInitOptions,
} from "./environment-service.factory";
import { CachedServices, factory, FactoryOptions } from "./factory-options"; import { CachedServices, factory, FactoryOptions } from "./factory-options";
import { logServiceFactory, LogServiceInitOptions } from "./log-service.factory"; import { logServiceFactory, LogServiceInitOptions } from "./log-service.factory";
import { import {
@@ -31,7 +35,8 @@ export type StateServiceInitOptions = StateServiceFactoryOptions &
SecureStorageServiceInitOptions & SecureStorageServiceInitOptions &
MemoryStorageServiceInitOptions & MemoryStorageServiceInitOptions &
LogServiceInitOptions & LogServiceInitOptions &
AccountServiceInitOptions; AccountServiceInitOptions &
EnvironmentServiceInitOptions;
export async function stateServiceFactory( export async function stateServiceFactory(
cache: { stateService?: BrowserStateService } & CachedServices, cache: { stateService?: BrowserStateService } & CachedServices,
@@ -42,13 +47,14 @@ export async function stateServiceFactory(
"stateService", "stateService",
opts, opts,
async () => async () =>
await new BrowserStateService( new BrowserStateService(
await diskStorageServiceFactory(cache, opts), await diskStorageServiceFactory(cache, opts),
await secureStorageServiceFactory(cache, opts), await secureStorageServiceFactory(cache, opts),
await memoryStorageServiceFactory(cache, opts), await memoryStorageServiceFactory(cache, opts),
await logServiceFactory(cache, opts), await logServiceFactory(cache, opts),
opts.stateServiceOptions.stateFactory, opts.stateServiceOptions.stateFactory,
await accountServiceFactory(cache, opts), await accountServiceFactory(cache, opts),
await environmentServiceFactory(cache, opts),
opts.stateServiceOptions.useAccountCache, opts.stateServiceOptions.useAccountCache,
), ),
); );

View File

@@ -1,16 +1,18 @@
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { EnvironmentService } from "@bitwarden/common/platform/services/environment.service"; import { EnvironmentService } from "@bitwarden/common/platform/services/environment.service";
import { StateProvider } from "@bitwarden/common/platform/state";
import { GroupPolicyEnvironment } from "../../admin-console/types/group-policy-environment"; import { GroupPolicyEnvironment } from "../../admin-console/types/group-policy-environment";
import { devFlagEnabled, devFlagValue } from "../flags"; import { devFlagEnabled, devFlagValue } from "../flags";
export class BrowserEnvironmentService extends EnvironmentService { export class BrowserEnvironmentService extends EnvironmentService {
constructor( constructor(
stateService: StateService,
private logService: LogService, private logService: LogService,
stateProvider: StateProvider,
accountService: AccountService,
) { ) {
super(stateService); super(stateProvider, accountService);
} }
async hasManagedEnvironment(): Promise<boolean> { async hasManagedEnvironment(): Promise<boolean> {

View File

@@ -1,6 +1,7 @@
import { mock, MockProxy } from "jest-mock-extended"; import { mock, MockProxy } from "jest-mock-extended";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { import {
AbstractMemoryStorageService, AbstractMemoryStorageService,
@@ -29,6 +30,7 @@ describe("Browser State Service", () => {
let stateFactory: MockProxy<StateFactory<GlobalState, Account>>; let stateFactory: MockProxy<StateFactory<GlobalState, Account>>;
let useAccountCache: boolean; let useAccountCache: boolean;
let accountService: MockProxy<AccountService>; let accountService: MockProxy<AccountService>;
let environmentService: MockProxy<EnvironmentService>;
let state: State<GlobalState, Account>; let state: State<GlobalState, Account>;
const userId = "userId"; const userId = "userId";
@@ -41,6 +43,7 @@ describe("Browser State Service", () => {
logService = mock(); logService = mock();
stateFactory = mock(); stateFactory = mock();
accountService = mock(); accountService = mock();
environmentService = mock();
// turn off account cache for tests // turn off account cache for tests
useAccountCache = false; useAccountCache = false;
@@ -66,6 +69,7 @@ describe("Browser State Service", () => {
logService, logService,
stateFactory, stateFactory,
accountService, accountService,
environmentService,
useAccountCache, useAccountCache,
); );
}); });

View File

@@ -1,6 +1,7 @@
import { BehaviorSubject } from "rxjs"; import { BehaviorSubject } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { import {
AbstractStorageService, AbstractStorageService,
@@ -44,6 +45,7 @@ export class BrowserStateService
logService: LogService, logService: LogService,
stateFactory: StateFactory<GlobalState, Account>, stateFactory: StateFactory<GlobalState, Account>,
accountService: AccountService, accountService: AccountService,
environmentService: EnvironmentService,
useAccountCache = true, useAccountCache = true,
) { ) {
super( super(
@@ -53,6 +55,7 @@ export class BrowserStateService
logService, logService,
stateFactory, stateFactory,
accountService, accountService,
environmentService,
useAccountCache, useAccountCache,
); );

View File

@@ -483,6 +483,7 @@ function getBgService<T>(service: keyof MainBackground) {
memoryStorageService: AbstractMemoryStorageService, memoryStorageService: AbstractMemoryStorageService,
logService: LogServiceAbstraction, logService: LogServiceAbstraction,
accountService: AccountServiceAbstraction, accountService: AccountServiceAbstraction,
environmentService: EnvironmentService,
) => { ) => {
return new BrowserStateService( return new BrowserStateService(
storageService, storageService,
@@ -491,6 +492,7 @@ function getBgService<T>(service: keyof MainBackground) {
logService, logService,
new StateFactory(GlobalState, Account), new StateFactory(GlobalState, Account),
accountService, accountService,
environmentService,
); );
}, },
deps: [ deps: [
@@ -499,6 +501,7 @@ function getBgService<T>(service: keyof MainBackground) {
MEMORY_STORAGE, MEMORY_STORAGE,
LogServiceAbstraction, LogServiceAbstraction,
AccountServiceAbstraction, AccountServiceAbstraction,
EnvironmentService,
], ],
}, },
{ {

View File

@@ -255,6 +255,8 @@ export class Main {
this.derivedStateProvider, this.derivedStateProvider,
); );
this.environmentService = new EnvironmentService(this.stateProvider, this.accountService);
this.stateService = new StateService( this.stateService = new StateService(
this.storageService, this.storageService,
this.secureStorageService, this.secureStorageService,
@@ -262,6 +264,7 @@ export class Main {
this.logService, this.logService,
new StateFactory(GlobalState, Account), new StateFactory(GlobalState, Account),
this.accountService, this.accountService,
this.environmentService,
); );
this.cryptoService = new CryptoService( this.cryptoService = new CryptoService(
@@ -276,7 +279,6 @@ export class Main {
this.appIdService = new AppIdService(this.storageService); this.appIdService = new AppIdService(this.storageService);
this.tokenService = new TokenService(this.stateService); this.tokenService = new TokenService(this.stateService);
this.environmentService = new EnvironmentService(this.stateService);
const customUserAgent = const customUserAgent =
"Bitwarden_CLI/" + "Bitwarden_CLI/" +

View File

@@ -22,6 +22,7 @@ import { BroadcasterService as BroadcasterServiceAbstraction } from "@bitwarden/
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto.service"; import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service";
import { import {
@@ -129,6 +130,7 @@ const RELOAD_CALLBACK = new InjectionToken<() => any>("RELOAD_CALLBACK");
LogService, LogService,
STATE_FACTORY, STATE_FACTORY,
AccountServiceAbstraction, AccountServiceAbstraction,
EnvironmentService,
STATE_SERVICE_USE_CACHE, STATE_SERVICE_USE_CACHE,
], ],
}, },

View File

@@ -5,10 +5,16 @@ import { app } from "electron";
import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service"; import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service";
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state"; import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
import { EnvironmentService } from "@bitwarden/common/platform/services/environment.service";
import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service"; import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service";
import { NoopMessagingService } from "@bitwarden/common/platform/services/noop-messaging.service"; import { NoopMessagingService } from "@bitwarden/common/platform/services/noop-messaging.service";
// eslint-disable-next-line import/no-restricted-paths -- We need the implementation to inject, but generally this should not be accessed /* eslint-disable import/no-restricted-paths -- We need the implementation to inject, but generally this should not be accessed */
import { DefaultActiveUserStateProvider } from "@bitwarden/common/platform/state/implementations/default-active-user-state.provider";
import { DefaultDerivedStateProvider } from "@bitwarden/common/platform/state/implementations/default-derived-state.provider";
import { DefaultGlobalStateProvider } from "@bitwarden/common/platform/state/implementations/default-global-state.provider"; import { DefaultGlobalStateProvider } from "@bitwarden/common/platform/state/implementations/default-global-state.provider";
import { DefaultSingleUserStateProvider } from "@bitwarden/common/platform/state/implementations/default-single-user-state.provider";
import { DefaultStateProvider } from "@bitwarden/common/platform/state/implementations/default-state.provider";
/*/ eslint-enable import/no-restricted-paths */
import { MenuMain } from "./main/menu/menu.main"; import { MenuMain } from "./main/menu/menu.main";
import { MessagingMain } from "./main/messaging.main"; import { MessagingMain } from "./main/messaging.main";
@@ -34,6 +40,7 @@ export class Main {
memoryStorageService: MemoryStorageService; memoryStorageService: MemoryStorageService;
messagingService: ElectronMainMessagingService; messagingService: ElectronMainMessagingService;
stateService: ElectronStateService; stateService: ElectronStateService;
environmentService: EnvironmentService;
desktopCredentialStorageListener: DesktopCredentialStorageListener; desktopCredentialStorageListener: DesktopCredentialStorageListener;
windowMain: WindowMain; windowMain: WindowMain;
@@ -93,6 +100,25 @@ export class Main {
this.storageService, this.storageService,
); );
const accountService = new AccountServiceImplementation(
new NoopMessagingService(),
this.logService,
globalStateProvider,
);
const stateProvider = new DefaultStateProvider(
new DefaultActiveUserStateProvider(
accountService,
this.memoryStorageService,
this.storageService,
),
new DefaultSingleUserStateProvider(this.memoryStorageService, this.storageService),
globalStateProvider,
new DefaultDerivedStateProvider(this.memoryStorageService),
);
this.environmentService = new EnvironmentService(stateProvider, accountService);
// TODO: this state service will have access to on disk storage, but not in memory storage. // TODO: this state service will have access to on disk storage, but not in memory storage.
// If we could get this to work using the stateService singleton that the rest of the app uses we could save // If we could get this to work using the stateService singleton that the rest of the app uses we could save
// ourselves from some hacks, like having to manually update the app menu vs. the menu subscribing to events. // ourselves from some hacks, like having to manually update the app menu vs. the menu subscribing to events.
@@ -102,11 +128,8 @@ export class Main {
this.memoryStorageService, this.memoryStorageService,
this.logService, this.logService,
new StateFactory(GlobalState, Account), new StateFactory(GlobalState, Account),
new AccountServiceImplementation( accountService, // will not broadcast logouts. This is a hack until we can remove messaging dependency
new NoopMessagingService(), this.environmentService,
this.logService,
globalStateProvider,
), // will not broadcast logouts. This is a hack until we can remove messaging dependency
false, // Do not use disk caching because this will get out of sync with the renderer service false, // Do not use disk caching because this will get out of sync with the renderer service
); );
@@ -128,7 +151,7 @@ export class Main {
this.menuMain = new MenuMain( this.menuMain = new MenuMain(
this.i18nService, this.i18nService,
this.messagingService, this.messagingService,
this.stateService, this.environmentService,
this.windowMain, this.windowMain,
this.updaterMain, this.updaterMain,
); );

View File

@@ -1,8 +1,8 @@
import { app, Menu } from "electron"; import { app, Menu } from "electron";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { UpdaterMain } from "../updater.main"; import { UpdaterMain } from "../updater.main";
import { WindowMain } from "../window.main"; import { WindowMain } from "../window.main";
@@ -16,7 +16,7 @@ export class MenuMain {
constructor( constructor(
private i18nService: I18nService, private i18nService: I18nService,
private messagingService: MessagingService, private messagingService: MessagingService,
private stateService: StateService, private environmentService: EnvironmentService,
private windowMain: WindowMain, private windowMain: WindowMain,
private updaterMain: UpdaterMain, private updaterMain: UpdaterMain,
) {} ) {}
@@ -45,16 +45,7 @@ export class MenuMain {
} }
private async getWebVaultUrl() { private async getWebVaultUrl() {
let webVaultUrl = cloudWebVaultUrl; return this.environmentService.getWebVaultUrl() ?? cloudWebVaultUrl;
const urlsObj = await this.stateService.getEnvironmentUrls();
if (urlsObj != null) {
if (urlsObj.base != null) {
webVaultUrl = urlsObj.base;
} else if (urlsObj.webVault != null) {
webVaultUrl = urlsObj.webVault;
}
}
return webVaultUrl;
} }
private initContextMenu() { private initContextMenu() {

View File

@@ -7,6 +7,7 @@ import {
STATE_SERVICE_USE_CACHE, STATE_SERVICE_USE_CACHE,
} from "@bitwarden/angular/services/injection-tokens"; } from "@bitwarden/angular/services/injection-tokens";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { import {
AbstractMemoryStorageService, AbstractMemoryStorageService,
@@ -32,6 +33,7 @@ export class StateService extends BaseStateService<GlobalState, Account> {
logService: LogService, logService: LogService,
@Inject(STATE_FACTORY) stateFactory: StateFactory<GlobalState, Account>, @Inject(STATE_FACTORY) stateFactory: StateFactory<GlobalState, Account>,
accountService: AccountService, accountService: AccountService,
environmentService: EnvironmentService,
@Inject(STATE_SERVICE_USE_CACHE) useAccountCache = true, @Inject(STATE_SERVICE_USE_CACHE) useAccountCache = true,
) { ) {
super( super(
@@ -41,6 +43,7 @@ export class StateService extends BaseStateService<GlobalState, Account> {
logService, logService,
stateFactory, stateFactory,
accountService, accountService,
environmentService,
useAccountCache, useAccountCache,
); );
} }

View File

@@ -382,7 +382,7 @@ import { ModalService } from "./modal.service";
{ {
provide: EnvironmentServiceAbstraction, provide: EnvironmentServiceAbstraction,
useClass: EnvironmentService, useClass: EnvironmentService,
deps: [StateServiceAbstraction], deps: [StateProvider, AccountServiceAbstraction],
}, },
{ {
provide: TotpServiceAbstraction, provide: TotpServiceAbstraction,
@@ -515,6 +515,7 @@ import { ModalService } from "./modal.service";
LogService, LogService,
STATE_FACTORY, STATE_FACTORY,
AccountServiceAbstraction, AccountServiceAbstraction,
EnvironmentServiceAbstraction,
STATE_SERVICE_USE_CACHE, STATE_SERVICE_USE_CACHE,
], ],
}, },

View File

@@ -1,5 +1,7 @@
import { Observable } from "rxjs"; import { Observable } from "rxjs";
import { UserId } from "../../types/guid";
export type Urls = { export type Urls = {
base?: string; base?: string;
webVault?: string; webVault?: string;
@@ -52,6 +54,12 @@ export abstract class EnvironmentService {
* @param {Region} region - The region of the cloud web vault app. * @param {Region} region - The region of the cloud web vault app.
*/ */
setCloudWebVaultUrl: (region: Region) => void; setCloudWebVaultUrl: (region: Region) => void;
/**
* Seed the environment for a given user based on the globally set defaults.
*/
seedUserEnvironment: (userId: UserId) => Promise<void>;
getSendUrl: () => string; getSendUrl: () => string;
getIconsUrl: () => string; getIconsUrl: () => string;
getApiUrl: () => string; getApiUrl: () => string;

View File

@@ -5,7 +5,6 @@ import { PolicyData } from "../../admin-console/models/data/policy.data";
import { ProviderData } from "../../admin-console/models/data/provider.data"; import { ProviderData } from "../../admin-console/models/data/provider.data";
import { Policy } from "../../admin-console/models/domain/policy"; import { Policy } from "../../admin-console/models/domain/policy";
import { AdminAuthRequestStorable } from "../../auth/models/domain/admin-auth-req-storable"; import { AdminAuthRequestStorable } from "../../auth/models/domain/admin-auth-req-storable";
import { EnvironmentUrls } from "../../auth/models/domain/environment-urls";
import { ForceSetPasswordReason } from "../../auth/models/domain/force-set-password-reason"; import { ForceSetPasswordReason } from "../../auth/models/domain/force-set-password-reason";
import { KdfConfig } from "../../auth/models/domain/kdf-config"; import { KdfConfig } from "../../auth/models/domain/kdf-config";
import { BiometricKey } from "../../auth/types/biometric-key"; import { BiometricKey } from "../../auth/types/biometric-key";
@@ -378,10 +377,6 @@ export abstract class StateService<T extends Account = Account> {
setEntityId: (value: string, options?: StorageOptions) => Promise<void>; setEntityId: (value: string, options?: StorageOptions) => Promise<void>;
getEntityType: (options?: StorageOptions) => Promise<any>; getEntityType: (options?: StorageOptions) => Promise<any>;
setEntityType: (value: string, options?: StorageOptions) => Promise<void>; setEntityType: (value: string, options?: StorageOptions) => Promise<void>;
getEnvironmentUrls: (options?: StorageOptions) => Promise<EnvironmentUrls>;
setEnvironmentUrls: (value: EnvironmentUrls, options?: StorageOptions) => Promise<void>;
getRegion: (options?: StorageOptions) => Promise<string>;
setRegion: (value: string, options?: StorageOptions) => Promise<void>;
getEquivalentDomains: (options?: StorageOptions) => Promise<string[][]>; getEquivalentDomains: (options?: StorageOptions) => Promise<string[][]>;
setEquivalentDomains: (value: string, options?: StorageOptions) => Promise<void>; setEquivalentDomains: (value: string, options?: StorageOptions) => Promise<void>;
getEventCollection: (options?: StorageOptions) => Promise<EventData[]>; getEventCollection: (options?: StorageOptions) => Promise<EventData[]>;

View File

@@ -5,7 +5,6 @@ import { PolicyData } from "../../../admin-console/models/data/policy.data";
import { ProviderData } from "../../../admin-console/models/data/provider.data"; import { ProviderData } from "../../../admin-console/models/data/provider.data";
import { Policy } from "../../../admin-console/models/domain/policy"; import { Policy } from "../../../admin-console/models/domain/policy";
import { AdminAuthRequestStorable } from "../../../auth/models/domain/admin-auth-req-storable"; import { AdminAuthRequestStorable } from "../../../auth/models/domain/admin-auth-req-storable";
import { EnvironmentUrls } from "../../../auth/models/domain/environment-urls";
import { ForceSetPasswordReason } from "../../../auth/models/domain/force-set-password-reason"; import { ForceSetPasswordReason } from "../../../auth/models/domain/force-set-password-reason";
import { KeyConnectorUserDecryptionOption } from "../../../auth/models/domain/user-decryption-options/key-connector-user-decryption-option"; import { KeyConnectorUserDecryptionOption } from "../../../auth/models/domain/user-decryption-options/key-connector-user-decryption-option";
import { TrustedDeviceUserDecryptionOption } from "../../../auth/models/domain/user-decryption-options/trusted-device-user-decryption-option"; import { TrustedDeviceUserDecryptionOption } from "../../../auth/models/domain/user-decryption-options/trusted-device-user-decryption-option";
@@ -239,7 +238,6 @@ export class AccountSettings {
enableAutoFillOnPageLoad?: boolean; enableAutoFillOnPageLoad?: boolean;
enableBiometric?: boolean; enableBiometric?: boolean;
enableFullWidth?: boolean; enableFullWidth?: boolean;
environmentUrls: EnvironmentUrls = new EnvironmentUrls();
equivalentDomains?: any; equivalentDomains?: any;
minimizeOnCopyToClipboard?: boolean; minimizeOnCopyToClipboard?: boolean;
passwordGenerationOptions?: PasswordGeneratorOptions; passwordGenerationOptions?: PasswordGeneratorOptions;
@@ -255,7 +253,6 @@ export class AccountSettings {
approveLoginRequests?: boolean; approveLoginRequests?: boolean;
avatarColor?: string; avatarColor?: string;
activateAutoFillOnPageLoadFromPolicy?: boolean; activateAutoFillOnPageLoadFromPolicy?: boolean;
region?: string;
smOnboardingTasks?: Record<string, Record<string, boolean>>; smOnboardingTasks?: Record<string, Record<string, boolean>>;
trustDeviceChoiceForDecryption?: boolean; trustDeviceChoiceForDecryption?: boolean;
biometricPromptCancelled?: boolean; biometricPromptCancelled?: boolean;
@@ -269,7 +266,6 @@ export class AccountSettings {
} }
return Object.assign(new AccountSettings(), obj, { return Object.assign(new AccountSettings(), obj, {
environmentUrls: EnvironmentUrls.fromJSON(obj?.environmentUrls),
pinProtected: EncryptionPair.fromJSON<string, EncString>( pinProtected: EncryptionPair.fromJSON<string, EncString>(
obj?.pinProtected, obj?.pinProtected,
EncString.fromJSON, EncString.fromJSON,

View File

@@ -1,4 +1,3 @@
import { EnvironmentUrls } from "../../../auth/models/domain/environment-urls";
import { WindowState } from "../../../models/domain/window-state"; import { WindowState } from "../../../models/domain/window-state";
import { ThemeType } from "../../enums"; import { ThemeType } from "../../enums";
@@ -24,7 +23,6 @@ export class GlobalState {
enableBiometrics?: boolean; enableBiometrics?: boolean;
biometricText?: string; biometricText?: string;
noAutoPromptBiometricsText?: string; noAutoPromptBiometricsText?: string;
environmentUrls: EnvironmentUrls = new EnvironmentUrls();
enableTray?: boolean; enableTray?: boolean;
enableMinimizeToTray?: boolean; enableMinimizeToTray?: boolean;
enableCloseToTray?: boolean; enableCloseToTray?: boolean;
@@ -34,7 +32,6 @@ export class GlobalState {
enableBrowserIntegration?: boolean; enableBrowserIntegration?: boolean;
enableBrowserIntegrationFingerprint?: boolean; enableBrowserIntegrationFingerprint?: boolean;
enableDuckDuckGoBrowserIntegration?: boolean; enableDuckDuckGoBrowserIntegration?: boolean;
region?: string;
neverDomains?: { [id: string]: unknown }; neverDomains?: { [id: string]: unknown };
enablePasskeys?: boolean; enablePasskeys?: boolean;
disableAddLoginNotification?: boolean; disableAddLoginNotification?: boolean;

View File

@@ -1,17 +1,22 @@
import { mock } from "jest-mock-extended";
import { firstValueFrom, timeout } from "rxjs"; import { firstValueFrom, timeout } from "rxjs";
import { awaitAsync } from "../../../spec"; import { awaitAsync } from "../../../spec";
import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service";
import { FakeStorageService } from "../../../spec/fake-storage.service"; import { FakeStorageService } from "../../../spec/fake-storage.service";
import { AuthenticationStatus } from "../../auth/enums/authentication-status";
import { EnvironmentUrls } from "../../auth/models/domain/environment-urls"; import { EnvironmentUrls } from "../../auth/models/domain/environment-urls";
import { UserId } from "../../types/guid"; import { UserId } from "../../types/guid";
import { Region } from "../abstractions/environment.service"; import { Region } from "../abstractions/environment.service";
import { StateFactory } from "../factories/state-factory"; import { StateProvider } from "../state";
import { Account } from "../models/domain/account"; /* eslint-disable import/no-restricted-paths -- Rare testing need */
import { GlobalState } from "../models/domain/global-state"; import { DefaultActiveUserStateProvider } from "../state/implementations/default-active-user-state.provider";
import { DefaultDerivedStateProvider } from "../state/implementations/default-derived-state.provider";
import { DefaultGlobalStateProvider } from "../state/implementations/default-global-state.provider";
import { DefaultSingleUserStateProvider } from "../state/implementations/default-single-user-state.provider";
import { DefaultStateProvider } from "../state/implementations/default-state.provider";
/* eslint-disable import/no-restricted-paths */
import { EnvironmentService } from "./environment.service"; import { EnvironmentService } from "./environment.service";
import { StateService } from "./state.service";
// There are a few main states EnvironmentService could be in when first used // There are a few main states EnvironmentService could be in when first used
// 1. Not initialized, no active user. Hopefully not to likely but possible // 1. Not initialized, no active user. Hopefully not to likely but possible
@@ -21,51 +26,55 @@ import { StateService } from "./state.service";
describe("EnvironmentService", () => { describe("EnvironmentService", () => {
let diskStorageService: FakeStorageService; let diskStorageService: FakeStorageService;
let memoryStorageService: FakeStorageService; let memoryStorageService: FakeStorageService;
let stateService: StateService; let accountService: FakeAccountService;
let stateProvider: StateProvider;
let sut: EnvironmentService; let sut: EnvironmentService;
const testUser = "testUser1" as UserId; const testUser = "00000000-0000-1000-a000-000000000001" as UserId;
const alternateTestUser = "00000000-0000-1000-a000-000000000002" as UserId;
// START: CAN CHANGE - When implementing new storage locations, the following code can change. But not tests
beforeEach(async () => { beforeEach(async () => {
diskStorageService = new FakeStorageService(); diskStorageService = new FakeStorageService();
memoryStorageService = new FakeStorageService(); memoryStorageService = new FakeStorageService();
stateService = new StateService(
diskStorageService, accountService = mockAccountServiceWith(undefined);
null, stateProvider = new DefaultStateProvider(
memoryStorageService as any, new DefaultActiveUserStateProvider(
mock(), accountService,
new StateFactory<GlobalState, Account>(GlobalState, Account), memoryStorageService as any,
mock(), diskStorageService,
false, ),
new DefaultSingleUserStateProvider(memoryStorageService as any, diskStorageService),
new DefaultGlobalStateProvider(memoryStorageService as any, diskStorageService),
new DefaultDerivedStateProvider(memoryStorageService),
); );
sut = new EnvironmentService(stateService); sut = new EnvironmentService(stateProvider, accountService);
}); });
const switchUser = async (userId: UserId) => { const switchUser = async (userId: UserId) => {
await stateService.setActiveUser(userId); accountService.activeAccountSubject.next({
id: userId,
email: "test@example.com",
name: `Test Name ${userId}`,
status: AuthenticationStatus.Unlocked,
});
await awaitAsync(); await awaitAsync();
}; };
const setGlobalData = (region: Region, environmentUrls: EnvironmentUrls) => { const setGlobalData = (region: Region, environmentUrls: EnvironmentUrls) => {
diskStorageService.internalUpdateStore({ const data = diskStorageService.internalStore;
...diskStorageService.internalStore, data["global_environment_region"] = region;
global: { data["global_environment_urls"] = environmentUrls;
region: region, diskStorageService.internalUpdateStore(data);
environmentUrls: environmentUrls,
},
});
}; };
const getGlobalData = () => { const getGlobalData = () => {
const storage = diskStorageService.internalStore as { const storage = diskStorageService.internalStore;
global?: { region?: Region; environmentUrls?: EnvironmentUrls };
};
return { return {
region: storage?.global?.region, region: storage?.["global_environment_region"],
urls: storage?.global?.environmentUrls, urls: storage?.["global_environment_urls"],
}; };
}; };
@@ -74,22 +83,15 @@ describe("EnvironmentService", () => {
environmentUrls: EnvironmentUrls, environmentUrls: EnvironmentUrls,
userId: UserId = testUser, userId: UserId = testUser,
) => { ) => {
const data = { ...diskStorageService.internalStore }; const data = diskStorageService.internalStore;
const userData = { data[`user_${userId}_environment_region`] = region;
settings: { data[`user_${userId}_environment_urls`] = environmentUrls;
region: region,
environmentUrls: environmentUrls,
},
};
data[userId] = userData;
diskStorageService.internalUpdateStore(data); diskStorageService.internalUpdateStore(data);
}; };
// END: CAN CHANGE // END: CAN CHANGE
const initialize = async (options: { switchUser: boolean }) => { const initialize = async (options: { switchUser: boolean }) => {
// This emulates the way EnvironmentService is initialized in each of our clients
await stateService.init();
await sut.setUrlsFromStorage(); await sut.setUrlsFromStorage();
sut.initialized = true; sut.initialized = true;
@@ -292,7 +294,7 @@ describe("EnvironmentService", () => {
}); });
const globalData = getGlobalData(); const globalData = getGlobalData();
expect(globalData.region).toBe("Self-hosted"); expect(globalData.region).toBe(Region.SelfHosted);
expect(globalData.urls).toEqual({ expect(globalData.urls).toEqual({
base: "https://base.example.com", base: "https://base.example.com",
api: null, api: null,
@@ -321,7 +323,7 @@ describe("EnvironmentService", () => {
}); });
const globalData = getGlobalData(); const globalData = getGlobalData();
expect(globalData.region).toBe("Self-hosted"); expect(globalData.region).toBe(Region.SelfHosted);
expect(globalData.urls).toEqual({ expect(globalData.urls).toEqual({
base: "https://base.example.com", base: "https://base.example.com",
api: "https://api.example.com", api: "https://api.example.com",
@@ -399,15 +401,13 @@ describe("EnvironmentService", () => {
])( ])(
"gets it from the passed in userId if there is any active user: %s", "gets it from the passed in userId if there is any active user: %s",
async ({ region, expectedHost }) => { async ({ region, expectedHost }) => {
const otherUser = "testUser2" as UserId;
setGlobalData(Region.US, new EnvironmentUrls()); setGlobalData(Region.US, new EnvironmentUrls());
setUserData(Region.US, new EnvironmentUrls()); setUserData(Region.US, new EnvironmentUrls());
setUserData(region, new EnvironmentUrls(), otherUser); setUserData(region, new EnvironmentUrls(), alternateTestUser);
await initialize({ switchUser: true }); await initialize({ switchUser: true });
const host = await sut.getHost(otherUser); const host = await sut.getHost(alternateTestUser);
expect(host).toBe(expectedHost); expect(host).toBe(expectedHost);
}, },
); );
@@ -438,17 +438,16 @@ describe("EnvironmentService", () => {
}); });
it("gets it from saved self host config from passed in user when there is an active user", async () => { it("gets it from saved self host config from passed in user when there is an active user", async () => {
const otherUser = "testUser2" as UserId;
setGlobalData(Region.US, new EnvironmentUrls()); setGlobalData(Region.US, new EnvironmentUrls());
setUserData(Region.EU, new EnvironmentUrls()); setUserData(Region.EU, new EnvironmentUrls());
const selfHostUserUrls = new EnvironmentUrls(); const selfHostUserUrls = new EnvironmentUrls();
selfHostUserUrls.base = "https://base.example.com"; selfHostUserUrls.base = "https://base.example.com";
setUserData(Region.SelfHosted, selfHostUserUrls, otherUser); setUserData(Region.SelfHosted, selfHostUserUrls, alternateTestUser);
await initialize({ switchUser: true }); await initialize({ switchUser: true });
const host = await sut.getHost(otherUser); const host = await sut.getHost(alternateTestUser);
expect(host).toBe("base.example.com"); expect(host).toBe("base.example.com");
}); });
}); });
@@ -498,7 +497,6 @@ describe("EnvironmentService", () => {
}); });
it("will get urls from signed in user", async () => { it("will get urls from signed in user", async () => {
await stateService.init();
await switchUser(testUser); await switchUser(testUser);
const userUrls = new EnvironmentUrls(); const userUrls = new EnvironmentUrls();

View File

@@ -1,14 +1,31 @@
import { concatMap, distinctUntilChanged, Observable, ReplaySubject } from "rxjs"; import {
concatMap,
distinctUntilChanged,
firstValueFrom,
map,
Observable,
ReplaySubject,
} from "rxjs";
import { AccountService } from "../../auth/abstractions/account.service";
import { EnvironmentUrls } from "../../auth/models/domain/environment-urls"; import { EnvironmentUrls } from "../../auth/models/domain/environment-urls";
import { UserId } from "../../types/guid";
import { import {
EnvironmentService as EnvironmentServiceAbstraction, EnvironmentService as EnvironmentServiceAbstraction,
Region, Region,
RegionDomain, RegionDomain,
Urls, Urls,
} from "../abstractions/environment.service"; } from "../abstractions/environment.service";
import { StateService } from "../abstractions/state.service";
import { Utils } from "../misc/utils"; import { Utils } from "../misc/utils";
import { ENVIRONMENT_DISK, GlobalState, KeyDefinition, StateProvider } from "../state";
const REGION_KEY = new KeyDefinition<Region>(ENVIRONMENT_DISK, "region", {
deserializer: (s) => s,
});
const URLS_KEY = new KeyDefinition<EnvironmentUrls>(ENVIRONMENT_DISK, "urls", {
deserializer: EnvironmentUrls.fromJSON,
});
export class EnvironmentService implements EnvironmentServiceAbstraction { export class EnvironmentService implements EnvironmentServiceAbstraction {
private readonly urlsSubject = new ReplaySubject<void>(1); private readonly urlsSubject = new ReplaySubject<void>(1);
@@ -27,6 +44,11 @@ export class EnvironmentService implements EnvironmentServiceAbstraction {
private scimUrl: string = null; private scimUrl: string = null;
private cloudWebVaultUrl: string; private cloudWebVaultUrl: string;
private regionGlobalState: GlobalState<Region | null>;
private urlsGlobalState: GlobalState<EnvironmentUrls | null>;
private activeAccountId$: Observable<UserId | null>;
readonly usUrls: Urls = { readonly usUrls: Urls = {
base: null, base: null,
api: "https://api.bitwarden.com", api: "https://api.bitwarden.com",
@@ -49,8 +71,15 @@ export class EnvironmentService implements EnvironmentServiceAbstraction {
scim: "https://scim.bitwarden.eu", scim: "https://scim.bitwarden.eu",
}; };
constructor(private stateService: StateService) { constructor(
this.stateService.activeAccount$ private stateProvider: StateProvider,
private accountService: AccountService,
) {
// We intentionally don't want the helper on account service, we want the null back if there is no active user
this.activeAccountId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id));
// TODO: Get rid of early subscription during EnvironmentService refactor
this.activeAccountId$
.pipe( .pipe(
// Use == here to not trigger on undefined -> null transition // Use == here to not trigger on undefined -> null transition
distinctUntilChanged((oldUserId: string, newUserId: string) => oldUserId == newUserId), distinctUntilChanged((oldUserId: string, newUserId: string) => oldUserId == newUserId),
@@ -62,6 +91,9 @@ export class EnvironmentService implements EnvironmentServiceAbstraction {
}), }),
) )
.subscribe(); .subscribe();
this.regionGlobalState = this.stateProvider.getGlobal(REGION_KEY);
this.urlsGlobalState = this.stateProvider.getGlobal(URLS_KEY);
} }
hasBaseUrl() { hasBaseUrl() {
@@ -180,8 +212,10 @@ export class EnvironmentService implements EnvironmentServiceAbstraction {
} }
async setUrlsFromStorage(): Promise<void> { async setUrlsFromStorage(): Promise<void> {
const region = await this.stateService.getRegion(); const activeUserId = await firstValueFrom(this.activeAccountId$);
const savedUrls = await this.stateService.getEnvironmentUrls();
const region = await this.getRegion(activeUserId);
const savedUrls = await this.getEnvironmentUrls(activeUserId);
const envUrls = new EnvironmentUrls(); const envUrls = new EnvironmentUrls();
// In release `2023.5.0`, we set the `base` property of the environment URLs to the US web vault URL when a user clicked the "US" region. // In release `2023.5.0`, we set the `base` property of the environment URLs to the US web vault URL when a user clicked the "US" region.
@@ -231,7 +265,8 @@ export class EnvironmentService implements EnvironmentServiceAbstraction {
// scimUrl cannot be cleared // scimUrl cannot be cleared
urls.scim = this.formatUrl(urls.scim) ?? this.scimUrl; urls.scim = this.formatUrl(urls.scim) ?? this.scimUrl;
await this.stateService.setEnvironmentUrls({ // Don't save scim url
await this.urlsGlobalState.update(() => ({
base: urls.base, base: urls.base,
api: urls.api, api: urls.api,
identity: urls.identity, identity: urls.identity,
@@ -240,8 +275,7 @@ export class EnvironmentService implements EnvironmentServiceAbstraction {
notifications: urls.notifications, notifications: urls.notifications,
events: urls.events, events: urls.events,
keyConnector: urls.keyConnector, keyConnector: urls.keyConnector,
// scimUrl is not saved to storage }));
});
this.baseUrl = urls.base; this.baseUrl = urls.base;
this.webVaultUrl = urls.webVault; this.webVaultUrl = urls.webVault;
@@ -287,8 +321,8 @@ export class EnvironmentService implements EnvironmentServiceAbstraction {
); );
} }
async getHost(userId?: string) { async getHost(userId?: UserId) {
const region = await this.getRegion(userId ? userId : null); const region = await this.getRegion(userId);
switch (region) { switch (region) {
case Region.US: case Region.US:
@@ -297,21 +331,30 @@ export class EnvironmentService implements EnvironmentServiceAbstraction {
return RegionDomain.EU; return RegionDomain.EU;
default: { default: {
// Environment is self-hosted // Environment is self-hosted
const envUrls = await this.stateService.getEnvironmentUrls( const envUrls = await this.getEnvironmentUrls(userId);
userId ? { userId: userId } : null,
);
return Utils.getHost(envUrls.webVault || envUrls.base); return Utils.getHost(envUrls.webVault || envUrls.base);
} }
} }
} }
private async getRegion(userId?: string) { private async getRegion(userId: UserId | null) {
return this.stateService.getRegion(userId ? { userId: userId } : null); // Previous rules dictated that we only get from user scoped state if there is an active user.
const activeUserId = await firstValueFrom(this.activeAccountId$);
return activeUserId == null
? await firstValueFrom(this.regionGlobalState.state$)
: await firstValueFrom(this.stateProvider.getUser(userId ?? activeUserId, REGION_KEY).state$);
}
private async getEnvironmentUrls(userId: UserId | null) {
return userId == null
? (await firstValueFrom(this.urlsGlobalState.state$)) ?? new EnvironmentUrls()
: (await firstValueFrom(this.stateProvider.getUser(userId, URLS_KEY).state$)) ??
new EnvironmentUrls();
} }
async setRegion(region: Region) { async setRegion(region: Region) {
this.selectedRegion = region; this.selectedRegion = region;
await this.stateService.setRegion(region); await this.regionGlobalState.update(() => region);
if (region === Region.SelfHosted) { if (region === Region.SelfHosted) {
// If user saves a self-hosted region with empty fields, default to US // If user saves a self-hosted region with empty fields, default to US
@@ -320,7 +363,7 @@ export class EnvironmentService implements EnvironmentServiceAbstraction {
} }
} else { } else {
// If we are setting the region to EU or US, clear the self-hosted URLs // If we are setting the region to EU or US, clear the self-hosted URLs
await this.stateService.setEnvironmentUrls(new EnvironmentUrls()); await this.urlsGlobalState.update(() => new EnvironmentUrls());
if (region === Region.EU) { if (region === Region.EU) {
this.setUrlsInternal(this.euUrls); this.setUrlsInternal(this.euUrls);
} else if (region === Region.US) { } else if (region === Region.US) {
@@ -329,6 +372,13 @@ export class EnvironmentService implements EnvironmentServiceAbstraction {
} }
} }
async seedUserEnvironment(userId: UserId) {
const globalRegion = await firstValueFrom(this.regionGlobalState.state$);
const globalUrls = await firstValueFrom(this.urlsGlobalState.state$);
await this.stateProvider.getUser(userId, REGION_KEY).update(() => globalRegion);
await this.stateProvider.getUser(userId, URLS_KEY).update(() => globalUrls);
}
private setUrlsInternal(urls: Urls) { private setUrlsInternal(urls: Urls) {
this.baseUrl = this.formatUrl(urls.base); this.baseUrl = this.formatUrl(urls.base);
this.webVaultUrl = this.formatUrl(urls.webVault); this.webVaultUrl = this.formatUrl(urls.webVault);

View File

@@ -9,7 +9,6 @@ import { Policy } from "../../admin-console/models/domain/policy";
import { AccountService } from "../../auth/abstractions/account.service"; import { AccountService } from "../../auth/abstractions/account.service";
import { AuthenticationStatus } from "../../auth/enums/authentication-status"; import { AuthenticationStatus } from "../../auth/enums/authentication-status";
import { AdminAuthRequestStorable } from "../../auth/models/domain/admin-auth-req-storable"; import { AdminAuthRequestStorable } from "../../auth/models/domain/admin-auth-req-storable";
import { EnvironmentUrls } from "../../auth/models/domain/environment-urls";
import { ForceSetPasswordReason } from "../../auth/models/domain/force-set-password-reason"; import { ForceSetPasswordReason } from "../../auth/models/domain/force-set-password-reason";
import { KdfConfig } from "../../auth/models/domain/kdf-config"; import { KdfConfig } from "../../auth/models/domain/kdf-config";
import { BiometricKey } from "../../auth/types/biometric-key"; import { BiometricKey } from "../../auth/types/biometric-key";
@@ -32,6 +31,7 @@ import { LocalData } from "../../vault/models/data/local.data";
import { CipherView } from "../../vault/models/view/cipher.view"; import { CipherView } from "../../vault/models/view/cipher.view";
import { CollectionView } from "../../vault/models/view/collection.view"; import { CollectionView } from "../../vault/models/view/collection.view";
import { AddEditCipherInfo } from "../../vault/types/add-edit-cipher-info"; import { AddEditCipherInfo } from "../../vault/types/add-edit-cipher-info";
import { EnvironmentService } from "../abstractions/environment.service";
import { LogService } from "../abstractions/log.service"; import { LogService } from "../abstractions/log.service";
import { StateService as StateServiceAbstraction } from "../abstractions/state.service"; import { StateService as StateServiceAbstraction } from "../abstractions/state.service";
import { import {
@@ -105,6 +105,7 @@ export class StateService<
protected logService: LogService, protected logService: LogService,
protected stateFactory: StateFactory<TGlobalState, TAccount>, protected stateFactory: StateFactory<TGlobalState, TAccount>,
protected accountService: AccountService, protected accountService: AccountService,
protected environmentService: EnvironmentService,
protected useAccountCache: boolean = true, protected useAccountCache: boolean = true,
) { ) {
// If the account gets changed, verify the new account is unlocked // If the account gets changed, verify the new account is unlocked
@@ -215,7 +216,7 @@ export class StateService<
} }
async addAccount(account: TAccount) { async addAccount(account: TAccount) {
account = await this.setAccountEnvironment(account); await this.environmentService.seedUserEnvironment(account.profile.userId as UserId);
await this.updateState(async (state) => { await this.updateState(async (state) => {
state.authenticatedAccounts.push(account.profile.userId); state.authenticatedAccounts.push(account.profile.userId);
await this.storageService.save(keys.authenticatedAccounts, state.authenticatedAccounts); await this.storageService.save(keys.authenticatedAccounts, state.authenticatedAccounts);
@@ -1983,49 +1984,6 @@ export class StateService<
); );
} }
async getEnvironmentUrls(options?: StorageOptions): Promise<EnvironmentUrls> {
if ((await this.state())?.activeUserId == null) {
return await this.getGlobalEnvironmentUrls(options);
}
options = this.reconcileOptions(options, await this.defaultOnDiskOptions());
return (await this.getAccount(options))?.settings?.environmentUrls ?? new EnvironmentUrls();
}
async setEnvironmentUrls(value: EnvironmentUrls, options?: StorageOptions): Promise<void> {
// Global values are set on each change and the current global settings are passed to any newly authed accounts.
// This is to allow setting environment values before an account is active, while still allowing individual accounts to have their own environments.
const globals = await this.getGlobals(
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
globals.environmentUrls = value;
await this.saveGlobals(
globals,
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
}
async getRegion(options?: StorageOptions): Promise<string> {
if ((await this.state())?.activeUserId == null) {
options = this.reconcileOptions(options, await this.defaultOnDiskOptions());
return (await this.getGlobals(options)).region ?? null;
}
options = this.reconcileOptions(options, await this.defaultOnDiskOptions());
return (await this.getAccount(options))?.settings?.region ?? null;
}
async setRegion(value: string, options?: StorageOptions): Promise<void> {
// Global values are set on each change and the current global settings are passed to any newly authed accounts.
// This is to allow setting region values before an account is active, while still allowing individual accounts to have their own region.
const globals = await this.getGlobals(
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
globals.region = value;
await this.saveGlobals(
globals,
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
}
async getEquivalentDomains(options?: StorageOptions): Promise<string[][]> { async getEquivalentDomains(options?: StorageOptions): Promise<string[][]> {
return ( return (
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
@@ -3032,17 +2990,12 @@ export class StateService<
await this.defaultOnDiskLocalOptions(), await this.defaultOnDiskLocalOptions(),
), ),
); );
// EnvironmentUrls and region are set before authenticating and should override whatever is stored from any previous session
const environmentUrls = account.settings.environmentUrls;
const region = account.settings.region;
if (storedAccount?.settings != null) { if (storedAccount?.settings != null) {
account.settings = storedAccount.settings; account.settings = storedAccount.settings;
} else if (await this.storageService.has(keys.tempAccountSettings)) { } else if (await this.storageService.has(keys.tempAccountSettings)) {
account.settings = await this.storageService.get<AccountSettings>(keys.tempAccountSettings); account.settings = await this.storageService.get<AccountSettings>(keys.tempAccountSettings);
await this.storageService.remove(keys.tempAccountSettings); await this.storageService.remove(keys.tempAccountSettings);
} }
account.settings.environmentUrls = environmentUrls;
account.settings.region = region;
if ( if (
account.settings.vaultTimeoutAction === VaultTimeoutAction.LogOut && account.settings.vaultTimeoutAction === VaultTimeoutAction.LogOut &&
@@ -3070,8 +3023,6 @@ export class StateService<
), ),
); );
if (storedAccount?.settings != null) { if (storedAccount?.settings != null) {
storedAccount.settings.environmentUrls = account.settings.environmentUrls;
storedAccount.settings.region = account.settings.region;
account.settings = storedAccount.settings; account.settings = storedAccount.settings;
} }
await this.storageService.save( await this.storageService.save(
@@ -3093,8 +3044,6 @@ export class StateService<
this.reconcileOptions({ userId: account.profile.userId }, await this.defaultOnDiskOptions()), this.reconcileOptions({ userId: account.profile.userId }, await this.defaultOnDiskOptions()),
); );
if (storedAccount?.settings != null) { if (storedAccount?.settings != null) {
storedAccount.settings.environmentUrls = account.settings.environmentUrls;
storedAccount.settings.region = account.settings.region;
account.settings = storedAccount.settings; account.settings = storedAccount.settings;
} }
await this.storageService.save( await this.storageService.save(
@@ -3237,23 +3186,6 @@ export class StateService<
return Object.assign(this.createAccount(), persistentAccountInformation); return Object.assign(this.createAccount(), persistentAccountInformation);
} }
// The environment urls and region are selected before login and are transferred here to an authenticated account
protected async setAccountEnvironment(account: TAccount): Promise<TAccount> {
account.settings.region = await this.getGlobalRegion();
account.settings.environmentUrls = await this.getGlobalEnvironmentUrls();
return account;
}
protected async getGlobalEnvironmentUrls(options?: StorageOptions): Promise<EnvironmentUrls> {
options = this.reconcileOptions(options, await this.defaultOnDiskOptions());
return (await this.getGlobals(options)).environmentUrls ?? new EnvironmentUrls();
}
protected async getGlobalRegion(options?: StorageOptions): Promise<string> {
options = this.reconcileOptions(options, await this.defaultOnDiskOptions());
return (await this.getGlobals(options)).region ?? null;
}
protected async clearDecryptedDataForActiveUser(): Promise<void> { protected async clearDecryptedDataForActiveUser(): Promise<void> {
await this.updateState(async (state) => { await this.updateState(async (state) => {
const userId = state?.activeUserId; const userId = state?.activeUserId;

View File

@@ -22,6 +22,7 @@ export const ACCOUNT_MEMORY = new StateDefinition("account", "memory");
export const BILLING_BANNERS_DISK = new StateDefinition("billingBanners", "disk"); export const BILLING_BANNERS_DISK = new StateDefinition("billingBanners", "disk");
export const CRYPTO_DISK = new StateDefinition("crypto", "disk"); export const CRYPTO_DISK = new StateDefinition("crypto", "disk");
export const ENVIRONMENT_DISK = new StateDefinition("environment", "disk");
export const GENERATOR_DISK = new StateDefinition("generator", "disk"); export const GENERATOR_DISK = new StateDefinition("generator", "disk");
export const GENERATOR_MEMORY = new StateDefinition("generator", "memory"); export const GENERATOR_MEMORY = new StateDefinition("generator", "memory");

View File

@@ -7,6 +7,7 @@ import { MigrationBuilder } from "./migration-builder";
import { MigrationHelper } from "./migration-helper"; import { MigrationHelper } from "./migration-helper";
import { EverHadUserKeyMigrator } from "./migrations/10-move-ever-had-user-key-to-state-providers"; 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 { OrganizationKeyMigrator } from "./migrations/11-move-org-keys-to-state-providers";
import { MoveEnvironmentStateToProviders } from "./migrations/12-move-environment-state-to-providers";
import { FixPremiumMigrator } from "./migrations/3-fix-premium"; import { FixPremiumMigrator } from "./migrations/3-fix-premium";
import { RemoveEverBeenUnlockedMigrator } from "./migrations/4-remove-ever-been-unlocked"; import { RemoveEverBeenUnlockedMigrator } from "./migrations/4-remove-ever-been-unlocked";
import { AddKeyTypeToOrgKeysMigrator } from "./migrations/5-add-key-type-to-org-keys"; import { AddKeyTypeToOrgKeysMigrator } from "./migrations/5-add-key-type-to-org-keys";
@@ -17,7 +18,7 @@ import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-setting
import { MinVersionMigrator } from "./migrations/min-version"; import { MinVersionMigrator } from "./migrations/min-version";
export const MIN_VERSION = 2; export const MIN_VERSION = 2;
export const CURRENT_VERSION = 11; export const CURRENT_VERSION = 12;
export type MinVersion = typeof MIN_VERSION; export type MinVersion = typeof MIN_VERSION;
export async function migrate( export async function migrate(
@@ -44,7 +45,8 @@ export async function migrate(
.with(MoveStateVersionMigrator, 7, 8) .with(MoveStateVersionMigrator, 7, 8)
.with(MoveBrowserSettingsToGlobal, 8, 9) .with(MoveBrowserSettingsToGlobal, 8, 9)
.with(EverHadUserKeyMigrator, 9, 10) .with(EverHadUserKeyMigrator, 9, 10)
.with(OrganizationKeyMigrator, 10, CURRENT_VERSION) .with(OrganizationKeyMigrator, 10, 11)
.with(MoveEnvironmentStateToProviders, 11, CURRENT_VERSION)
.migrate(migrationHelper); .migrate(migrationHelper);
} }

View File

@@ -1,11 +1,16 @@
import { MockProxy, mock } from "jest-mock-extended"; import { MockProxy, mock } from "jest-mock-extended";
// eslint-disable-next-line import/no-restricted-paths -- Needed to print log messages
import { FakeStorageService } from "../../spec/fake-storage.service";
// eslint-disable-next-line import/no-restricted-paths -- Needed to print log messages // eslint-disable-next-line import/no-restricted-paths -- Needed to print log messages
import { LogService } from "../platform/abstractions/log.service"; import { LogService } from "../platform/abstractions/log.service";
// eslint-disable-next-line import/no-restricted-paths -- Needed to interface with storage locations // eslint-disable-next-line import/no-restricted-paths -- Needed to interface with storage locations
import { AbstractStorageService } from "../platform/abstractions/storage.service"; 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 } from "./migration-helper"; import { MigrationHelper } from "./migration-helper";
import { Migrator } from "./migrator";
const exampleJSON = { const exampleJSON = {
authenticatedAccounts: [ authenticatedAccounts: [
@@ -172,3 +177,129 @@ export function mockMigrationHelper(
mockHelper.getAccounts.mockImplementation(() => helper.getAccounts()); mockHelper.getAccounts.mockImplementation(() => helper.getAccounts());
return mockHelper; return mockHelper;
} }
// TODO: Use const generic for TUsers in TypeScript 5.0 so consumers don't have to `as const` themselves
export type InitialDataHint<TUsers extends readonly string[]> = {
/**
* A string array of the users id who are authenticated
*
* NOTE: It's recommended to as const this string array so you get type help defining the users data
*/
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.
*/
// TODO: Use const generic for TUsers in TypeScript 5.0 so consumers don't have to `as const` themselves
export async function runMigrator<
TMigrator extends Migrator<number, number>,
TUsers extends readonly string[] = string[],
>(migrator: TMigrator, initalData?: InitialDataHint<TUsers>): Promise<Record<string, unknown>> {
// Inject fake data at every level of the object
const allInjectedData = injectData(initalData, []);
const fakeStorageService = new FakeStorageService(initalData);
const helper = new MigrationHelper(migrator.fromVersion, fakeStorageService, mock());
// Run their migrations
await migrator.migrate(helper);
const [data, leftoverInjectedData] = expectInjectedData(
fakeStorageService.internalStore,
allInjectedData,
);
expect(leftoverInjectedData).toHaveLength(0);
return data;
}

View File

@@ -0,0 +1,157 @@
import { runMigrator } from "../migration-helper.spec";
import { MoveEnvironmentStateToProviders } from "./12-move-environment-state-to-providers";
describe("MoveEnvironmentStateToProviders", () => {
const migrator = new MoveEnvironmentStateToProviders(11, 12);
it("can migrate all data", async () => {
const output = await runMigrator(migrator, {
authenticatedAccounts: ["user1", "user2"] as const,
global: {
region: "US",
environmentUrls: {
base: "example.com",
},
extra: "data",
},
user1: {
extra: "data",
settings: {
extra: "data",
region: "US",
environmentUrls: {
base: "example.com",
},
},
},
user2: {
extra: "data",
settings: {
region: "EU",
environmentUrls: {
base: "other.example.com",
},
extra: "data",
},
},
extra: "data",
});
expect(output).toEqual({
authenticatedAccounts: ["user1", "user2"],
global: {
extra: "data",
},
global_environment_region: "US",
global_environment_urls: {
base: "example.com",
},
user1: {
extra: "data",
settings: {
extra: "data",
},
},
user2: {
extra: "data",
settings: {
extra: "data",
},
},
extra: "data",
user_user1_environment_region: "US",
user_user2_environment_region: "EU",
user_user1_environment_urls: {
base: "example.com",
},
user_user2_environment_urls: {
base: "other.example.com",
},
});
});
it("handles missing parts", async () => {
const output = await runMigrator(migrator, {
authenticatedAccounts: ["user1", "user2"],
global: {
extra: "data",
},
user1: {
extra: "data",
settings: {
extra: "data",
},
},
user2: null,
});
expect(output).toEqual({
authenticatedAccounts: ["user1", "user2"],
global: {
extra: "data",
},
user1: {
extra: "data",
settings: {
extra: "data",
},
},
user2: null,
});
});
it("can migrate only global data", async () => {
const output = await runMigrator(migrator, {
authenticatedAccounts: [] as const,
global: {
region: "Self-Hosted",
},
});
expect(output).toEqual({
authenticatedAccounts: [],
global_environment_region: "Self-Hosted",
global: {},
});
});
it("can migrate only user state", async () => {
const output = await runMigrator(migrator, {
authenticatedAccounts: ["user1"] as const,
global: null,
user1: {
settings: {
region: "Self-Hosted",
environmentUrls: {
base: "some-base-url",
api: "some-api-url",
identity: "some-identity-url",
icons: "some-icons-url",
notifications: "some-notifications-url",
events: "some-events-url",
webVault: "some-webVault-url",
keyConnector: "some-keyConnector-url",
},
},
},
});
expect(output).toEqual({
authenticatedAccounts: ["user1"] as const,
global: null,
user1: { settings: {} },
user_user1_environment_region: "Self-Hosted",
user_user1_environment_urls: {
base: "some-base-url",
api: "some-api-url",
identity: "some-identity-url",
icons: "some-icons-url",
notifications: "some-notifications-url",
events: "some-events-url",
webVault: "some-webVault-url",
keyConnector: "some-keyConnector-url",
},
});
});
});

View File

@@ -0,0 +1,132 @@
import { KeyDefinitionLike, MigrationHelper, StateDefinitionLike } from "../migration-helper";
import { Migrator } from "../migrator";
type EnvironmentUrls = Record<string, string>;
type ExpectedAccountType = {
settings?: { region?: string; environmentUrls?: EnvironmentUrls };
};
type ExpectedGlobalType = { region?: string; environmentUrls?: EnvironmentUrls };
const ENVIRONMENT_STATE: StateDefinitionLike = { name: "environment" };
const REGION_KEY: KeyDefinitionLike = { key: "region", stateDefinition: ENVIRONMENT_STATE };
const URLS_KEY: KeyDefinitionLike = { key: "urls", stateDefinition: ENVIRONMENT_STATE };
export class MoveEnvironmentStateToProviders extends Migrator<11, 12> {
async migrate(helper: MigrationHelper): Promise<void> {
const legacyGlobal = await helper.get<ExpectedGlobalType>("global");
// Move global data
if (legacyGlobal?.region != null) {
await helper.setToGlobal(REGION_KEY, legacyGlobal.region);
}
if (legacyGlobal?.environmentUrls != null) {
await helper.setToGlobal(URLS_KEY, legacyGlobal.environmentUrls);
}
const legacyAccounts = await helper.getAccounts<ExpectedAccountType>();
await Promise.all(
legacyAccounts.map(async ({ userId, account }) => {
// Move account data
if (account?.settings?.region != null) {
await helper.setToUser(userId, REGION_KEY, account.settings.region);
}
if (account?.settings?.environmentUrls != null) {
await helper.setToUser(userId, URLS_KEY, account.settings.environmentUrls);
}
// Delete old account data
delete account?.settings?.region;
delete account?.settings?.environmentUrls;
await helper.set(userId, account);
}),
);
// Delete legacy global data
delete legacyGlobal?.region;
delete legacyGlobal?.environmentUrls;
await helper.set("global", legacyGlobal);
}
async rollback(helper: MigrationHelper): Promise<void> {
let legacyGlobal = await helper.get<ExpectedGlobalType>("global");
let updatedLegacyGlobal = false;
const globalRegion = await helper.getFromGlobal<string>(REGION_KEY);
if (globalRegion) {
if (!legacyGlobal) {
legacyGlobal = {};
}
updatedLegacyGlobal = true;
legacyGlobal.region = globalRegion;
await helper.setToGlobal(REGION_KEY, null);
}
const globalUrls = await helper.getFromGlobal<EnvironmentUrls>(URLS_KEY);
if (globalUrls) {
if (!legacyGlobal) {
legacyGlobal = {};
}
updatedLegacyGlobal = true;
legacyGlobal.environmentUrls = globalUrls;
await helper.setToGlobal(URLS_KEY, null);
}
if (updatedLegacyGlobal) {
await helper.set("global", legacyGlobal);
}
async function rollbackUser(userId: string, account: ExpectedAccountType) {
let updatedAccount = false;
const userRegion = await helper.getFromUser<string>(userId, REGION_KEY);
if (userRegion) {
if (!account) {
account = {};
}
if (!account.settings) {
account.settings = {};
}
updatedAccount = true;
account.settings.region = userRegion;
await helper.setToUser(userId, REGION_KEY, null);
}
const userUrls = await helper.getFromUser<EnvironmentUrls>(userId, URLS_KEY);
if (userUrls) {
if (!account) {
account = {};
}
if (!account.settings) {
account.settings = {};
}
updatedAccount = true;
account.settings.environmentUrls = userUrls;
await helper.setToUser(userId, URLS_KEY, null);
}
if (updatedAccount) {
await helper.set(userId, account);
}
}
const accounts = await helper.getAccounts<ExpectedAccountType>();
await Promise.all(accounts.map(({ userId, account }) => rollbackUser(userId, account)));
}
}

View File

@@ -1,31 +1,14 @@
import { mock } from "jest-mock-extended"; import { runMigrator } from "../migration-helper.spec";
import { FakeStorageService } from "../../../spec/fake-storage.service";
import { MigrationHelper } from "../migration-helper";
import { Migrator } from "../migrator";
import { MoveBrowserSettingsToGlobal } from "./9-move-browser-settings-to-global"; import { MoveBrowserSettingsToGlobal } from "./9-move-browser-settings-to-global";
type TestState = { authenticatedAccounts: string[] } & { [key: string]: unknown };
// This could become a helper available to anyone
const runMigrator = async <TMigrator extends Migrator<number, number>>(
migrator: TMigrator,
initalData?: Record<string, unknown>,
): Promise<Record<string, unknown>> => {
const fakeStorageService = new FakeStorageService(initalData);
const helper = new MigrationHelper(migrator.fromVersion, fakeStorageService, mock());
await migrator.migrate(helper);
return fakeStorageService.internalStore;
};
describe("MoveBrowserSettingsToGlobal", () => { describe("MoveBrowserSettingsToGlobal", () => {
const myMigrator = new MoveBrowserSettingsToGlobal(8, 9); const myMigrator = new MoveBrowserSettingsToGlobal(8, 9);
// This could be the state for a browser client who has never touched the settings or this could // This could be the state for a browser client who has never touched the settings or this could
// be a different client who doesn't make it possible to toggle these settings // be a different client who doesn't make it possible to toggle these settings
it("doesn't set any value to global if there is no equivalent settings on the account", async () => { it("doesn't set any value to global if there is no equivalent settings on the account", async () => {
const testInput: TestState = { const output = await runMigrator(myMigrator, {
authenticatedAccounts: ["user1"], authenticatedAccounts: ["user1"],
global: { global: {
theme: "system", // A real global setting that should persist after migration theme: "system", // A real global setting that should persist after migration
@@ -35,9 +18,7 @@ describe("MoveBrowserSettingsToGlobal", () => {
region: "Self-hosted", region: "Self-hosted",
}, },
}, },
}; });
const output = await runMigrator(myMigrator, testInput);
// No additions to the global state // No additions to the global state
expect(output["global"]).toEqual({ expect(output["global"]).toEqual({
@@ -55,7 +36,7 @@ describe("MoveBrowserSettingsToGlobal", () => {
// This could be a user who opened up the settings page and toggled the checkbox, since this setting infers undefined // This could be a user who opened up the settings page and toggled the checkbox, since this setting infers undefined
// as false this is essentially the default value. // as false this is essentially the default value.
it("sets the setting from the users settings if they have toggled the setting but placed it back to it's inferred", async () => { it("sets the setting from the users settings if they have toggled the setting but placed it back to it's inferred", async () => {
const testInput: TestState = { const output = await runMigrator(myMigrator, {
authenticatedAccounts: ["user1"], authenticatedAccounts: ["user1"],
global: { global: {
theme: "system", // A real global setting that should persist after migration theme: "system", // A real global setting that should persist after migration
@@ -71,9 +52,7 @@ describe("MoveBrowserSettingsToGlobal", () => {
region: "Self-hosted", region: "Self-hosted",
}, },
}, },
}; });
const output = await runMigrator(myMigrator, testInput);
// User settings should have moved to global // User settings should have moved to global
expect(output["global"]).toEqual({ expect(output["global"]).toEqual({
@@ -94,7 +73,7 @@ describe("MoveBrowserSettingsToGlobal", () => {
// The user has set a value and it's not the default, we should respect that choice globally // The user has set a value and it's not the default, we should respect that choice globally
it("should take the only users settings", async () => { it("should take the only users settings", async () => {
const testInput: TestState = { const output = await runMigrator(myMigrator, {
authenticatedAccounts: ["user1"], authenticatedAccounts: ["user1"],
global: { global: {
theme: "system", // A real global setting that should persist after migration theme: "system", // A real global setting that should persist after migration
@@ -110,9 +89,7 @@ describe("MoveBrowserSettingsToGlobal", () => {
region: "Self-hosted", region: "Self-hosted",
}, },
}, },
}; });
const output = await runMigrator(myMigrator, testInput);
// The value for the single user value should be set to global // The value for the single user value should be set to global
expect(output["global"]).toEqual({ expect(output["global"]).toEqual({
@@ -134,7 +111,7 @@ describe("MoveBrowserSettingsToGlobal", () => {
// but in the bizzare case, we should interpret any user having the feature turned on as the value for // but in the bizzare case, we should interpret any user having the feature turned on as the value for
// all the accounts. // all the accounts.
it("should take the false value if there are conflicting choices", async () => { it("should take the false value if there are conflicting choices", async () => {
const testInput: TestState = { const output = await runMigrator(myMigrator, {
authenticatedAccounts: ["user1", "user2"], authenticatedAccounts: ["user1", "user2"],
global: { global: {
theme: "system", // A real global setting that should persist after migration theme: "system", // A real global setting that should persist after migration
@@ -161,9 +138,7 @@ describe("MoveBrowserSettingsToGlobal", () => {
region: "Self-hosted", region: "Self-hosted",
}, },
}, },
}; });
const output = await runMigrator(myMigrator, testInput);
// The false settings should be respected over the true values // The false settings should be respected over the true values
// neverDomains should be combined into a single object // neverDomains should be combined into a single object
@@ -191,7 +166,7 @@ describe("MoveBrowserSettingsToGlobal", () => {
// if one user has toggled the setting back to on and one user has never touched the setting, // if one user has toggled the setting back to on and one user has never touched the setting,
// persist the false value into the global state. // persist the false value into the global state.
it("should persist the false value if one user has that in their settings", async () => { it("should persist the false value if one user has that in their settings", async () => {
const testInput: TestState = { const output = await runMigrator(myMigrator, {
authenticatedAccounts: ["user1", "user2"], authenticatedAccounts: ["user1", "user2"],
global: { global: {
theme: "system", // A real global setting that should persist after migration theme: "system", // A real global setting that should persist after migration
@@ -212,9 +187,7 @@ describe("MoveBrowserSettingsToGlobal", () => {
region: "Self-hosted", region: "Self-hosted",
}, },
}, },
}; });
const output = await runMigrator(myMigrator, testInput);
// The false settings should be respected over the true values // The false settings should be respected over the true values
// neverDomains should be combined into a single object // neverDomains should be combined into a single object
@@ -241,7 +214,7 @@ describe("MoveBrowserSettingsToGlobal", () => {
// if one user has toggled the setting off and one user has never touched the setting, // if one user has toggled the setting off and one user has never touched the setting,
// persist the false value into the global state. // persist the false value into the global state.
it("should persist the false value from a user with no settings since undefined is inferred as false", async () => { it("should persist the false value from a user with no settings since undefined is inferred as false", async () => {
const testInput: TestState = { const output = await runMigrator(myMigrator, {
authenticatedAccounts: ["user1", "user2"], authenticatedAccounts: ["user1", "user2"],
global: { global: {
theme: "system", // A real global setting that should persist after migration theme: "system", // A real global setting that should persist after migration
@@ -262,9 +235,7 @@ describe("MoveBrowserSettingsToGlobal", () => {
region: "Self-hosted", region: "Self-hosted",
}, },
}, },
}; });
const output = await runMigrator(myMigrator, testInput);
// The false settings should be respected over the true values // The false settings should be respected over the true values
// neverDomains should be combined into a single object // neverDomains should be combined into a single object
@@ -292,7 +263,7 @@ describe("MoveBrowserSettingsToGlobal", () => {
// id of the non-current account isn't saved to the authenticatedAccounts array so we don't have a great way to // id of the non-current account isn't saved to the authenticatedAccounts array so we don't have a great way to
// get the state and include it in our calculations for what the global state should be. // get the state and include it in our calculations for what the global state should be.
it("only cares about users defined in authenticatedAccounts", async () => { it("only cares about users defined in authenticatedAccounts", async () => {
const testInput: TestState = { const output = await runMigrator(myMigrator, {
authenticatedAccounts: ["user1"], authenticatedAccounts: ["user1"],
global: { global: {
theme: "system", // A real global setting that should persist after migration theme: "system", // A real global setting that should persist after migration
@@ -319,9 +290,7 @@ describe("MoveBrowserSettingsToGlobal", () => {
region: "Self-hosted", region: "Self-hosted",
}, },
}, },
}; });
const output = await runMigrator(myMigrator, testInput);
// The true settings should be respected over the false values because that whole users values // The true settings should be respected over the false values because that whole users values
// shouldn't be respected. // shouldn't be respected.