mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
feat: Create PodCreator class to contain most pod creation logic
This commit is contained in:
parent
3bfd9e3298
commit
42a1ca7b64
@ -5,9 +5,9 @@
|
||||
"comment": "Initializer that instantiates all the seeded accounts and pods.",
|
||||
"@id": "urn:solid-server:default:SeededAccountInitializer",
|
||||
"@type": "SeededAccountInitializer",
|
||||
"accountHandler": { "@id": "urn:solid-server:default:CreateAccountHandler" },
|
||||
"passwordHandler": { "@id": "urn:solid-server:default:CreatePasswordHandler" },
|
||||
"podHandler": { "@id": "urn:solid-server:default:CreatePodHandler" },
|
||||
"accountStore": { "@id": "urn:solid-server:default:AccountStore" },
|
||||
"passwordStore": { "@id": "urn:solid-server:default:PasswordStore" },
|
||||
"podCreator": { "@id": "urn:solid-server:default:PodCreator" },
|
||||
"configFilePath": { "@id": "urn:solid-server:default:variable:seedConfig" }
|
||||
}
|
||||
]
|
||||
|
@ -16,14 +16,19 @@
|
||||
"source": {
|
||||
"@id": "urn:solid-server:default:CreatePodHandler",
|
||||
"@type": "CreatePodHandler",
|
||||
"baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" },
|
||||
"identifierGenerator": { "@id": "urn:solid-server:default:IdentifierGenerator" },
|
||||
"relativeWebIdPath": "/profile/card#me",
|
||||
"webIdStore": { "@id": "urn:solid-server:default:WebIdStore" },
|
||||
"podStore": { "@id": "urn:solid-server:default:PodStore" },
|
||||
"webIdLinkRoute": { "@id": "urn:solid-server:default:AccountWebIdLinkRoute" },
|
||||
"podIdRoute": { "@id": "urn:solid-server:default:AccountPodIdRoute" },
|
||||
"allowRoot": false
|
||||
"allowRoot": false,
|
||||
"podCreator": {
|
||||
"@id": "urn:solid-server:default:PodCreator",
|
||||
"@type": "BasePodCreator",
|
||||
"baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" },
|
||||
"identifierGenerator": { "@id": "urn:solid-server:default:IdentifierGenerator" },
|
||||
"relativeWebIdPath": "/profile/card#me",
|
||||
"webIdStore": { "@id": "urn:solid-server:default:WebIdStore" },
|
||||
"podStore": { "@id": "urn:solid-server:default:PodStore" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,20 +1,15 @@
|
||||
import type { StringSchema } from 'yup';
|
||||
import { object, string } from 'yup';
|
||||
import type { ResourceIdentifier } from '../../../http/representation/ResourceIdentifier';
|
||||
import { getLoggerFor } from '../../../logging/LogUtil';
|
||||
import type { IdentifierGenerator } from '../../../pods/generate/IdentifierGenerator';
|
||||
import type { PodSettings } from '../../../pods/settings/PodSettings';
|
||||
import { BadRequestHttpError } from '../../../util/errors/BadRequestHttpError';
|
||||
import { joinUrl } from '../../../util/PathUtil';
|
||||
import { assertAccountId } from '../account/util/AccountUtil';
|
||||
import type { JsonRepresentation } from '../InteractionUtil';
|
||||
import { JsonInteractionHandler } from '../JsonInteractionHandler';
|
||||
import type { JsonInteractionHandlerInput } from '../JsonInteractionHandler';
|
||||
import type { JsonView } from '../JsonView';
|
||||
import type { WebIdStore } from '../webid/util/WebIdStore';
|
||||
import type { WebIdLinkRoute } from '../webid/WebIdLinkRoute';
|
||||
import { parseSchema, URL_SCHEMA, validateWithError } from '../YupUtil';
|
||||
import type { PodIdRoute } from './PodIdRoute';
|
||||
import type { PodCreator } from './util/PodCreator';
|
||||
import type { PodStore } from './util/PodStore';
|
||||
|
||||
const inSchema = object({
|
||||
@ -24,43 +19,6 @@ const inSchema = object({
|
||||
}).optional(),
|
||||
});
|
||||
|
||||
export interface CreatePodHandlerArgs {
|
||||
/**
|
||||
* Base URL of the server.
|
||||
* Used to potentially set the `solid:oidcIssuer` triple
|
||||
* and/or the pod URL if it is a root pod.
|
||||
*/
|
||||
baseUrl: string;
|
||||
/**
|
||||
* Generates the base URL of the pod based on the input `name`.
|
||||
*/
|
||||
identifierGenerator: IdentifierGenerator;
|
||||
/**
|
||||
* The path of where the WebID will be generated by the template, relative to the pod URL.
|
||||
*/
|
||||
relativeWebIdPath: string;
|
||||
/**
|
||||
* WebID data store.
|
||||
*/
|
||||
webIdStore: WebIdStore;
|
||||
/**
|
||||
* Pod data store.
|
||||
*/
|
||||
podStore: PodStore;
|
||||
/**
|
||||
* Route to generate WebID link resource URLs.
|
||||
*/
|
||||
webIdLinkRoute: WebIdLinkRoute;
|
||||
/**
|
||||
* Route to generate Pod ID resource URLs
|
||||
*/
|
||||
podIdRoute: PodIdRoute;
|
||||
/**
|
||||
* Whether it is allowed to generate a pod in the root of the server.
|
||||
*/
|
||||
allowRoot: boolean;
|
||||
}
|
||||
|
||||
type OutType = {
|
||||
pod: string;
|
||||
podResource: string;
|
||||
@ -70,34 +28,29 @@ type OutType = {
|
||||
|
||||
/**
|
||||
* Handles the creation of pods.
|
||||
* Will call the stored {@link PodStore} with the settings found in the input JSON.
|
||||
* Will call the stored {@link PodCreator} with the settings found in the input JSON.
|
||||
*/
|
||||
export class CreatePodHandler extends JsonInteractionHandler<OutType> implements JsonView {
|
||||
private readonly logger = getLoggerFor(this);
|
||||
|
||||
private readonly baseUrl: string;
|
||||
private readonly identifierGenerator: IdentifierGenerator;
|
||||
private readonly relativeWebIdPath: string;
|
||||
private readonly webIdStore: WebIdStore;
|
||||
private readonly podStore: PodStore;
|
||||
private readonly podCreator: PodCreator;
|
||||
private readonly webIdLinkRoute: WebIdLinkRoute;
|
||||
private readonly podIdRoute: PodIdRoute;
|
||||
|
||||
private readonly inSchema: typeof inSchema;
|
||||
|
||||
public constructor(args: CreatePodHandlerArgs) {
|
||||
public constructor(podStore: PodStore, podCreator: PodCreator, webIdLinkRoute: WebIdLinkRoute, podIdRoute: PodIdRoute,
|
||||
allowRoot = false) {
|
||||
super();
|
||||
this.baseUrl = args.baseUrl;
|
||||
this.identifierGenerator = args.identifierGenerator;
|
||||
this.relativeWebIdPath = args.relativeWebIdPath;
|
||||
this.webIdStore = args.webIdStore;
|
||||
this.podStore = args.podStore;
|
||||
this.webIdLinkRoute = args.webIdLinkRoute;
|
||||
this.podIdRoute = args.podIdRoute;
|
||||
this.podStore = podStore;
|
||||
this.podCreator = podCreator;
|
||||
this.webIdLinkRoute = webIdLinkRoute;
|
||||
this.podIdRoute = podIdRoute;
|
||||
|
||||
this.inSchema = inSchema.clone();
|
||||
|
||||
if (!args.allowRoot) {
|
||||
if (!allowRoot) {
|
||||
// Casting is necessary to prevent errors
|
||||
this.inSchema.fields.name = (this.inSchema.fields.name as StringSchema).required();
|
||||
}
|
||||
@ -117,60 +70,13 @@ export class CreatePodHandler extends JsonInteractionHandler<OutType> implements
|
||||
const { name, settings } = await validateWithError(inSchema, json);
|
||||
assertAccountId(accountId);
|
||||
|
||||
const baseIdentifier = this.generateBaseIdentifier(name);
|
||||
// Either the input WebID or the one generated in the pod
|
||||
const webId = settings?.webId ?? joinUrl(baseIdentifier.path, this.relativeWebIdPath);
|
||||
const linkWebId = !settings?.webId;
|
||||
const result = await this.podCreator.handleSafe({
|
||||
accountId, webId: settings?.webId, name, settings,
|
||||
});
|
||||
|
||||
const podSettings: PodSettings = {
|
||||
...settings,
|
||||
base: baseIdentifier,
|
||||
webId,
|
||||
};
|
||||
const webIdResource = result.webIdLink && this.webIdLinkRoute.getPath({ accountId, webIdLink: result.webIdLink });
|
||||
const podResource = this.podIdRoute.getPath({ accountId, podId: result.podId });
|
||||
|
||||
// Link the WebID to the account immediately if no WebID was provided.
|
||||
// This WebID will be necessary anyway to access the data in the pod,
|
||||
// so might as well link it to the account immediately.
|
||||
let webIdLink: string | undefined;
|
||||
let webIdResource: string | undefined;
|
||||
if (linkWebId) {
|
||||
// It is important that this check happens here.
|
||||
// Otherwise, if the account already has this WebID link,
|
||||
// this link would be deleted if pod creation fails,
|
||||
// since we clean up the WebID link again afterwards.
|
||||
// Current implementation of the {@link WebIdStore} also has this check but better safe than sorry.
|
||||
if (await this.webIdStore.isLinked(webId, accountId)) {
|
||||
this.logger.warn('Trying to create pod which would generate a WebID that is already linked to this account');
|
||||
throw new BadRequestHttpError(`${webId} is already registered to this account.`);
|
||||
}
|
||||
|
||||
webIdLink = await this.webIdStore.create(webId, accountId);
|
||||
webIdResource = this.webIdLinkRoute.getPath({ accountId, webIdLink });
|
||||
// Need to have the necessary `solid:oidcIssuer` triple if the WebID is linked
|
||||
podSettings.oidcIssuer = this.baseUrl;
|
||||
}
|
||||
|
||||
// Create the pod
|
||||
let podId: string;
|
||||
try {
|
||||
podId = await this.podStore.create(accountId, podSettings, !name);
|
||||
} catch (error: unknown) {
|
||||
// Undo the WebID linking if pod creation fails
|
||||
if (webIdLink) {
|
||||
await this.webIdStore.delete(webIdLink);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
const podResource = this.podIdRoute.getPath({ accountId, podId });
|
||||
return { json: { pod: baseIdentifier.path, webId, podResource, webIdResource }};
|
||||
}
|
||||
|
||||
private generateBaseIdentifier(name?: string): ResourceIdentifier {
|
||||
if (name) {
|
||||
return this.identifierGenerator.generate(name);
|
||||
}
|
||||
return { path: this.baseUrl };
|
||||
return { json: { pod: result.podUrl, webId: result.webId, podResource, webIdResource }};
|
||||
}
|
||||
}
|
||||
|
132
src/identity/interaction/pod/util/BasePodCreator.ts
Normal file
132
src/identity/interaction/pod/util/BasePodCreator.ts
Normal file
@ -0,0 +1,132 @@
|
||||
import type { ResourceIdentifier } from '../../../../http/representation/ResourceIdentifier';
|
||||
import { getLoggerFor } from '../../../../logging/LogUtil';
|
||||
import type { IdentifierGenerator } from '../../../../pods/generate/IdentifierGenerator';
|
||||
import type { PodSettings } from '../../../../pods/settings/PodSettings';
|
||||
import { BadRequestHttpError } from '../../../../util/errors/BadRequestHttpError';
|
||||
import { joinUrl } from '../../../../util/PathUtil';
|
||||
import type { WebIdStore } from '../../webid/util/WebIdStore';
|
||||
import { PodCreator } from './PodCreator';
|
||||
import type { PodCreatorInput, PodCreatorOutput } from './PodCreator';
|
||||
import type { PodStore } from './PodStore';
|
||||
|
||||
export interface BasePodCreatorArgs {
|
||||
/**
|
||||
* Base URL of the server.
|
||||
* Used to potentially set the `solid:oidcIssuer` triple
|
||||
* and/or the pod URL if it is a root pod.
|
||||
*/
|
||||
baseUrl: string;
|
||||
/**
|
||||
* Generates the base URL of the pod based on the input `name`.
|
||||
*/
|
||||
identifierGenerator: IdentifierGenerator;
|
||||
/**
|
||||
* The path of where the WebID will be generated by the template, relative to the pod URL.
|
||||
*/
|
||||
relativeWebIdPath: string;
|
||||
/**
|
||||
* WebID data store.
|
||||
*/
|
||||
webIdStore: WebIdStore;
|
||||
/**
|
||||
* Pod data store.
|
||||
*/
|
||||
podStore: PodStore;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the creation of pods.
|
||||
* Will call the stored {@link PodStore} with the provided settings.
|
||||
*/
|
||||
export class BasePodCreator extends PodCreator {
|
||||
protected readonly logger = getLoggerFor(this);
|
||||
|
||||
protected readonly baseUrl: string;
|
||||
protected readonly identifierGenerator: IdentifierGenerator;
|
||||
protected readonly relativeWebIdPath: string;
|
||||
protected readonly webIdStore: WebIdStore;
|
||||
protected readonly podStore: PodStore;
|
||||
|
||||
public constructor(args: BasePodCreatorArgs) {
|
||||
super();
|
||||
this.baseUrl = args.baseUrl;
|
||||
this.identifierGenerator = args.identifierGenerator;
|
||||
this.relativeWebIdPath = args.relativeWebIdPath;
|
||||
this.webIdStore = args.webIdStore;
|
||||
this.podStore = args.podStore;
|
||||
}
|
||||
|
||||
public async handle(input: PodCreatorInput): Promise<PodCreatorOutput> {
|
||||
const baseIdentifier = this.generateBaseIdentifier(input.name);
|
||||
// Either the input WebID or the one generated in the pod
|
||||
const webId = input.webId ?? joinUrl(baseIdentifier.path, this.relativeWebIdPath);
|
||||
|
||||
const podSettings: PodSettings = {
|
||||
...input.settings,
|
||||
base: baseIdentifier,
|
||||
webId,
|
||||
};
|
||||
|
||||
// Link the WebID to the account immediately if no WebID was provided as this is expected behaviour.
|
||||
// We do this first as we can't undo creating the pod if this would fail.
|
||||
// If an external WebID is the owner we do not want to link it to the account automatically
|
||||
const webIdLink = await this.handleWebId(!input.webId, webId, input.accountId, podSettings);
|
||||
|
||||
// Create the pod
|
||||
const podId = await this.createPod(input.accountId, podSettings, !input.name, webIdLink);
|
||||
|
||||
return {
|
||||
podUrl: baseIdentifier.path,
|
||||
webId,
|
||||
podId,
|
||||
webIdLink,
|
||||
};
|
||||
}
|
||||
|
||||
protected generateBaseIdentifier(name?: string): ResourceIdentifier {
|
||||
if (name) {
|
||||
return this.identifierGenerator.generate(name);
|
||||
}
|
||||
return { path: this.baseUrl };
|
||||
}
|
||||
|
||||
/**
|
||||
* Links the WebID to the account if `linkWebId` is true.
|
||||
* Also updates the `oidcIssuer` value in the settings object in that case.
|
||||
*/
|
||||
protected async handleWebId(linkWebId: boolean, webId: string, accountId: string, settings: PodSettings):
|
||||
Promise<string | undefined> {
|
||||
if (linkWebId) {
|
||||
// It is important that this check happens here.
|
||||
// Otherwise, if the account already has this WebID link,
|
||||
// this link would be deleted if pod creation fails,
|
||||
// since we clean up the WebID link again afterwards.
|
||||
// Current implementation of the {@link WebIdStore} also has this check but better safe than sorry.
|
||||
if (await this.webIdStore.isLinked(webId, accountId)) {
|
||||
this.logger.warn('Trying to create pod which would generate a WebID that is already linked to this account');
|
||||
throw new BadRequestHttpError(`${webId} is already registered to this account.`);
|
||||
}
|
||||
// Need to have the necessary `solid:oidcIssuer` triple if the WebID is linked
|
||||
settings.oidcIssuer = this.baseUrl;
|
||||
|
||||
return this.webIdStore.create(webId, accountId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a pod with the given settings.
|
||||
* In case pod creation fails, the given WebID link will be removed, if there is one, before throwing an error.
|
||||
*/
|
||||
protected async createPod(accountId: string, settings: PodSettings, overwrite: boolean, webIdLink?: string):
|
||||
Promise<string> {
|
||||
try {
|
||||
return await this.podStore.create(accountId, settings, overwrite);
|
||||
} catch (error: unknown) {
|
||||
// Undo the WebID linking if pod creation fails
|
||||
if (webIdLink) {
|
||||
await this.webIdStore.delete(webIdLink);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
48
src/identity/interaction/pod/util/PodCreator.ts
Normal file
48
src/identity/interaction/pod/util/PodCreator.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import { AsyncHandler } from '../../../../util/handlers/AsyncHandler';
|
||||
|
||||
export interface PodCreatorInput {
|
||||
/**
|
||||
* The ID of the account to create the pod for.
|
||||
*/
|
||||
accountId: string;
|
||||
/**
|
||||
* The name to use when generating the base URL of the pod.
|
||||
* If undefined, the pod will be created in the root of the server.
|
||||
*/
|
||||
name?: string;
|
||||
/**
|
||||
* The WebID to use for creation of the pod.
|
||||
* This WebID will be used in the templates to, for example, determine who has access.
|
||||
* If none is provided, the WebID generated by the creator will be used,
|
||||
* in which case that WebID will also be linked to the account.
|
||||
*/
|
||||
webId?: string;
|
||||
/**
|
||||
* Additional settings to use when generating a pod.
|
||||
*/
|
||||
settings?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface PodCreatorOutput {
|
||||
/**
|
||||
* The ID of the generated pod.
|
||||
*/
|
||||
podId: string;
|
||||
/**
|
||||
* The URl of the generated pod.
|
||||
*/
|
||||
podUrl: string;
|
||||
/**
|
||||
* The WebID that was used to generate the pod.
|
||||
*/
|
||||
webId: string;
|
||||
/**
|
||||
* The ID of the WebID link if one was generated.
|
||||
*/
|
||||
webIdLink?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles creating a pod and linking the created WebID.
|
||||
*/
|
||||
export abstract class PodCreator extends AsyncHandler<PodCreatorInput, PodCreatorOutput> {}
|
@ -201,8 +201,10 @@ export * from './identity/interaction/password/ResetPasswordHandler';
|
||||
export * from './identity/interaction/password/UpdatePasswordHandler';
|
||||
|
||||
// Identity/Interaction/Pod/Util
|
||||
export * from './identity/interaction/pod/util/BasePodCreator';
|
||||
export * from './identity/interaction/pod/util/BasePodStore';
|
||||
export * from './identity/interaction/pod/util/OwnerMetadataWriter';
|
||||
export * from './identity/interaction/pod/util/PodCreator';
|
||||
export * from './identity/interaction/pod/util/PodStore';
|
||||
|
||||
// Identity/Interaction/Pod
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { readJson } from 'fs-extra';
|
||||
import { array, object, string } from 'yup';
|
||||
import { RepresentationMetadata } from '../http/representation/RepresentationMetadata';
|
||||
import type { JsonInteractionHandler } from '../identity/interaction/JsonInteractionHandler';
|
||||
import type { ResolveLoginHandler } from '../identity/interaction/login/ResolveLoginHandler';
|
||||
import type { AccountStore } from '../identity/interaction/account/util/AccountStore';
|
||||
import type { PasswordStore } from '../identity/interaction/password/util/PasswordStore';
|
||||
import type { PodCreator } from '../identity/interaction/pod/util/PodCreator';
|
||||
import { URL_SCHEMA } from '../identity/interaction/YupUtil';
|
||||
import { getLoggerFor } from '../logging/LogUtil';
|
||||
import { createErrorMessage } from '../util/errors/ErrorUtil';
|
||||
@ -24,15 +24,15 @@ export interface SeededAccountInitializerArgs {
|
||||
/**
|
||||
* Creates the accounts.
|
||||
*/
|
||||
accountHandler: ResolveLoginHandler;
|
||||
accountStore: AccountStore;
|
||||
/**
|
||||
* Adds the login methods.
|
||||
*/
|
||||
passwordHandler: JsonInteractionHandler;
|
||||
passwordStore: PasswordStore;
|
||||
/**
|
||||
* Creates the pods.
|
||||
*/
|
||||
podHandler: JsonInteractionHandler;
|
||||
podCreator: PodCreator;
|
||||
/**
|
||||
* File path of the JSON describing the accounts to seed.
|
||||
*/
|
||||
@ -47,16 +47,16 @@ export interface SeededAccountInitializerArgs {
|
||||
export class SeededAccountInitializer extends Initializer {
|
||||
protected readonly logger = getLoggerFor(this);
|
||||
|
||||
private readonly accountHandler: ResolveLoginHandler;
|
||||
private readonly passwordHandler: JsonInteractionHandler;
|
||||
private readonly podHandler: JsonInteractionHandler;
|
||||
private readonly accountStore: AccountStore;
|
||||
private readonly passwordStore: PasswordStore;
|
||||
private readonly podCreator: PodCreator;
|
||||
private readonly configFilePath?: string;
|
||||
|
||||
public constructor(args: SeededAccountInitializerArgs) {
|
||||
super();
|
||||
this.accountHandler = args.accountHandler;
|
||||
this.passwordHandler = args.passwordHandler;
|
||||
this.podHandler = args.podHandler;
|
||||
this.accountStore = args.accountStore;
|
||||
this.passwordStore = args.passwordStore;
|
||||
this.podCreator = args.podCreator;
|
||||
this.configFilePath = args.configFilePath;
|
||||
}
|
||||
|
||||
@ -76,30 +76,25 @@ export class SeededAccountInitializer extends Initializer {
|
||||
throw new Error(msg);
|
||||
}
|
||||
|
||||
// Dummy data for requests to all the handlers
|
||||
const method = 'POST';
|
||||
const target = { path: '' };
|
||||
const metadata = new RepresentationMetadata();
|
||||
|
||||
let accounts = 0;
|
||||
let pods = 0;
|
||||
for await (const input of configuration) {
|
||||
let accountCount = 0;
|
||||
let podCount = 0;
|
||||
for await (const { email, password, pods } of configuration) {
|
||||
try {
|
||||
this.logger.info(`Creating account for ${input.email}`);
|
||||
const accountResult = await this.accountHandler.login({ method, target, metadata, json: {}});
|
||||
const { accountId } = accountResult.json;
|
||||
await this.passwordHandler.handleSafe({ method, target, metadata, accountId, json: input });
|
||||
accounts += 1;
|
||||
this.logger.info(`Creating account for ${email}`);
|
||||
const accountId = await this.accountStore.create();
|
||||
const id = await this.passwordStore.create(email, accountId, password);
|
||||
await this.passwordStore.confirmVerification(id);
|
||||
accountCount += 1;
|
||||
|
||||
for (const pod of input.pods ?? []) {
|
||||
this.logger.info(`Creating pod with name ${pod.name}`);
|
||||
await this.podHandler.handleSafe({ method, target, metadata, accountId, json: pod });
|
||||
pods += 1;
|
||||
for (const { name, settings } of pods ?? []) {
|
||||
this.logger.info(`Creating pod with name ${name}`);
|
||||
await this.podCreator.handleSafe({ accountId, name, webId: settings?.webId, settings });
|
||||
podCount += 1;
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
this.logger.warn(`Error while initializing seeded account: ${createErrorMessage(error)}`);
|
||||
}
|
||||
}
|
||||
this.logger.info(`Initialized ${accounts} accounts and ${pods} pods.`);
|
||||
this.logger.info(`Initialized ${accountCount} accounts and ${podCount} pods.`);
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,8 @@
|
||||
import { CreatePodHandler } from '../../../../../src/identity/interaction/pod/CreatePodHandler';
|
||||
import type { PodIdRoute } from '../../../../../src/identity/interaction/pod/PodIdRoute';
|
||||
import { PodCreator } from '../../../../../src/identity/interaction/pod/util/PodCreator';
|
||||
import { PodStore } from '../../../../../src/identity/interaction/pod/util/PodStore';
|
||||
import { WebIdStore } from '../../../../../src/identity/interaction/webid/util/WebIdStore';
|
||||
import type { WebIdLinkRoute } from '../../../../../src/identity/interaction/webid/WebIdLinkRoute';
|
||||
import type { IdentifierGenerator } from '../../../../../src/pods/generate/IdentifierGenerator';
|
||||
|
||||
describe('A CreatePodHandler', (): void => {
|
||||
const name = 'name';
|
||||
@ -12,17 +11,13 @@ describe('A CreatePodHandler', (): void => {
|
||||
const podId = 'podId';
|
||||
const webIdLink = 'webIdLink';
|
||||
let json: unknown;
|
||||
const baseUrl = 'http://example.com/';
|
||||
const relativeWebIdPath = '/profile/card#me';
|
||||
const podUrl = 'http://example.com/name/';
|
||||
const generatedWebId = 'http://example.com/name/profile/card#me';
|
||||
const webIdResource = 'http://example.com/.account/webID';
|
||||
const podResource = 'http://example.com/.account/pod';
|
||||
let identifierGenerator: jest.Mocked<IdentifierGenerator>;
|
||||
let webIdStore: jest.Mocked<WebIdStore>;
|
||||
let webIdLinkRoute: jest.Mocked<WebIdLinkRoute>;
|
||||
let podIdRoute: jest.Mocked<PodIdRoute>;
|
||||
let podStore: jest.Mocked<PodStore>;
|
||||
let podCreator: jest.Mocked<PodCreator>;
|
||||
let handler: CreatePodHandler;
|
||||
|
||||
beforeEach(async(): Promise<void> => {
|
||||
@ -30,16 +25,9 @@ describe('A CreatePodHandler', (): void => {
|
||||
name,
|
||||
};
|
||||
|
||||
identifierGenerator = {
|
||||
generate: jest.fn().mockReturnValue({ path: podUrl }),
|
||||
extractPod: jest.fn(),
|
||||
};
|
||||
|
||||
webIdStore = {
|
||||
isLinked: jest.fn().mockResolvedValue(false),
|
||||
create: jest.fn().mockResolvedValue(webIdLink),
|
||||
delete: jest.fn(),
|
||||
} satisfies Partial<WebIdStore> as any;
|
||||
podCreator = {
|
||||
handleSafe: jest.fn().mockResolvedValue({ podUrl, podId, webId, webIdLink }),
|
||||
} satisfies Partial<PodCreator> as any;
|
||||
|
||||
podStore = {
|
||||
create: jest.fn().mockResolvedValue(podId),
|
||||
@ -56,16 +44,7 @@ describe('A CreatePodHandler', (): void => {
|
||||
matchPath: jest.fn(),
|
||||
};
|
||||
|
||||
handler = new CreatePodHandler({
|
||||
webIdStore,
|
||||
podStore,
|
||||
baseUrl,
|
||||
relativeWebIdPath,
|
||||
identifierGenerator,
|
||||
webIdLinkRoute,
|
||||
podIdRoute,
|
||||
allowRoot: false,
|
||||
});
|
||||
handler = new CreatePodHandler(podStore, podCreator, webIdLinkRoute, podIdRoute);
|
||||
});
|
||||
|
||||
it('returns the required input fields and known pods.', async(): Promise<void> => {
|
||||
@ -91,75 +70,25 @@ describe('A CreatePodHandler', (): void => {
|
||||
|
||||
it('generates a pod and WebID.', async(): Promise<void> => {
|
||||
await expect(handler.handle({ json, accountId } as any)).resolves.toEqual({ json: {
|
||||
pod: podUrl, webId: generatedWebId, podResource, webIdResource,
|
||||
pod: podUrl, webId, podResource, webIdResource,
|
||||
}});
|
||||
expect(webIdStore.isLinked).toHaveBeenCalledTimes(1);
|
||||
expect(webIdStore.isLinked).toHaveBeenLastCalledWith(generatedWebId, accountId);
|
||||
expect(webIdStore.create).toHaveBeenCalledTimes(1);
|
||||
expect(webIdStore.create).toHaveBeenLastCalledWith(generatedWebId, accountId);
|
||||
expect(podStore.create).toHaveBeenCalledTimes(1);
|
||||
expect(podStore.create).toHaveBeenLastCalledWith(accountId, {
|
||||
base: { path: podUrl },
|
||||
webId: generatedWebId,
|
||||
oidcIssuer: baseUrl,
|
||||
}, false);
|
||||
expect(podCreator.handleSafe).toHaveBeenCalledTimes(1);
|
||||
expect(podCreator.handleSafe).toHaveBeenLastCalledWith({ accountId, name, settings: {}});
|
||||
});
|
||||
|
||||
it('can use an external WebID for the pod generation.', async(): Promise<void> => {
|
||||
json = { name, settings: { webId }};
|
||||
|
||||
it('generates a pod with a WebID if there is one.', async(): Promise<void> => {
|
||||
const settings = { webId };
|
||||
json = { name, settings };
|
||||
await expect(handler.handle({ json, accountId } as any)).resolves.toEqual({ json: {
|
||||
pod: podUrl, webId, podResource,
|
||||
pod: podUrl, webId, podResource, webIdResource,
|
||||
}});
|
||||
expect(webIdStore.isLinked).toHaveBeenCalledTimes(0);
|
||||
expect(webIdStore.create).toHaveBeenCalledTimes(0);
|
||||
expect(podStore.create).toHaveBeenCalledTimes(1);
|
||||
expect(podStore.create).toHaveBeenLastCalledWith(accountId, {
|
||||
base: { path: podUrl },
|
||||
webId,
|
||||
}, false);
|
||||
});
|
||||
|
||||
it('errors if the account is already linked to the WebID that would be generated.', async(): Promise<void> => {
|
||||
webIdStore.isLinked.mockResolvedValueOnce(true);
|
||||
await expect(handler.handle({ json, accountId } as any))
|
||||
.rejects.toThrow(`${generatedWebId} is already registered to this account.`);
|
||||
expect(webIdStore.isLinked).toHaveBeenCalledTimes(1);
|
||||
expect(webIdStore.isLinked).toHaveBeenLastCalledWith(generatedWebId, accountId);
|
||||
expect(webIdStore.create).toHaveBeenCalledTimes(0);
|
||||
expect(podStore.create).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('undoes any changes if something goes wrong creating the pod.', async(): Promise<void> => {
|
||||
const error = new Error('bad data');
|
||||
podStore.create.mockRejectedValueOnce(error);
|
||||
|
||||
await expect(handler.handle({ json, accountId } as any)).rejects.toBe(error);
|
||||
|
||||
expect(webIdStore.create).toHaveBeenCalledTimes(1);
|
||||
expect(webIdStore.create).toHaveBeenLastCalledWith(generatedWebId, accountId);
|
||||
expect(podStore.create).toHaveBeenCalledTimes(1);
|
||||
expect(podStore.create).toHaveBeenLastCalledWith(accountId, {
|
||||
base: { path: podUrl },
|
||||
webId: generatedWebId,
|
||||
oidcIssuer: baseUrl,
|
||||
}, false);
|
||||
expect(webIdStore.delete).toHaveBeenCalledTimes(1);
|
||||
expect(webIdStore.delete).toHaveBeenLastCalledWith(webIdLink);
|
||||
expect(podCreator.handleSafe).toHaveBeenCalledTimes(1);
|
||||
expect(podCreator.handleSafe).toHaveBeenLastCalledWith({ accountId, name, webId, settings });
|
||||
});
|
||||
|
||||
describe('allowing root pods', (): void => {
|
||||
beforeEach(async(): Promise<void> => {
|
||||
handler = new CreatePodHandler({
|
||||
webIdStore,
|
||||
podStore,
|
||||
baseUrl,
|
||||
relativeWebIdPath,
|
||||
identifierGenerator,
|
||||
webIdLinkRoute,
|
||||
podIdRoute,
|
||||
allowRoot: true,
|
||||
});
|
||||
handler = new CreatePodHandler(podStore, podCreator, webIdLinkRoute, podIdRoute, true);
|
||||
});
|
||||
|
||||
it('does not require a name.', async(): Promise<void> => {
|
||||
@ -179,19 +108,5 @@ describe('A CreatePodHandler', (): void => {
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('generates a pod and WebID in the root.', async(): Promise<void> => {
|
||||
await expect(handler.handle({ json: {}, accountId } as any)).resolves.toEqual({ json: {
|
||||
pod: baseUrl, webId: `${baseUrl}profile/card#me`, podResource, webIdResource,
|
||||
}});
|
||||
expect(webIdStore.create).toHaveBeenCalledTimes(1);
|
||||
expect(webIdStore.create).toHaveBeenLastCalledWith(`${baseUrl}profile/card#me`, accountId);
|
||||
expect(podStore.create).toHaveBeenCalledTimes(1);
|
||||
expect(podStore.create).toHaveBeenLastCalledWith(accountId, {
|
||||
base: { path: baseUrl },
|
||||
webId: `${baseUrl}profile/card#me`,
|
||||
oidcIssuer: baseUrl,
|
||||
}, true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
111
test/unit/identity/interaction/pod/util/BasePodCreator.test.ts
Normal file
111
test/unit/identity/interaction/pod/util/BasePodCreator.test.ts
Normal file
@ -0,0 +1,111 @@
|
||||
import { BasePodCreator } from '../../../../../../src/identity/interaction/pod/util/BasePodCreator';
|
||||
import { PodStore } from '../../../../../../src/identity/interaction/pod/util/PodStore';
|
||||
import { WebIdStore } from '../../../../../../src/identity/interaction/webid/util/WebIdStore';
|
||||
import type { IdentifierGenerator } from '../../../../../../src/pods/generate/IdentifierGenerator';
|
||||
|
||||
describe('A BasePodCreator', (): void => {
|
||||
const name = 'name';
|
||||
const webId = 'http://example.com/other/webId#me';
|
||||
const accountId = 'accountId';
|
||||
const podId = 'podId';
|
||||
const webIdLink = 'webIdLink';
|
||||
const baseUrl = 'http://example.com/';
|
||||
const relativeWebIdPath = '/profile/card#me';
|
||||
const podUrl = 'http://example.com/name/';
|
||||
const generatedWebId = 'http://example.com/name/profile/card#me';
|
||||
let identifierGenerator: jest.Mocked<IdentifierGenerator>;
|
||||
let webIdStore: jest.Mocked<WebIdStore>;
|
||||
let podStore: jest.Mocked<PodStore>;
|
||||
let creator: BasePodCreator;
|
||||
|
||||
beforeEach(async(): Promise<void> => {
|
||||
identifierGenerator = {
|
||||
generate: jest.fn().mockReturnValue({ path: podUrl }),
|
||||
extractPod: jest.fn(),
|
||||
};
|
||||
|
||||
webIdStore = {
|
||||
isLinked: jest.fn().mockResolvedValue(false),
|
||||
create: jest.fn().mockResolvedValue(webIdLink),
|
||||
delete: jest.fn(),
|
||||
} satisfies Partial<WebIdStore> as any;
|
||||
|
||||
podStore = {
|
||||
create: jest.fn().mockResolvedValue(podId),
|
||||
findPods: jest.fn().mockResolvedValue([{ id: podId, baseUrl: podUrl }]),
|
||||
} satisfies Partial<PodStore> as any;
|
||||
|
||||
creator = new BasePodCreator({
|
||||
webIdStore,
|
||||
podStore,
|
||||
baseUrl,
|
||||
relativeWebIdPath,
|
||||
identifierGenerator,
|
||||
});
|
||||
});
|
||||
|
||||
it('generates a pod and WebID.', async(): Promise<void> => {
|
||||
await expect(creator.handle({ accountId, name })).resolves
|
||||
.toEqual({ podUrl, webId: generatedWebId, podId, webIdLink });
|
||||
expect(webIdStore.isLinked).toHaveBeenCalledTimes(1);
|
||||
expect(webIdStore.isLinked).toHaveBeenLastCalledWith(generatedWebId, accountId);
|
||||
expect(webIdStore.create).toHaveBeenCalledTimes(1);
|
||||
expect(webIdStore.create).toHaveBeenLastCalledWith(generatedWebId, accountId);
|
||||
expect(podStore.create).toHaveBeenCalledTimes(1);
|
||||
expect(podStore.create).toHaveBeenLastCalledWith(accountId, {
|
||||
base: { path: podUrl },
|
||||
webId: generatedWebId,
|
||||
oidcIssuer: baseUrl,
|
||||
}, false);
|
||||
});
|
||||
|
||||
it('can use an external WebID for the pod generation.', async(): Promise<void> => {
|
||||
await expect(creator.handle({ accountId, name, webId })).resolves.toEqual({ podUrl, webId, podId });
|
||||
expect(webIdStore.isLinked).toHaveBeenCalledTimes(0);
|
||||
expect(webIdStore.create).toHaveBeenCalledTimes(0);
|
||||
expect(podStore.create).toHaveBeenCalledTimes(1);
|
||||
expect(podStore.create).toHaveBeenLastCalledWith(accountId, {
|
||||
base: { path: podUrl },
|
||||
webId,
|
||||
}, false);
|
||||
});
|
||||
|
||||
it('create a root pod.', async(): Promise<void> => {
|
||||
await expect(creator.handle({ accountId, webId })).resolves.toEqual({ podUrl: baseUrl, webId, podId });
|
||||
expect(webIdStore.isLinked).toHaveBeenCalledTimes(0);
|
||||
expect(webIdStore.create).toHaveBeenCalledTimes(0);
|
||||
expect(podStore.create).toHaveBeenCalledTimes(1);
|
||||
expect(podStore.create).toHaveBeenLastCalledWith(accountId, {
|
||||
base: { path: baseUrl },
|
||||
webId,
|
||||
}, true);
|
||||
});
|
||||
|
||||
it('errors if the account is already linked to the WebID that would be generated.', async(): Promise<void> => {
|
||||
webIdStore.isLinked.mockResolvedValueOnce(true);
|
||||
await expect(creator.handle({ name, accountId }))
|
||||
.rejects.toThrow(`${generatedWebId} is already registered to this account.`);
|
||||
expect(webIdStore.isLinked).toHaveBeenCalledTimes(1);
|
||||
expect(webIdStore.isLinked).toHaveBeenLastCalledWith(generatedWebId, accountId);
|
||||
expect(webIdStore.create).toHaveBeenCalledTimes(0);
|
||||
expect(podStore.create).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('undoes any changes if something goes wrong creating the pod.', async(): Promise<void> => {
|
||||
const error = new Error('bad data');
|
||||
podStore.create.mockRejectedValueOnce(error);
|
||||
|
||||
await expect(creator.handle({ name, accountId })).rejects.toBe(error);
|
||||
|
||||
expect(webIdStore.create).toHaveBeenCalledTimes(1);
|
||||
expect(webIdStore.create).toHaveBeenLastCalledWith(generatedWebId, accountId);
|
||||
expect(podStore.create).toHaveBeenCalledTimes(1);
|
||||
expect(podStore.create).toHaveBeenLastCalledWith(accountId, {
|
||||
base: { path: podUrl },
|
||||
webId: generatedWebId,
|
||||
oidcIssuer: baseUrl,
|
||||
}, false);
|
||||
expect(webIdStore.delete).toHaveBeenCalledTimes(1);
|
||||
expect(webIdStore.delete).toHaveBeenLastCalledWith(webIdLink);
|
||||
});
|
||||
});
|
@ -1,6 +1,7 @@
|
||||
import { writeJson } from 'fs-extra';
|
||||
import type { JsonInteractionHandler } from '../../../src/identity/interaction/JsonInteractionHandler';
|
||||
import type { ResolveLoginHandler } from '../../../src/identity/interaction/login/ResolveLoginHandler';
|
||||
import { AccountStore } from '../../../src/identity/interaction/account/util/AccountStore';
|
||||
import { PasswordStore } from '../../../src/identity/interaction/password/util/PasswordStore';
|
||||
import { PodCreator } from '../../../src/identity/interaction/pod/util/PodCreator';
|
||||
import { SeededAccountInitializer } from '../../../src/init/SeededAccountInitializer';
|
||||
import { mockFileSystem } from '../../util/Util';
|
||||
|
||||
@ -25,40 +26,45 @@ describe('A SeededAccountInitializer', (): void => {
|
||||
},
|
||||
];
|
||||
const configFilePath = './seeded-pod-config.json';
|
||||
let accountHandler: jest.Mocked<ResolveLoginHandler>;
|
||||
let passwordHandler: jest.Mocked<JsonInteractionHandler>;
|
||||
let podHandler: jest.Mocked<JsonInteractionHandler>;
|
||||
let accountStore: jest.Mocked<AccountStore>;
|
||||
let passwordStore: jest.Mocked<PasswordStore>;
|
||||
let podCreator: jest.Mocked<PodCreator>;
|
||||
let initializer: SeededAccountInitializer;
|
||||
|
||||
beforeEach(async(): Promise<void> => {
|
||||
let count = 0;
|
||||
accountHandler = {
|
||||
login: jest.fn(async(): Promise<unknown> => {
|
||||
accountStore = {
|
||||
create: jest.fn(async(): Promise<string> => {
|
||||
count += 1;
|
||||
return { json: { accountId: `account${count}` }};
|
||||
return `account${count}`;
|
||||
}),
|
||||
} as any;
|
||||
} satisfies Partial<AccountStore> as any;
|
||||
|
||||
passwordHandler = {
|
||||
handleSafe: jest.fn(),
|
||||
} as any;
|
||||
let pwCount = 0;
|
||||
passwordStore = {
|
||||
create: jest.fn(async(): Promise<string> => {
|
||||
pwCount += 1;
|
||||
return `password${pwCount}`;
|
||||
}),
|
||||
confirmVerification: jest.fn(),
|
||||
} satisfies Partial<PasswordStore> as any;
|
||||
|
||||
podHandler = {
|
||||
podCreator = {
|
||||
handleSafe: jest.fn(),
|
||||
} as any;
|
||||
} satisfies Partial<PodCreator> as any;
|
||||
|
||||
mockFileSystem('/');
|
||||
await writeJson(configFilePath, dummyConfig);
|
||||
|
||||
initializer = new SeededAccountInitializer({
|
||||
accountHandler, passwordHandler, podHandler, configFilePath,
|
||||
accountStore, passwordStore, podCreator, configFilePath,
|
||||
});
|
||||
});
|
||||
|
||||
it('does not generate any accounts or pods if no config file is specified.', async(): Promise<void> => {
|
||||
await expect(new SeededAccountInitializer({ accountHandler, passwordHandler, podHandler }).handle())
|
||||
await expect(new SeededAccountInitializer({ accountStore, passwordStore, podCreator }).handle())
|
||||
.resolves.toBeUndefined();
|
||||
expect(accountHandler.login).toHaveBeenCalledTimes(0);
|
||||
expect(accountStore.create).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('errors if the seed file is invalid.', async(): Promise<void> => {
|
||||
@ -69,24 +75,25 @@ describe('A SeededAccountInitializer', (): void => {
|
||||
|
||||
it('generates an account with the specified settings.', async(): Promise<void> => {
|
||||
await expect(initializer.handleSafe()).resolves.toBeUndefined();
|
||||
expect(accountHandler.login).toHaveBeenCalledTimes(2);
|
||||
expect(passwordHandler.handleSafe).toHaveBeenCalledTimes(2);
|
||||
expect(passwordHandler.handleSafe.mock.calls[0][0].json)
|
||||
.toEqual(expect.objectContaining({ email: 'hello@example.com', password: 'abc123' }));
|
||||
expect(passwordHandler.handleSafe.mock.calls[1][0].json)
|
||||
.toEqual(expect.objectContaining({ email: 'hello2@example.com', password: '123abc' }));
|
||||
expect(podHandler.handleSafe).toHaveBeenCalledTimes(3);
|
||||
expect(podHandler.handleSafe.mock.calls[0][0].json).toEqual(expect.objectContaining(dummyConfig[0].pods![0]));
|
||||
expect(podHandler.handleSafe.mock.calls[1][0].json).toEqual(expect.objectContaining(dummyConfig[0].pods![1]));
|
||||
expect(podHandler.handleSafe.mock.calls[2][0].json).toEqual(expect.objectContaining(dummyConfig[0].pods![2]));
|
||||
expect(accountStore.create).toHaveBeenCalledTimes(2);
|
||||
expect(passwordStore.create).toHaveBeenCalledTimes(2);
|
||||
expect(passwordStore.create).toHaveBeenNthCalledWith(1, 'hello@example.com', 'account1', 'abc123');
|
||||
expect(passwordStore.create).toHaveBeenNthCalledWith(2, 'hello2@example.com', 'account2', '123abc');
|
||||
expect(passwordStore.confirmVerification).toHaveBeenCalledTimes(2);
|
||||
expect(passwordStore.confirmVerification).toHaveBeenNthCalledWith(1, 'password1');
|
||||
expect(passwordStore.confirmVerification).toHaveBeenNthCalledWith(2, 'password2');
|
||||
expect(podCreator.handleSafe).toHaveBeenCalledTimes(3);
|
||||
expect(podCreator.handleSafe).toHaveBeenNthCalledWith(1, { accountId: 'account1', name: 'pod1', settings: {}});
|
||||
expect(podCreator.handleSafe).toHaveBeenNthCalledWith(2, { accountId: 'account1', name: 'pod2', settings: {}});
|
||||
expect(podCreator.handleSafe).toHaveBeenNthCalledWith(3, { accountId: 'account1', name: 'pod3', settings: {}});
|
||||
});
|
||||
|
||||
it('does not throw exceptions when one of the steps fails.', async(): Promise<void> => {
|
||||
accountHandler.login.mockRejectedValueOnce(new Error('bad data'));
|
||||
accountStore.create.mockRejectedValueOnce(new Error('bad data'));
|
||||
await expect(initializer.handleSafe()).resolves.toBeUndefined();
|
||||
expect(accountHandler.login).toHaveBeenCalledTimes(2);
|
||||
expect(accountStore.create).toHaveBeenCalledTimes(2);
|
||||
// Steps for first account will be skipped due to error
|
||||
expect(passwordHandler.handleSafe).toHaveBeenCalledTimes(1);
|
||||
expect(podHandler.handleSafe).toHaveBeenCalledTimes(0);
|
||||
expect(passwordStore.create).toHaveBeenCalledTimes(1);
|
||||
expect(podCreator.handleSafe).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user