mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
feat: Add migration for v6 account data
This commit is contained in:
@@ -0,0 +1 @@
|
||||
{"email":"test@example.com","password":"$2a$10$t0eTjR3aUe3BSc3Cy4wXdeEpNoFK6O7vdtAvVyQTswOPlMmeT4sYK","verified":true,"webId":"http://localhost:6999/test/profile/card#me"}
|
||||
@@ -0,0 +1 @@
|
||||
{"email":"test2@example.com","password":"$2a$10$5hVJglnlJDbPymrA8SzBrus7zUndQfWi9wwBq.sYUuUdS8fjskEGm","verified":true,"webId":"http://localhost:6999/test2/profile/card#me"}
|
||||
@@ -0,0 +1 @@
|
||||
{"useIdp":true,"podBaseUrl":"http://localhost:6999/test/","clientCredentials":["token_fd13b73d-2527-4280-82af-278e5b8fe607"]}
|
||||
@@ -0,0 +1 @@
|
||||
{"useIdp":true,"podBaseUrl":"http://localhost:6999/test2/","clientCredentials":[]}
|
||||
@@ -0,0 +1 @@
|
||||
{"secret":"a809d7ce5daf0e9acd457c91d712ff05038e4a87192e27191c837602bd4b370c633282864c133650b0e9a35b59018b064157532642f628affb2f79e81999e898","webId":"http://localhost:6999/test/profile/card#me"}
|
||||
@@ -0,0 +1 @@
|
||||
{"expires":"2023-10-03T15:21:29.106Z","payload":{"recordId":"9524afb4-dfb9-48bf-a139-a30aaf4ff685","email":"test@example.com"}}
|
||||
@@ -0,0 +1 @@
|
||||
"http://localhost:6999/"
|
||||
@@ -0,0 +1 @@
|
||||
"6.0.2"
|
||||
@@ -0,0 +1 @@
|
||||
true
|
||||
1
test/assets/migration/v6/test/.acl
Normal file
1
test/assets/migration/v6/test/.acl
Normal file
@@ -0,0 +1 @@
|
||||
# Test comment for integration test
|
||||
1
test/assets/migration/v6/test/.meta
Normal file
1
test/assets/migration/v6/test/.meta
Normal file
@@ -0,0 +1 @@
|
||||
<http://localhost:6999/test/> a <http://www.w3.org/ns/pim/space#Storage>.
|
||||
12
test/assets/migration/v6/test/profile/card$.ttl
Normal file
12
test/assets/migration/v6/test/profile/card$.ttl
Normal file
@@ -0,0 +1,12 @@
|
||||
@prefix foaf: <http://xmlns.com/foaf/0.1/>.
|
||||
@prefix solid: <http://www.w3.org/ns/solid/terms#>.
|
||||
|
||||
<>
|
||||
a foaf:PersonalProfileDocument;
|
||||
foaf:maker <http://localhost:6999/test/profile/card#me>;
|
||||
foaf:primaryTopic <http://localhost:6999/test/profile/card#me>.
|
||||
|
||||
<http://localhost:6999/test/profile/card#me>
|
||||
|
||||
solid:oidcIssuer <http://localhost:6999/>;
|
||||
a foaf:Person.
|
||||
8
test/assets/migration/v6/test/profile/card.acl
Normal file
8
test/assets/migration/v6/test/profile/card.acl
Normal file
@@ -0,0 +1,8 @@
|
||||
@prefix acl: <http://www.w3.org/ns/auth/acl#>.
|
||||
@prefix foaf: <http://xmlns.com/foaf/0.1/>.
|
||||
|
||||
<#public>
|
||||
a acl:Authorization;
|
||||
acl:agentClass foaf:Agent;
|
||||
acl:accessTo <./card>;
|
||||
acl:mode acl:Read.
|
||||
@@ -56,5 +56,6 @@ export function getDefaultVariables(port: number, baseUrl?: string): Record<stri
|
||||
'urn:solid-server:default:variable:showStackTrace': true,
|
||||
'urn:solid-server:default:variable:seedConfig': null,
|
||||
'urn:solid-server:default:variable:workers': 1,
|
||||
'urn:solid-server:default:variable:confirmMigration': false,
|
||||
};
|
||||
}
|
||||
|
||||
145
test/integration/V6Migration.test.ts
Normal file
145
test/integration/V6Migration.test.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import { createDpopHeader, generateDpopKeyPair } from '@inrupt/solid-client-authn-core';
|
||||
import fetch from 'cross-fetch';
|
||||
import { copy, readdir } from 'fs-extra';
|
||||
import type { App } from '../../src/init/App';
|
||||
import { APPLICATION_X_WWW_FORM_URLENCODED } from '../../src/util/ContentTypes';
|
||||
import { joinFilePath, joinUrl, resolveAssetPath } from '../../src/util/PathUtil';
|
||||
import { getPort } from '../util/Util';
|
||||
import { getDefaultVariables, getTestConfigPath, getTestFolder, instantiateFromConfig, removeFolder } from './Config';
|
||||
import { IdentityTestState } from './IdentityTestState';
|
||||
|
||||
// This port needs to remain fixed as the assets used are generated with this port in mind
|
||||
const port = getPort('V6Migration');
|
||||
const baseUrl = `http://localhost:${port}/`;
|
||||
const rootFilePath = getTestFolder('v6-migration');
|
||||
const assetPath = resolveAssetPath('@css:test/assets/migration/v6/');
|
||||
|
||||
// Prevent panva/node-openid-client from emitting DraftWarning
|
||||
jest.spyOn(process, 'emitWarning').mockImplementation();
|
||||
|
||||
describe('A server migrating from v6', (): void => {
|
||||
let app: App;
|
||||
|
||||
beforeAll(async(): Promise<void> => {
|
||||
await removeFolder(rootFilePath);
|
||||
const variables = {
|
||||
...getDefaultVariables(port, baseUrl),
|
||||
'urn:solid-server:default:variable:rootFilePath': rootFilePath,
|
||||
// Skip the confirmation prompt
|
||||
'urn:solid-server:default:variable:confirmMigration': true,
|
||||
};
|
||||
|
||||
// Create and start the server
|
||||
const instances = await instantiateFromConfig(
|
||||
'urn:solid-server:test:Instances',
|
||||
[
|
||||
getTestConfigPath('file-pod.json'),
|
||||
],
|
||||
variables,
|
||||
) as Record<string, any>;
|
||||
({ app } = instances);
|
||||
|
||||
// Move the v6 internal data to the server
|
||||
await copy(assetPath, rootFilePath);
|
||||
});
|
||||
|
||||
afterAll(async(): Promise<void> => {
|
||||
// Await removeFolder(rootFilePath);
|
||||
await app.stop();
|
||||
});
|
||||
|
||||
it('can start the server to migrate the data.', async(): Promise<void> => {
|
||||
// This is going to trigger the migration step
|
||||
await expect(app.start()).resolves.toBeUndefined();
|
||||
// If migration was successful, there should be no files left in these folders
|
||||
const accountDir = await readdir(joinFilePath(rootFilePath, '.internal/accounts/'));
|
||||
expect(accountDir).toEqual(expect.arrayContaining([ 'data', 'index', 'credentials' ]));
|
||||
const credentialsDir = await readdir(joinFilePath(rootFilePath, '.internal/accounts/credentials/'));
|
||||
expect(credentialsDir).toEqual([]);
|
||||
const forgotDir = await readdir(joinFilePath(rootFilePath, '.internal/forgot-password/'));
|
||||
expect(forgotDir).toEqual([]);
|
||||
});
|
||||
|
||||
it('still allows existing accounts to log in.', async(): Promise<void> => {
|
||||
const indexUrl = joinUrl(baseUrl, '.account/');
|
||||
let res = await fetch(indexUrl);
|
||||
expect(res.status).toBe(200);
|
||||
const { controls } = await res.json();
|
||||
|
||||
res = await fetch(controls.password.login, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ email: 'test@example.com', password: 'password' }),
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
res = await fetch(controls.password.login, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ email: 'test2@example.com', password: 'password2' }),
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
});
|
||||
|
||||
it('still gives control access to pod owners.', async(): Promise<void> => {
|
||||
// Init
|
||||
const state = new IdentityTestState(baseUrl, 'http://mockedredirect/', baseUrl);
|
||||
let url = await state.initSession();
|
||||
expect(url.startsWith(baseUrl)).toBeTruthy();
|
||||
url = await state.handleRedirect(url);
|
||||
|
||||
// Log in
|
||||
let res = await state.fetchIdp(url);
|
||||
expect(res.status).toBe(200);
|
||||
const { controls } = await res.json();
|
||||
res = await state.fetchIdp(controls.password.login,
|
||||
'POST',
|
||||
JSON.stringify({ email: 'test@example.com', password: 'password' }));
|
||||
await state.handleLocationRedirect(res);
|
||||
|
||||
res = await state.fetchIdp(controls.oidc.webId);
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
// Pick WebID
|
||||
const webId = joinUrl(baseUrl, 'test/profile/card#me');
|
||||
res = await state.fetchIdp(controls.oidc.webId, 'POST', { webId, remember: true });
|
||||
await state.handleLocationRedirect(res);
|
||||
|
||||
// Consent
|
||||
res = await state.fetchIdp(controls.oidc.consent, 'POST');
|
||||
|
||||
// Redirect back to the client and verify login success
|
||||
await state.handleIncomingRedirect(res, webId);
|
||||
|
||||
// GET the root ACL (which is initialized as an empty file with the given comment)
|
||||
url = joinUrl(baseUrl, 'test/.acl');
|
||||
res = await state.session.fetch(url);
|
||||
expect(res.status).toBe(200);
|
||||
await expect(res.text()).resolves.toBe('# Test comment for integration test\n');
|
||||
|
||||
// Log out of session again
|
||||
await state.session.logout();
|
||||
});
|
||||
|
||||
it('still supports the existing client credentials.', async(): Promise<void> => {
|
||||
// These are the values stored in the original assets
|
||||
const id = 'token_fd13b73d-2527-4280-82af-278e5b8fe607';
|
||||
// eslint-disable-next-line max-len
|
||||
const secret = 'a809d7ce5daf0e9acd457c91d712ff05038e4a87192e27191c837602bd4b370c633282864c133650b0e9a35b59018b064157532642f628affb2f79e81999e898';
|
||||
const tokenUrl = joinUrl(baseUrl, '.oidc/token');
|
||||
const dpopHeader = await createDpopHeader(tokenUrl, 'POST', await generateDpopKeyPair());
|
||||
const authString = `${encodeURIComponent(id)}:${encodeURIComponent(secret)}`;
|
||||
const res = await fetch(tokenUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
authorization: `Basic ${Buffer.from(authString).toString('base64')}`,
|
||||
'content-type': APPLICATION_X_WWW_FORM_URLENCODED,
|
||||
dpop: dpopHeader,
|
||||
},
|
||||
body: 'grant_type=client_credentials&scope=webid',
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
const { access_token: accessToken } = await res.json();
|
||||
expect(typeof accessToken).toBe('string');
|
||||
});
|
||||
});
|
||||
51
test/integration/config/file-pod.json
Normal file
51
test/integration/config/file-pod.json
Normal file
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
|
||||
"import": [
|
||||
"css:config/app/main/default.json",
|
||||
"css:config/app/init/static-root.json",
|
||||
"css:config/http/handler/default.json",
|
||||
"css:config/http/middleware/default.json",
|
||||
"css:config/http/notifications/all.json",
|
||||
"css:config/http/server-factory/http.json",
|
||||
"css:config/http/static/default.json",
|
||||
"css:config/identity/access/public.json",
|
||||
"css:config/identity/email/example.json",
|
||||
"css:config/identity/handler/default.json",
|
||||
"css:config/identity/interaction/default.json",
|
||||
"css:config/identity/ownership/token.json",
|
||||
"css:config/identity/pod/static.json",
|
||||
"css:config/ldp/authentication/dpop-bearer.json",
|
||||
"css:config/ldp/authorization/webacl.json",
|
||||
"css:config/ldp/handler/default.json",
|
||||
"css:config/ldp/metadata-parser/default.json",
|
||||
"css:config/ldp/metadata-writer/default.json",
|
||||
"css:config/ldp/modes/default.json",
|
||||
"css:config/storage/backend/file.json",
|
||||
"css:config/storage/key-value/resource-store.json",
|
||||
"css:config/storage/location/pod.json",
|
||||
"css:config/storage/middleware/default.json",
|
||||
"css:config/util/auxiliary/acl.json",
|
||||
"css:config/util/identifiers/suffix.json",
|
||||
"css:config/util/index/default.json",
|
||||
"css:config/util/logging/winston.json",
|
||||
"css:config/util/representation-conversion/default.json",
|
||||
"css:config/util/resource-locker/file.json",
|
||||
"css:config/util/variables/default.json"
|
||||
],
|
||||
"@graph": [
|
||||
{
|
||||
"@id": "urn:solid-server:test:Instances",
|
||||
"@type": "RecordObject",
|
||||
"record": [
|
||||
{
|
||||
"RecordObject:_record_key": "app",
|
||||
"RecordObject:_record_value": { "@id": "urn:solid-server:default:App" }
|
||||
},
|
||||
{
|
||||
"RecordObject:_record_key": "store",
|
||||
"RecordObject:_record_value": { "@id": "urn:solid-server:default:ResourceStore_Backend" }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
69
test/unit/init/migration/SingleContainerJsonStorage.test.ts
Normal file
69
test/unit/init/migration/SingleContainerJsonStorage.test.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { BasicRepresentation } from '../../../../src/http/representation/BasicRepresentation';
|
||||
import type { Representation } from '../../../../src/http/representation/Representation';
|
||||
import { RepresentationMetadata } from '../../../../src/http/representation/RepresentationMetadata';
|
||||
import { SingleContainerJsonStorage } from '../../../../src/init/migration/SingleContainerJsonStorage';
|
||||
import { ResourceStore } from '../../../../src/storage/ResourceStore';
|
||||
import { NotFoundHttpError } from '../../../../src/util/errors/NotFoundHttpError';
|
||||
import { isContainerIdentifier } from '../../../../src/util/PathUtil';
|
||||
import { LDP } from '../../../../src/util/Vocabularies';
|
||||
|
||||
describe('A SingleContainerJsonStorage', (): void => {
|
||||
const baseUrl = 'http://example.com/';
|
||||
const container = '.internal/accounts/';
|
||||
let store: jest.Mocked<ResourceStore>;
|
||||
let storage: SingleContainerJsonStorage<any>;
|
||||
|
||||
beforeEach(async(): Promise<void> => {
|
||||
store = {
|
||||
getRepresentation: jest.fn(async(id): Promise<Representation> => {
|
||||
if (isContainerIdentifier(id)) {
|
||||
const metadata = new RepresentationMetadata(id);
|
||||
metadata.add(LDP.terms.contains, 'http://example.com/.internal/accounts/foo');
|
||||
metadata.add(LDP.terms.contains, 'http://example.com/.internal/accounts/bar/');
|
||||
metadata.add(LDP.terms.contains, 'http://example.com/.internal/accounts/baz');
|
||||
metadata.add(LDP.terms.contains, 'http://example.com/.internal/accounts/unknown');
|
||||
return new BasicRepresentation('', metadata);
|
||||
}
|
||||
if (id.path.endsWith('unknown')) {
|
||||
throw new NotFoundHttpError();
|
||||
}
|
||||
return new BasicRepresentation(`{ "id": "${id.path}" }`, 'text/plain');
|
||||
}),
|
||||
} satisfies Partial<ResourceStore> as any;
|
||||
|
||||
storage = new SingleContainerJsonStorage(store, baseUrl, container);
|
||||
});
|
||||
|
||||
it('only iterates over the documents in the base container.', async(): Promise<void> => {
|
||||
const entries = [];
|
||||
for await (const entry of storage.entries()) {
|
||||
entries.push(entry);
|
||||
}
|
||||
expect(entries).toEqual([
|
||||
[ '/foo', { id: 'http://example.com/.internal/accounts/foo' }],
|
||||
[ '/baz', { id: 'http://example.com/.internal/accounts/baz' }],
|
||||
]);
|
||||
expect(store.getRepresentation).toHaveBeenCalledTimes(4);
|
||||
expect(store.getRepresentation).toHaveBeenNthCalledWith(1,
|
||||
{ path: 'http://example.com/.internal/accounts/' },
|
||||
{});
|
||||
expect(store.getRepresentation).toHaveBeenNthCalledWith(2,
|
||||
{ path: 'http://example.com/.internal/accounts/foo' },
|
||||
{ type: { 'application/json': 1 }});
|
||||
expect(store.getRepresentation).toHaveBeenNthCalledWith(3,
|
||||
{ path: 'http://example.com/.internal/accounts/baz' },
|
||||
{ type: { 'application/json': 1 }});
|
||||
expect(store.getRepresentation).toHaveBeenNthCalledWith(4,
|
||||
{ path: 'http://example.com/.internal/accounts/unknown' },
|
||||
{ type: { 'application/json': 1 }});
|
||||
});
|
||||
|
||||
it('does nothing if the container does not exist.', async(): Promise<void> => {
|
||||
store.getRepresentation.mockRejectedValueOnce(new NotFoundHttpError());
|
||||
const entries = [];
|
||||
for await (const entry of storage.entries()) {
|
||||
entries.push(entry);
|
||||
}
|
||||
expect(entries).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
223
test/unit/init/migration/V6MigrationInitializer.test.ts
Normal file
223
test/unit/init/migration/V6MigrationInitializer.test.ts
Normal file
@@ -0,0 +1,223 @@
|
||||
import { ACCOUNT_TYPE, AccountLoginStorage } 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 { 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('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<string, Settings>;
|
||||
let accounts: Record<string, Account>;
|
||||
let clientCredentials: Record<string, ClientCredentials>;
|
||||
const versionKey = 'version';
|
||||
let versionStorage: jest.Mocked<KeyValueStorage<string, string>>;
|
||||
let accountStorage: jest.Mocked<KeyValueStorage<string, Account | Settings>>;
|
||||
let clientCredentialsStorage: jest.Mocked<KeyValueStorage<string, ClientCredentials>>;
|
||||
let forgotPasswordStorage: jest.Mocked<KeyValueStorage<string, unknown>>;
|
||||
let newStorage: jest.Mocked<AccountLoginStorage<any>>;
|
||||
let initializer: V6MigrationInitializer;
|
||||
|
||||
beforeEach(async(): Promise<void> => {
|
||||
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!' },
|
||||
};
|
||||
|
||||
versionStorage = {
|
||||
get: jest.fn().mockResolvedValue('6.0.0'),
|
||||
} satisfies Partial<KeyValueStorage<string, string>> 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<KeyValueStorage<string, any>> as any;
|
||||
|
||||
clientCredentialsStorage = {
|
||||
delete: jest.fn(),
|
||||
entries: jest.fn(async function* (): AsyncIterableIterator<[string, any]> {
|
||||
yield* Object.entries(clientCredentials);
|
||||
}),
|
||||
} satisfies Partial<KeyValueStorage<string, any>> as any;
|
||||
|
||||
forgotPasswordStorage = {
|
||||
delete: jest.fn(),
|
||||
entries: jest.fn(async function* (): AsyncIterableIterator<[string, any]> {
|
||||
yield [ 'forgot', {}];
|
||||
}),
|
||||
} satisfies Partial<KeyValueStorage<string, any>> as any;
|
||||
|
||||
newStorage = {
|
||||
create: jest.fn((type): any => ({ id: `${type}-id` })),
|
||||
} satisfies Partial<AccountLoginStorage<any>> as any;
|
||||
|
||||
initializer = new V6MigrationInitializer({
|
||||
versionKey,
|
||||
versionStorage,
|
||||
accountStorage,
|
||||
clientCredentialsStorage,
|
||||
forgotPasswordStorage,
|
||||
newStorage,
|
||||
skipConfirmation: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('migrates the data.', async(): Promise<void> => {
|
||||
await expect(initializer.handle()).resolves.toBeUndefined();
|
||||
|
||||
expect(versionStorage.get).toHaveBeenCalledTimes(1);
|
||||
expect(versionStorage.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(newStorage.create).toHaveBeenCalledTimes(11);
|
||||
expect(newStorage.create).toHaveBeenCalledWith(ACCOUNT_TYPE, {});
|
||||
expect(newStorage.create).toHaveBeenCalledWith(PASSWORD_STORAGE_TYPE,
|
||||
{ email: 'email@example.com', password: '123', verified: true, accountId: 'account-id' });
|
||||
expect(newStorage.create).toHaveBeenCalledWith(PASSWORD_STORAGE_TYPE,
|
||||
{ email: 'email2@example.com', password: '1234', verified: true, accountId: 'account-id' });
|
||||
expect(newStorage.create).toHaveBeenCalledWith(WEBID_STORAGE_TYPE, { webId, accountId: 'account-id' });
|
||||
expect(newStorage.create).toHaveBeenCalledWith(WEBID_STORAGE_TYPE, { webId: webId2, accountId: 'account-id' });
|
||||
expect(newStorage.create).toHaveBeenCalledWith(POD_STORAGE_TYPE, { baseUrl: 'http://example.com/test/', accountId: 'account-id' });
|
||||
expect(newStorage.create).toHaveBeenCalledWith(POD_STORAGE_TYPE, { baseUrl: 'http://example.com/test2/', accountId: 'account-id' });
|
||||
expect(newStorage.create).toHaveBeenCalledWith(OWNER_STORAGE_TYPE, { webId, podId: 'pod-id', visible: false });
|
||||
expect(newStorage.create).toHaveBeenCalledWith(OWNER_STORAGE_TYPE,
|
||||
{ webId: webId2, podId: 'pod-id', visible: false });
|
||||
expect(newStorage.create).toHaveBeenCalledWith(CLIENT_CREDENTIALS_STORAGE_TYPE,
|
||||
{ label: 'token', secret: 'secret!', webId, accountId: 'account-id' });
|
||||
});
|
||||
|
||||
it('does nothing if the server has no stored version number.', async(): Promise<void> => {
|
||||
versionStorage.get.mockResolvedValueOnce(undefined);
|
||||
await expect(initializer.handle()).resolves.toBeUndefined();
|
||||
expect(accountStorage.get).toHaveBeenCalledTimes(0);
|
||||
expect(newStorage.create).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('does nothing if stored version is more than 6.', async(): Promise<void> => {
|
||||
versionStorage.get.mockResolvedValueOnce('7.0.0');
|
||||
await expect(initializer.handle()).resolves.toBeUndefined();
|
||||
expect(accountStorage.get).toHaveBeenCalledTimes(0);
|
||||
expect(newStorage.create).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('ignores accounts and credentials for which it cannot find the settings.', async(): Promise<void> => {
|
||||
delete settings[webId];
|
||||
await expect(initializer.handle()).resolves.toBeUndefined();
|
||||
|
||||
expect(versionStorage.get).toHaveBeenCalledTimes(1);
|
||||
expect(versionStorage.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(newStorage.create).toHaveBeenCalledTimes(5);
|
||||
expect(newStorage.create).toHaveBeenCalledWith(ACCOUNT_TYPE, {});
|
||||
expect(newStorage.create).toHaveBeenCalledWith(PASSWORD_STORAGE_TYPE,
|
||||
{ email: 'email2@example.com', password: '1234', verified: true, accountId: 'account-id' });
|
||||
expect(newStorage.create).toHaveBeenCalledWith(WEBID_STORAGE_TYPE, { webId: webId2, accountId: 'account-id' });
|
||||
expect(newStorage.create).toHaveBeenCalledWith(POD_STORAGE_TYPE, { baseUrl: 'http://example.com/test2/', accountId: 'account-id' });
|
||||
expect(newStorage.create).toHaveBeenCalledWith(OWNER_STORAGE_TYPE,
|
||||
{ webId: webId2, podId: 'pod-id', visible: false });
|
||||
});
|
||||
|
||||
describe('with prompts enabled', (): void => {
|
||||
beforeEach(async(): Promise<void> => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
initializer = new V6MigrationInitializer({
|
||||
versionKey,
|
||||
versionStorage,
|
||||
accountStorage,
|
||||
clientCredentialsStorage,
|
||||
forgotPasswordStorage,
|
||||
newStorage,
|
||||
skipConfirmation: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('shows a prompt before migrating the data.', async(): Promise<void> => {
|
||||
await expect(initializer.handle()).resolves.toBeUndefined();
|
||||
|
||||
expect(questionMock).toHaveBeenCalledTimes(1);
|
||||
expect(questionMock.mock.invocationCallOrder[0]).toBeLessThan(newStorage.create.mock.invocationCallOrder[0]);
|
||||
|
||||
expect(newStorage.create).toHaveBeenCalledTimes(11);
|
||||
});
|
||||
|
||||
it('throws an error to stop the server if no positive answer is received.', async(): Promise<void> => {
|
||||
questionMock.mockImplementation((input, callback): void => callback('n'));
|
||||
await expect(initializer.handle()).rejects.toThrow('Stopping server as migration was cancelled.');
|
||||
expect(newStorage.create).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('does not show the prompt if there are no accounts.', async(): Promise<void> => {
|
||||
settings = {};
|
||||
accounts = {};
|
||||
await expect(initializer.handle()).resolves.toBeUndefined();
|
||||
expect(questionMock).toHaveBeenCalledTimes(0);
|
||||
expect(accountStorage.get).toHaveBeenCalledTimes(0);
|
||||
expect(newStorage.create).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -38,12 +38,26 @@ const portNames = [
|
||||
'BaseServerFactory',
|
||||
] as const;
|
||||
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
// These are ports that are not allowed to change for various reasons
|
||||
const fixedPorts = {
|
||||
V6Migration: 6999,
|
||||
} as const;
|
||||
/* eslint-enable @typescript-eslint/naming-convention */
|
||||
|
||||
const socketNames = [
|
||||
// Unit
|
||||
'BaseHttpServerFactory',
|
||||
];
|
||||
|
||||
export function getPort(name: typeof portNames[number]): number {
|
||||
function isFixedPortName(name: string): name is keyof typeof fixedPorts {
|
||||
return Boolean(fixedPorts[name as keyof typeof fixedPorts]);
|
||||
}
|
||||
|
||||
export function getPort(name: typeof portNames[number] | keyof typeof fixedPorts): number {
|
||||
if (isFixedPortName(name)) {
|
||||
return fixedPorts[name];
|
||||
}
|
||||
const idx = portNames.indexOf(name);
|
||||
// Just in case something doesn't listen to the typings
|
||||
if (idx < 0) {
|
||||
|
||||
Reference in New Issue
Block a user