import type { AccountLoginStorage } from '../../../../src/identity/interaction/account/util/LoginStorage'; import { ACCOUNT_TYPE } from '../../../../src/identity/interaction/account/util/LoginStorage'; import { CLIENT_CREDENTIALS_STORAGE_TYPE, } from '../../../../src/identity/interaction/client-credentials/util/BaseClientCredentialsStore'; import { PASSWORD_STORAGE_TYPE } from '../../../../src/identity/interaction/password/util/BasePasswordStore'; import { OWNER_STORAGE_TYPE, POD_STORAGE_TYPE } from '../../../../src/identity/interaction/pod/util/BasePodStore'; import { WEBID_STORAGE_TYPE } from '../../../../src/identity/interaction/webid/util/BaseWebIdStore'; import { V6MigrationInitializer } from '../../../../src/init/migration/V6MigrationInitializer'; import type { KeyValueStorage } from '../../../../src/storage/keyvalue/KeyValueStorage'; type Account = { webId: string; email: string; password: string; verified: boolean; }; type Settings = { useIdp: boolean; podBaseUrl?: string; clientCredentials?: string[]; }; type ClientCredentials = { webId: string; secret: string; }; const questionMock = jest.fn().mockImplementation((input, callback): void => callback('y')); const closeMock = jest.fn(); jest.mock('node:readline', (): any => ({ createInterface: jest.fn().mockImplementation((): any => ({ question: questionMock, close: closeMock, })), })); describe('A V6MigrationInitializer', (): void => { const webId = 'http://example.com/test/profile/card#me'; const webId2 = 'http://example.com/test2/profile/card#me'; let settings: Record; let accounts: Record; let clientCredentials: Record; const versionKey = 'version'; let setupStorage: jest.Mocked>; let accountStorage: jest.Mocked>; let clientCredentialsStorage: jest.Mocked>; let forgotPasswordStorage: jest.Mocked>; let newAccountStorage: jest.Mocked>; let newSetupStorage: jest.Mocked>; let initializer: V6MigrationInitializer; beforeEach(async(): Promise => { settings = { [webId]: { useIdp: true, podBaseUrl: 'http://example.com/test/', clientCredentials: [ 'token' ]}, [webId2]: { useIdp: true, podBaseUrl: 'http://example.com/test2/' }, }; accounts = { account: { email: 'EMAIL@example.com', password: '123', webId, verified: true }, account2: { email: 'email2@example.com', password: '1234', webId: webId2, verified: true }, }; clientCredentials = { token: { webId, secret: 'secret!' }, }; setupStorage = { get: jest.fn().mockResolvedValue('6.0.0'), delete: jest.fn(), entries: jest.fn(async function* (): AsyncGenerator<[string, string]> { yield [ 'version', '6.0.0' ]; }), } satisfies Partial> as any; newSetupStorage = { set: jest.fn(), } satisfies Partial> as any; accountStorage = { get: jest.fn((id): any => settings[id] ?? accounts[id]), delete: jest.fn(), entries: jest.fn(async function* (): AsyncIterableIterator<[string, any]> { yield* Object.entries(accounts); yield* Object.entries(settings); }), } satisfies Partial> as any; clientCredentialsStorage = { delete: jest.fn(), entries: jest.fn(async function* (): AsyncIterableIterator<[string, any]> { yield* Object.entries(clientCredentials); }), } satisfies Partial> as any; forgotPasswordStorage = { delete: jest.fn(), entries: jest.fn(async function* (): AsyncIterableIterator<[string, any]> { yield [ 'forgot', {}]; }), } satisfies Partial> as any; newAccountStorage = { create: jest.fn((type): any => ({ id: `${type}-id` })), } satisfies Partial> as any; initializer = new V6MigrationInitializer({ versionKey, setupStorage, accountStorage, clientCredentialsStorage, cleanupStorages: [ accountStorage, clientCredentialsStorage, forgotPasswordStorage ], newAccountStorage, newSetupStorage, skipConfirmation: true, }); }); it('migrates the data.', async(): Promise => { await expect(initializer.handle()).resolves.toBeUndefined(); expect(setupStorage.get).toHaveBeenCalledTimes(1); expect(setupStorage.get).toHaveBeenLastCalledWith(versionKey); expect(accountStorage.get).toHaveBeenCalledTimes(2); expect(accountStorage.get).toHaveBeenCalledWith(webId); expect(accountStorage.get).toHaveBeenCalledWith(webId2); expect(accountStorage.delete).toHaveBeenCalledTimes(4); expect(accountStorage.delete).toHaveBeenCalledWith(webId); expect(accountStorage.delete).toHaveBeenCalledWith(webId2); expect(accountStorage.delete).toHaveBeenCalledWith('account'); expect(accountStorage.delete).toHaveBeenCalledWith('account2'); expect(clientCredentialsStorage.delete).toHaveBeenCalledTimes(1); expect(clientCredentialsStorage.delete).toHaveBeenCalledWith('token'); expect(forgotPasswordStorage.delete).toHaveBeenCalledTimes(1); expect(forgotPasswordStorage.delete).toHaveBeenCalledWith('forgot'); expect(newAccountStorage.create).toHaveBeenCalledTimes(11); expect(newAccountStorage.create).toHaveBeenCalledWith(ACCOUNT_TYPE, {}); expect(newAccountStorage.create).toHaveBeenCalledWith( PASSWORD_STORAGE_TYPE, { email: 'email@example.com', password: '123', verified: true, accountId: 'account-id' }, ); expect(newAccountStorage.create).toHaveBeenCalledWith( PASSWORD_STORAGE_TYPE, { email: 'email2@example.com', password: '1234', verified: true, accountId: 'account-id' }, ); expect(newAccountStorage.create).toHaveBeenCalledWith(WEBID_STORAGE_TYPE, { webId, accountId: 'account-id' }); expect(newAccountStorage.create).toHaveBeenCalledWith( WEBID_STORAGE_TYPE, { webId: webId2, accountId: 'account-id' }, ); expect(newAccountStorage.create).toHaveBeenCalledWith(POD_STORAGE_TYPE, { baseUrl: 'http://example.com/test/', accountId: 'account-id' }); expect(newAccountStorage.create).toHaveBeenCalledWith(POD_STORAGE_TYPE, { baseUrl: 'http://example.com/test2/', accountId: 'account-id' }); expect(newAccountStorage.create).toHaveBeenCalledWith( OWNER_STORAGE_TYPE, { webId, podId: 'pod-id', visible: false }, ); expect(newAccountStorage.create).toHaveBeenCalledWith( OWNER_STORAGE_TYPE, { webId: webId2, podId: 'pod-id', visible: false }, ); expect(newAccountStorage.create).toHaveBeenCalledWith( CLIENT_CREDENTIALS_STORAGE_TYPE, { label: 'token', secret: 'secret!', webId, accountId: 'account-id' }, ); expect(newSetupStorage.set).toHaveBeenCalledTimes(1); expect(newSetupStorage.set).toHaveBeenLastCalledWith('version', '6.0.0'); expect(setupStorage.delete).toHaveBeenCalledTimes(1); expect(setupStorage.delete).toHaveBeenLastCalledWith('version'); }); it('does nothing if the server has no stored version number.', async(): Promise => { setupStorage.get.mockResolvedValueOnce(undefined); await expect(initializer.handle()).resolves.toBeUndefined(); expect(accountStorage.get).toHaveBeenCalledTimes(0); expect(newAccountStorage.create).toHaveBeenCalledTimes(0); }); it('does nothing if stored version is more than 6.', async(): Promise => { setupStorage.get.mockResolvedValueOnce('7.0.0'); await expect(initializer.handle()).resolves.toBeUndefined(); expect(accountStorage.get).toHaveBeenCalledTimes(0); expect(newAccountStorage.create).toHaveBeenCalledTimes(0); }); it('ignores accounts and credentials for which it cannot find the settings.', async(): Promise => { delete settings[webId]; await expect(initializer.handle()).resolves.toBeUndefined(); expect(setupStorage.get).toHaveBeenCalledTimes(1); expect(setupStorage.get).toHaveBeenLastCalledWith(versionKey); expect(accountStorage.get).toHaveBeenCalledTimes(2); expect(accountStorage.get).toHaveBeenCalledWith(webId); expect(accountStorage.get).toHaveBeenCalledWith(webId2); expect(accountStorage.delete).toHaveBeenCalledTimes(3); expect(accountStorage.delete).toHaveBeenCalledWith(webId2); expect(accountStorage.delete).toHaveBeenCalledWith('account'); expect(accountStorage.delete).toHaveBeenCalledWith('account2'); expect(newAccountStorage.create).toHaveBeenCalledTimes(5); expect(newAccountStorage.create).toHaveBeenCalledWith(ACCOUNT_TYPE, {}); expect(newAccountStorage.create).toHaveBeenCalledWith( PASSWORD_STORAGE_TYPE, { email: 'email2@example.com', password: '1234', verified: true, accountId: 'account-id' }, ); expect(newAccountStorage.create).toHaveBeenCalledWith( WEBID_STORAGE_TYPE, { webId: webId2, accountId: 'account-id' }, ); expect(newAccountStorage.create).toHaveBeenCalledWith(POD_STORAGE_TYPE, { baseUrl: 'http://example.com/test2/', accountId: 'account-id' }); expect(newAccountStorage.create).toHaveBeenCalledWith( OWNER_STORAGE_TYPE, { webId: webId2, podId: 'pod-id', visible: false }, ); expect(newSetupStorage.set).toHaveBeenCalledTimes(1); expect(newSetupStorage.set).toHaveBeenLastCalledWith('version', '6.0.0'); expect(setupStorage.delete).toHaveBeenCalledTimes(1); expect(setupStorage.delete).toHaveBeenLastCalledWith('version'); }); describe('with prompts enabled', (): void => { beforeEach(async(): Promise => { jest.clearAllMocks(); initializer = new V6MigrationInitializer({ versionKey, setupStorage, accountStorage, clientCredentialsStorage, cleanupStorages: [ accountStorage, clientCredentialsStorage, forgotPasswordStorage ], newAccountStorage, newSetupStorage, skipConfirmation: false, }); }); it('shows a prompt before migrating the data.', async(): Promise => { await expect(initializer.handle()).resolves.toBeUndefined(); expect(questionMock).toHaveBeenCalledTimes(1); expect(questionMock.mock.invocationCallOrder[0]) .toBeLessThan(newAccountStorage.create.mock.invocationCallOrder[0]); expect(newAccountStorage.create).toHaveBeenCalledTimes(11); }); it('throws an error to stop the server if no positive answer is received.', async(): Promise => { questionMock.mockImplementation((input, callback): void => callback('n')); await expect(initializer.handle()).rejects.toThrow('Stopping server as migration was cancelled.'); expect(newAccountStorage.create).toHaveBeenCalledTimes(0); }); }); });