mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
feat: Combine pod creation with IDP registration
This commit is contained in:
parent
9bb42ddf0d
commit
4d7d939dc4
@ -8,12 +8,13 @@
|
||||
"pathName": "^/idp/register/?$",
|
||||
"postHandler": {
|
||||
"@type": "RegistrationHandler",
|
||||
"args_ownershipValidator": {
|
||||
"@type": "TokenOwnershipValidator",
|
||||
"storage": { "@id": "urn:solid-server:default:ExpiringIdpStorage" }
|
||||
},
|
||||
"args_baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" },
|
||||
"args_webIdSuffix": "/profile/card",
|
||||
"args_identifierGenerator": { "@id": "urn:solid-server:default:IdentifierGenerator" },
|
||||
"args_ownershipValidator": { "@id": "urn:solid-server:auth:password:OwnershipValidator" },
|
||||
"args_accountStore": { "@id": "urn:solid-server:auth:password:AccountStore" },
|
||||
"args_interactionCompleter": { "@id": "urn:solid-server:auth:password:InteractionCompleter" }
|
||||
"args_podManager": { "@id": "urn:solid-server:default:PodManager" },
|
||||
"args_responseHandler": { "@id": "urn:solid-server:auth:password:RegisterResponseRenderHandler" }
|
||||
},
|
||||
"renderHandler": { "@id": "urn:solid-server:auth:password:RegisterRenderHandler" }
|
||||
},
|
||||
@ -24,6 +25,13 @@
|
||||
"@type": "RenderEjsHandler",
|
||||
"templatePath": { "@id": "urn:solid-server:default:variable:idpTemplateFolder" },
|
||||
"templateFile": "./email-password-interaction/register.ejs"
|
||||
},
|
||||
{
|
||||
"comment": "Renders the successful registration page",
|
||||
"@id": "urn:solid-server:auth:password:RegisterResponseRenderHandler",
|
||||
"@type": "RenderEjsHandler",
|
||||
"templatePath": { "@id": "urn:solid-server:default:variable:idpTemplateFolder" },
|
||||
"templateFile": "./email-password-interaction/registerResponse.ejs"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -1,83 +1,241 @@
|
||||
import assert from 'assert';
|
||||
import urljoin from 'url-join';
|
||||
import type { ResourceIdentifier } from '../../../../ldp/representation/ResourceIdentifier';
|
||||
import { getLoggerFor } from '../../../../logging/LogUtil';
|
||||
import type { IdentifierGenerator } from '../../../../pods/generate/IdentifierGenerator';
|
||||
import type { PodManager } from '../../../../pods/PodManager';
|
||||
import type { HttpHandlerInput } from '../../../../server/HttpHandler';
|
||||
import { HttpHandler } from '../../../../server/HttpHandler';
|
||||
import type { HttpRequest } from '../../../../server/HttpRequest';
|
||||
import type { InteractionHttpHandlerInput } from '../../InteractionHttpHandler';
|
||||
import { InteractionHttpHandler } from '../../InteractionHttpHandler';
|
||||
import type { RenderHandler } from '../../../../server/util/RenderHandler';
|
||||
import type { OwnershipValidator } from '../../../ownership/OwnershipValidator';
|
||||
import { getFormDataRequestBody } from '../../util/FormDataUtil';
|
||||
import type { InteractionCompleter } from '../../util/InteractionCompleter';
|
||||
import type { OwnershipValidator } from '../../util/OwnershipValidator';
|
||||
import { assertPassword, throwIdpInteractionError } from '../EmailPasswordUtil';
|
||||
import type { AccountStore } from '../storage/AccountStore';
|
||||
|
||||
const emailRegex = /^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/u;
|
||||
|
||||
interface RegistrationHandlerArgs {
|
||||
export interface RegistrationHandlerArgs {
|
||||
/**
|
||||
* Used to set the `oidcIssuer` value of newly registered pods.
|
||||
*/
|
||||
baseUrl: string;
|
||||
/**
|
||||
* Appended to the generated pod identifier to create the corresponding WebID.
|
||||
*/
|
||||
webIdSuffix: string;
|
||||
/**
|
||||
* Generates identifiers for new pods.
|
||||
*/
|
||||
identifierGenerator: IdentifierGenerator;
|
||||
/**
|
||||
* Verifies the user is the owner of the WebID they provide.
|
||||
*/
|
||||
ownershipValidator: OwnershipValidator;
|
||||
/**
|
||||
* Stores all the registered account information.
|
||||
*/
|
||||
accountStore: AccountStore;
|
||||
interactionCompleter: InteractionCompleter;
|
||||
/**
|
||||
* Creates the new pods.
|
||||
*/
|
||||
podManager: PodManager;
|
||||
/**
|
||||
* Renders the response when registration is successful.
|
||||
*/
|
||||
responseHandler: RenderHandler<NodeJS.Dict<any>>;
|
||||
}
|
||||
|
||||
// Results when parsing the input form data
|
||||
type ParseResult = {
|
||||
/**
|
||||
* All the parameters that will be parsed from a request.
|
||||
* `data` contains all the raw values to potentially be used by pod templates.
|
||||
*/
|
||||
interface ParseResult {
|
||||
email: string;
|
||||
password: string;
|
||||
webId: string;
|
||||
remember: boolean;
|
||||
};
|
||||
password?: string;
|
||||
podName?: string;
|
||||
webId?: string;
|
||||
createWebId: boolean;
|
||||
register: boolean;
|
||||
createPod: boolean;
|
||||
data: NodeJS.Dict<string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the submission of the registration form.
|
||||
* Creates the user and logs them in if successful.
|
||||
* This class handles the 3 potential steps of the registration process:
|
||||
* 1. Generating a new WebID.
|
||||
* 2. Registering a WebID with the IDP.
|
||||
* 3. Creating a new pod for a given WebID.
|
||||
*
|
||||
* All of these steps are optional and will be determined based on the input parameters of a request,
|
||||
* with the following considerations:
|
||||
* * At least one option needs to be chosen.
|
||||
* * In case a new WebID needs to be created, the other 2 steps are obligatory.
|
||||
* * Ownership will be verified when the WebID is provided.
|
||||
* * When registering and creating a pod, the base URL will be used as oidcIssuer value.
|
||||
*/
|
||||
export class RegistrationHandler extends InteractionHttpHandler {
|
||||
export class RegistrationHandler extends HttpHandler {
|
||||
protected readonly logger = getLoggerFor(this);
|
||||
|
||||
private readonly baseUrl: string;
|
||||
private readonly webIdSuffix: string;
|
||||
private readonly identifierGenerator: IdentifierGenerator;
|
||||
private readonly ownershipValidator: OwnershipValidator;
|
||||
private readonly accountStore: AccountStore;
|
||||
private readonly interactionCompleter: InteractionCompleter;
|
||||
private readonly podManager: PodManager;
|
||||
private readonly responseHandler: RenderHandler<NodeJS.Dict<any>>;
|
||||
|
||||
public constructor(args: RegistrationHandlerArgs) {
|
||||
super();
|
||||
this.baseUrl = args.baseUrl;
|
||||
this.webIdSuffix = args.webIdSuffix;
|
||||
this.identifierGenerator = args.identifierGenerator;
|
||||
this.ownershipValidator = args.ownershipValidator;
|
||||
this.accountStore = args.accountStore;
|
||||
this.interactionCompleter = args.interactionCompleter;
|
||||
this.podManager = args.podManager;
|
||||
this.responseHandler = args.responseHandler;
|
||||
}
|
||||
|
||||
public async handle(input: InteractionHttpHandlerInput): Promise<void> {
|
||||
const { email, webId, password, remember } = await this.parseInput(input.request);
|
||||
public async handle({ request, response }: HttpHandlerInput): Promise<void> {
|
||||
const result = await this.parseInput(request);
|
||||
|
||||
try {
|
||||
// Check if WebId contains required triples and register new account if successful
|
||||
await this.ownershipValidator.handleSafe({ webId });
|
||||
await this.accountStore.create(email, webId, password);
|
||||
await this.interactionCompleter.handleSafe({
|
||||
...input,
|
||||
webId,
|
||||
shouldRemember: Boolean(remember),
|
||||
});
|
||||
this.logger.debug(`Registering agent ${email} with WebId ${webId}`);
|
||||
} catch (err: unknown) {
|
||||
throwIdpInteractionError(err, { email, webId });
|
||||
const props = await this.register(result);
|
||||
|
||||
await this.responseHandler.handleSafe({ response, props });
|
||||
} catch (error: unknown) {
|
||||
throwIdpInteractionError(error, result.data as Record<string, string>);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses and validates the input form data.
|
||||
* Will throw an {@link IdpInteractionError} in case something is wrong.
|
||||
* All relevant data that was correct up to that point will be prefilled.
|
||||
* Does the full registration and pod creation process,
|
||||
* with the steps chosen by the values in the `ParseResult`.
|
||||
*/
|
||||
private async register(result: ParseResult): Promise<NodeJS.Dict<any>> {
|
||||
// This is only used when createWebId and/or createPod are true
|
||||
let podBaseUrl: ResourceIdentifier | undefined;
|
||||
|
||||
// Create or verify the WebID
|
||||
if (result.createWebId) {
|
||||
podBaseUrl = this.identifierGenerator.generate(result.podName!);
|
||||
result.webId = urljoin(podBaseUrl.path, this.webIdSuffix);
|
||||
} else {
|
||||
await this.ownershipValidator.handleSafe({ webId: result.webId! });
|
||||
}
|
||||
|
||||
// Register the account
|
||||
if (result.register) {
|
||||
await this.accountStore.create(result.email, result.webId!, result.password!);
|
||||
|
||||
// Add relevant data for the templates
|
||||
result.data.oidcIssuer = this.baseUrl;
|
||||
}
|
||||
|
||||
// Create the pod
|
||||
if (result.createPod) {
|
||||
podBaseUrl = podBaseUrl ?? this.identifierGenerator.generate(result.podName!);
|
||||
try {
|
||||
await this.podManager.createPod(podBaseUrl, { ...result.data, webId: result.webId! });
|
||||
} catch (error: unknown) {
|
||||
// In case pod creation errors we don't want to keep the account
|
||||
if (result.register) {
|
||||
await this.accountStore.deleteAccount(result.email);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Verify the account
|
||||
if (result.register) {
|
||||
// This prevents there being a small timeframe where the account can be used before the pod creation is finished.
|
||||
// That timeframe could potentially be used by malicious users.
|
||||
await this.accountStore.verify(result.email);
|
||||
}
|
||||
|
||||
return {
|
||||
webId: result.webId,
|
||||
email: result.email,
|
||||
oidcIssuer: this.baseUrl,
|
||||
podBaseUrl: podBaseUrl?.path,
|
||||
createWebId: result.createWebId,
|
||||
register: result.register,
|
||||
createPod: result.createPod,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the input request into a `ParseResult`.
|
||||
*/
|
||||
private async parseInput(request: HttpRequest): Promise<ParseResult> {
|
||||
const prefilled: Record<string, string> = {};
|
||||
const parsed = await getFormDataRequestBody(request);
|
||||
let prefilled: Record<string, string> = {};
|
||||
try {
|
||||
const { email, webId, password, confirmPassword, remember } = await getFormDataRequestBody(request);
|
||||
assert(typeof email === 'string' && email.length > 0, 'Email required');
|
||||
assert(emailRegex.test(email), 'Invalid email');
|
||||
prefilled.email = email;
|
||||
assert(typeof webId === 'string' && webId.length > 0, 'WebId required');
|
||||
prefilled.webId = webId;
|
||||
assertPassword(password, confirmPassword);
|
||||
return { email, password, webId, remember: Boolean(remember) };
|
||||
for (const key of Object.keys(parsed)) {
|
||||
if (Array.isArray(parsed[key])) {
|
||||
throw new Error(`Multiple values found for key ${key}`);
|
||||
}
|
||||
}
|
||||
prefilled = parsed as Record<string, string>;
|
||||
return this.validateInput(prefilled);
|
||||
} catch (err: unknown) {
|
||||
throwIdpInteractionError(err, prefilled);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the raw input date into a `ParseResult`.
|
||||
* Verifies that all the data combinations make sense.
|
||||
*/
|
||||
private validateInput(parsed: NodeJS.Dict<string>): ParseResult {
|
||||
const { email, password, confirmPassword, podName, webId } = parsed;
|
||||
|
||||
assert(typeof email === 'string' && email.length > 0 && emailRegex.test(email),
|
||||
'A valid e-mail address is required');
|
||||
|
||||
const result: ParseResult = {
|
||||
email,
|
||||
createWebId: Boolean(parsed.createWebId),
|
||||
register: Boolean(parsed.register),
|
||||
createPod: Boolean(parsed.createPod),
|
||||
data: parsed,
|
||||
};
|
||||
|
||||
const validWebId = typeof webId === 'string' && webId.length > 0;
|
||||
if (result.createWebId) {
|
||||
if (validWebId) {
|
||||
throw new Error('A WebID should only be provided when no new one is being created');
|
||||
}
|
||||
} else {
|
||||
if (!validWebId) {
|
||||
throw new Error('A WebID is required if no new one is being created');
|
||||
}
|
||||
result.webId = webId;
|
||||
}
|
||||
|
||||
if (result.register) {
|
||||
assertPassword(password, confirmPassword);
|
||||
result.password = password;
|
||||
} else if (typeof password === 'string' && password.length > 0) {
|
||||
throw new Error('A password should only be provided when registering');
|
||||
}
|
||||
|
||||
if (result.createWebId || result.createPod) {
|
||||
assert(typeof podName === 'string' && podName.length > 0,
|
||||
'A pod name is required when creating a pod and/or WebID');
|
||||
result.podName = podName;
|
||||
} else if (typeof podName === 'string' && podName.length > 0) {
|
||||
throw new Error('A pod name should only be provided when creating a pod and/or WebID');
|
||||
}
|
||||
|
||||
if (result.createWebId && !(result.register && result.createPod)) {
|
||||
throw new Error('Creating a WebID is only possible when also registering and creating a pod');
|
||||
}
|
||||
|
||||
if (!result.createWebId && !result.register && !result.createPod) {
|
||||
throw new Error('At least one option needs to be chosen');
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,6 @@ import { getLoggerFor } from '../logging/LogUtil';
|
||||
import type { KeyValueStorage } from '../storage/keyvalue/KeyValueStorage';
|
||||
import type { ResourceStore } from '../storage/ResourceStore';
|
||||
import { addGeneratedResources } from './generate/GenerateUtil';
|
||||
import type { IdentifierGenerator } from './generate/IdentifierGenerator';
|
||||
import type { PodGenerator } from './generate/PodGenerator';
|
||||
import type { ResourcesGenerator } from './generate/ResourcesGenerator';
|
||||
import type { PodManager } from './PodManager';
|
||||
@ -22,27 +21,23 @@ import type { PodSettings } from './settings/PodSettings';
|
||||
*/
|
||||
export class ConfigPodManager implements PodManager {
|
||||
protected readonly logger = getLoggerFor(this);
|
||||
private readonly idGenerator: IdentifierGenerator;
|
||||
private readonly podGenerator: PodGenerator;
|
||||
private readonly routingStorage: KeyValueStorage<string, ResourceStore>;
|
||||
private readonly resourcesGenerator: ResourcesGenerator;
|
||||
|
||||
/**
|
||||
* @param idGenerator - Generator for the pod identifiers.
|
||||
* @param podGenerator - Generator for the pod stores.
|
||||
* @param resourcesGenerator - Generator for the pod resources.
|
||||
* @param routingStorage - Where to store the generated pods so they can be routed to.
|
||||
*/
|
||||
public constructor(idGenerator: IdentifierGenerator, podGenerator: PodGenerator,
|
||||
resourcesGenerator: ResourcesGenerator, routingStorage: KeyValueStorage<string, ResourceStore>) {
|
||||
this.idGenerator = idGenerator;
|
||||
public constructor(podGenerator: PodGenerator, resourcesGenerator: ResourcesGenerator,
|
||||
routingStorage: KeyValueStorage<string, ResourceStore>) {
|
||||
this.podGenerator = podGenerator;
|
||||
this.routingStorage = routingStorage;
|
||||
this.resourcesGenerator = resourcesGenerator;
|
||||
}
|
||||
|
||||
public async createPod(settings: PodSettings): Promise<ResourceIdentifier> {
|
||||
const identifier = this.idGenerator.generate(settings.login);
|
||||
public async createPod(identifier: ResourceIdentifier, settings: PodSettings): Promise<void> {
|
||||
this.logger.info(`Creating pod ${identifier.path}`);
|
||||
|
||||
// Will error in case there already is a store for the given identifier
|
||||
@ -52,7 +47,5 @@ export class ConfigPodManager implements PodManager {
|
||||
this.logger.info(`Added ${count} resources to ${identifier.path}`);
|
||||
|
||||
await this.routingStorage.set(identifier.path, store);
|
||||
|
||||
return identifier;
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,6 @@ import { getLoggerFor } from '../logging/LogUtil';
|
||||
import type { ResourceStore } from '../storage/ResourceStore';
|
||||
import { ConflictHttpError } from '../util/errors/ConflictHttpError';
|
||||
import { addGeneratedResources } from './generate/GenerateUtil';
|
||||
import type { IdentifierGenerator } from './generate/IdentifierGenerator';
|
||||
import type { ResourcesGenerator } from './generate/ResourcesGenerator';
|
||||
import type { PodManager } from './PodManager';
|
||||
import type { PodSettings } from './settings/PodSettings';
|
||||
@ -16,29 +15,24 @@ export class GeneratedPodManager implements PodManager {
|
||||
protected readonly logger = getLoggerFor(this);
|
||||
|
||||
private readonly store: ResourceStore;
|
||||
private readonly idGenerator: IdentifierGenerator;
|
||||
private readonly resourcesGenerator: ResourcesGenerator;
|
||||
|
||||
public constructor(store: ResourceStore, idGenerator: IdentifierGenerator, resourcesGenerator: ResourcesGenerator) {
|
||||
public constructor(store: ResourceStore, resourcesGenerator: ResourcesGenerator) {
|
||||
this.store = store;
|
||||
this.idGenerator = idGenerator;
|
||||
this.resourcesGenerator = resourcesGenerator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new pod, pre-populating it with the resources created by the data generator.
|
||||
* Pod identifiers are created based on the identifier generator.
|
||||
* Will throw an error if the given identifier already has a resource.
|
||||
*/
|
||||
public async createPod(settings: PodSettings): Promise<ResourceIdentifier> {
|
||||
const podIdentifier = this.idGenerator.generate(settings.login);
|
||||
this.logger.info(`Creating pod ${podIdentifier.path}`);
|
||||
if (await this.store.resourceExists(podIdentifier)) {
|
||||
throw new ConflictHttpError(`There already is a resource at ${podIdentifier.path}`);
|
||||
public async createPod(identifier: ResourceIdentifier, settings: PodSettings): Promise<void> {
|
||||
this.logger.info(`Creating pod ${identifier.path}`);
|
||||
if (await this.store.resourceExists(identifier)) {
|
||||
throw new ConflictHttpError(`There already is a resource at ${identifier.path}`);
|
||||
}
|
||||
|
||||
const count = await addGeneratedResources(podIdentifier, settings, this.resourcesGenerator, this.store);
|
||||
this.logger.info(`Added ${count} resources to ${podIdentifier.path}`);
|
||||
return podIdentifier;
|
||||
const count = await addGeneratedResources(identifier, settings, this.resourcesGenerator, this.store);
|
||||
this.logger.info(`Added ${count} resources to ${identifier.path}`);
|
||||
}
|
||||
}
|
||||
|
@ -8,8 +8,8 @@ import type { PodSettings } from './settings/PodSettings';
|
||||
export interface PodManager {
|
||||
/**
|
||||
* Creates a pod for the given settings.
|
||||
* @param identifier - Root identifier indicating where the pod should be created.
|
||||
* @param settings - Settings describing the pod.
|
||||
* @returns {@link ResourceIdentifier} of the newly created pod.
|
||||
*/
|
||||
createPod: (settings: PodSettings) => Promise<ResourceIdentifier>;
|
||||
createPod: (identifier: ResourceIdentifier, settings: PodSettings) => Promise<void>;
|
||||
}
|
||||
|
@ -4,10 +4,6 @@
|
||||
* they give an indication of what is sometimes expected.
|
||||
*/
|
||||
export interface PodSettings extends NodeJS.Dict<string> {
|
||||
/**
|
||||
* The name of the pod. Will be used for generating the identifier.
|
||||
*/
|
||||
login: string;
|
||||
/**
|
||||
* The WebId of the owner of this pod.
|
||||
*/
|
||||
@ -29,8 +25,4 @@ export interface PodSettings extends NodeJS.Dict<string> {
|
||||
* The OIDC issuer of the owner's WebId.
|
||||
*/
|
||||
oidcIssuer?: string;
|
||||
/**
|
||||
* A registration token for linking the owner's WebId to an IDP.
|
||||
*/
|
||||
oidcIssuerRegistrationToken?: string;
|
||||
}
|
||||
|
@ -28,12 +28,7 @@
|
||||
</div>
|
||||
|
||||
<div class="input-wrap">
|
||||
<label for="webId">WebId:</label>
|
||||
<input id="webId" type="text" name="webId" <% if (prefilled.webId) { %> value="<%= prefilled.webId %>" <% } %> />
|
||||
</div>
|
||||
|
||||
<div class="input-wrap">
|
||||
<label for="password">Password:</label>
|
||||
<label for="password">Password (when registering with the IDP):</label>
|
||||
<input id="password" type="password" name="password" />
|
||||
</div>
|
||||
|
||||
@ -43,15 +38,28 @@
|
||||
</div>
|
||||
|
||||
<div class="input-wrap">
|
||||
<label class="checkbox"><input type="checkbox" name="remember" value="yes" checked>Stay signed in</label>
|
||||
<label for="podName">Pod name (when creating a pod):</label>
|
||||
<input id="podName" type="text" name="podName" <% if (prefilled.podName) { %> value="<%= prefilled.podName %>" <% } %> />
|
||||
</div>
|
||||
|
||||
<button type="submit" name="submit" class="ids-link-filled">Create Identity</button>
|
||||
|
||||
<hr />
|
||||
<div class="space-between">
|
||||
<a href="/idp/login" class="link">Sign In</a>
|
||||
<div class="input-wrap">
|
||||
<label for="webId">WebID (when not creating a WebID):</label>
|
||||
<input id="webId" type="text" name="webId" <% if (prefilled.webId) { %> value="<%= prefilled.webId %>" <% } %> />
|
||||
</div>
|
||||
|
||||
<div class="input-wrap">
|
||||
<label class="checkbox"><input type="checkbox" name="createWebId" checked>Create new WebID (also requires the other two options)</label>
|
||||
</div>
|
||||
|
||||
<div class="input-wrap">
|
||||
<label class="checkbox"><input type="checkbox" name="register" checked>Register your WebID with the IDP</label>
|
||||
</div>
|
||||
|
||||
<div class="input-wrap">
|
||||
<label class="checkbox"><input type="checkbox" name="createPod" checked>Create a new pod</label>
|
||||
</div>
|
||||
|
||||
<button type="submit" name="submit" class="ids-link-filled">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -0,0 +1,43 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Registration Succesful</title>
|
||||
<link rel="stylesheet" href="/idp/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="main-content">
|
||||
<section class="main-content-section">
|
||||
<div class="wrapper">
|
||||
<h1 class="title--white">Registration Successful</h1>
|
||||
<div class="login-panel">
|
||||
<div class="panel-body">
|
||||
<% if (createWebId) { %>
|
||||
<p class="content__message">
|
||||
Your new WebID is <a href="<%= webId %>" class="link"><%= webId %></a>
|
||||
</p>
|
||||
<% } %>
|
||||
<% if (register) { %>
|
||||
<p class="content__message">
|
||||
You can now identify as <a href="<%= webId %>" class="link"><%= webId %></a> with our IDP using <%= email %>
|
||||
</p>
|
||||
<% if (!createWebId) { %>
|
||||
<p class="content__message">
|
||||
Make sure you add the triple
|
||||
<%= `<${webId}> <http://www.w3.org/ns/solid/terms#oidcIssuer> <${oidcIssuer}>.`%>
|
||||
to your WebID profile.
|
||||
</p>
|
||||
<% } %>
|
||||
<% } %>
|
||||
<% if (createPod) { %>
|
||||
<p class="content__message">
|
||||
Your new pod has been created and can be found at <a href="<%= podBaseUrl %>" class="link"><%= podBaseUrl %></a>
|
||||
</p>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@ -7,7 +7,6 @@
|
||||
foaf:primaryTopic <{{webId}}>.
|
||||
|
||||
<{{webId}}>
|
||||
a foaf:Person;
|
||||
foaf:name "{{name}}";
|
||||
solid:oidcIssuer <{{oidcIssuer}}> ;
|
||||
solid:oidcIssuerRegistrationToken "{{oidcIssuerRegistrationToken}}" .
|
||||
{{#if name}}foaf:name "{{name}}";{{/if}}
|
||||
{{#if oidcIssuer}}solid:oidcIssuer <{{oidcIssuer}}>;{{/if}}
|
||||
a foaf:Person.
|
||||
|
@ -1,8 +1,10 @@
|
||||
import { mkdirSync } from 'fs';
|
||||
import type { Server } from 'http';
|
||||
import { stringify } from 'querystring';
|
||||
import fetch from 'cross-fetch';
|
||||
import type { Initializer } from '../../src/init/Initializer';
|
||||
import type { HttpServerFactory } from '../../src/server/HttpServerFactory';
|
||||
import type { WrappedExpiringStorage } from '../../src/storage/keyvalue/WrappedExpiringStorage';
|
||||
import { joinFilePath } from '../../src/util/PathUtil';
|
||||
import { getPort } from '../util/Util';
|
||||
import { getTestConfigPath, getTestFolder, instantiateFromConfig, removeFolder } from './Config';
|
||||
@ -27,15 +29,16 @@ describe.each(configs)('A dynamic pod server with template config %s', (template
|
||||
let server: Server;
|
||||
let initializer: Initializer;
|
||||
let factory: HttpServerFactory;
|
||||
const agent = { login: 'alice', webId: 'http://test.com/#alice', name: 'Alice Bob', template };
|
||||
const podUrl = `${baseUrl}${agent.login}/`;
|
||||
let expiringStorage: WrappedExpiringStorage<any, any>;
|
||||
const settings = { podName: 'alice', webId: 'http://test.com/#alice', email: 'alice@test.email', template, createPod: true };
|
||||
const podUrl = `${baseUrl}${settings.podName}/`;
|
||||
|
||||
beforeAll(async(): Promise<void> => {
|
||||
const variables: Record<string, any> = {
|
||||
'urn:solid-server:default:variable:baseUrl': baseUrl,
|
||||
'urn:solid-server:default:variable:port': port,
|
||||
'urn:solid-server:default:variable:rootFilePath': rootFilePath,
|
||||
'urn:solid-server:default:variable:podConfigJson': podConfigJson,
|
||||
'urn:solid-server:default:variable:idpTemplateFolder': joinFilePath(__dirname, '../../templates/idp'),
|
||||
};
|
||||
|
||||
// Need to make sure the temp folder exists so the podConfigJson can be written to it
|
||||
@ -47,7 +50,7 @@ describe.each(configs)('A dynamic pod server with template config %s', (template
|
||||
getTestConfigPath('server-dynamic-unsafe.json'),
|
||||
variables,
|
||||
) as Record<string, any>;
|
||||
({ factory, initializer } = instances);
|
||||
({ factory, initializer, expiringStorage } = instances);
|
||||
|
||||
// Set up the internal store
|
||||
await initializer.handleSafe();
|
||||
@ -56,6 +59,7 @@ describe.each(configs)('A dynamic pod server with template config %s', (template
|
||||
});
|
||||
|
||||
afterAll(async(): Promise<void> => {
|
||||
expiringStorage.finalize();
|
||||
await new Promise((resolve, reject): void => {
|
||||
server.close((error): void => error ? reject(error) : resolve());
|
||||
});
|
||||
@ -63,15 +67,13 @@ describe.each(configs)('A dynamic pod server with template config %s', (template
|
||||
});
|
||||
|
||||
it('creates a pod with the given config.', async(): Promise<void> => {
|
||||
const res = await fetch(`${baseUrl}pods`, {
|
||||
const res = await fetch(`${baseUrl}idp/register`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(agent),
|
||||
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
||||
body: stringify(settings),
|
||||
});
|
||||
expect(res.status).toBe(201);
|
||||
expect(res.headers.get('location')).toBe(podUrl);
|
||||
expect(res.status).toBe(200);
|
||||
await expect(res.text()).resolves.toContain(podUrl);
|
||||
});
|
||||
|
||||
it('can fetch the created pod.', async(): Promise<void> => {
|
||||
@ -87,7 +89,7 @@ describe.each(configs)('A dynamic pod server with template config %s', (template
|
||||
it('should be able to read acl file with the correct credentials.', async(): Promise<void> => {
|
||||
const res = await fetch(`${podUrl}.acl`, {
|
||||
headers: {
|
||||
authorization: `WebID ${agent.webId}`,
|
||||
authorization: `WebID ${settings.webId}`,
|
||||
},
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
@ -96,7 +98,7 @@ describe.each(configs)('A dynamic pod server with template config %s', (template
|
||||
it('should be able to write to the pod now as the owner.', async(): Promise<void> => {
|
||||
let res = await fetch(`${podUrl}test`, {
|
||||
headers: {
|
||||
authorization: `WebID ${agent.webId}`,
|
||||
authorization: `WebID ${settings.webId}`,
|
||||
},
|
||||
});
|
||||
expect(res.status).toBe(404);
|
||||
@ -104,7 +106,7 @@ describe.each(configs)('A dynamic pod server with template config %s', (template
|
||||
res = await fetch(`${podUrl}test`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
authorization: `WebID ${agent.webId}`,
|
||||
authorization: `WebID ${settings.webId}`,
|
||||
'content-type': 'text/plain',
|
||||
},
|
||||
body: 'this is new data!',
|
||||
@ -113,7 +115,7 @@ describe.each(configs)('A dynamic pod server with template config %s', (template
|
||||
|
||||
res = await fetch(`${podUrl}test`, {
|
||||
headers: {
|
||||
authorization: `WebID ${agent.webId}`,
|
||||
authorization: `WebID ${settings.webId}`,
|
||||
},
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
@ -121,13 +123,13 @@ describe.each(configs)('A dynamic pod server with template config %s', (template
|
||||
});
|
||||
|
||||
it('should not be able to create a pod with the same name.', async(): Promise<void> => {
|
||||
const res = await fetch(`${baseUrl}pods`, {
|
||||
const res = await fetch(`${baseUrl}idp/register`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(agent),
|
||||
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
||||
body: stringify(settings),
|
||||
});
|
||||
expect(res.status).toBe(409);
|
||||
// 200 due to there only being a HTML solution right now that only returns 200
|
||||
expect(res.status).toBe(200);
|
||||
await expect(res.text()).resolves.toContain(`There already is a pod at ${podUrl}`);
|
||||
});
|
||||
});
|
||||
|
@ -2,7 +2,9 @@ import type { Server } from 'http';
|
||||
import { stringify } from 'querystring';
|
||||
import { URL } from 'url';
|
||||
import { load } from 'cheerio';
|
||||
import type { Response } from 'cross-fetch';
|
||||
import { fetch } from 'cross-fetch';
|
||||
import urljoin from 'url-join';
|
||||
import type { Initializer } from '../../src/init/Initializer';
|
||||
import type { HttpServerFactory } from '../../src/server/HttpServerFactory';
|
||||
import type { WrappedExpiringStorage } from '../../src/storage/keyvalue/WrappedExpiringStorage';
|
||||
@ -24,6 +26,14 @@ jest.mock('nodemailer');
|
||||
// Prevent panva/node-openid-client from emitting DraftWarning
|
||||
jest.spyOn(process, 'emitWarning').mockImplementation();
|
||||
|
||||
async function postForm(url: string, formBody: string): Promise<Response> {
|
||||
return fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': APPLICATION_X_WWW_FORM_URLENCODED },
|
||||
body: formBody,
|
||||
});
|
||||
}
|
||||
|
||||
// No way around the cookies https://github.com/panva/node-oidc-provider/issues/552 .
|
||||
// They will be simulated by storing the values and passing them along.
|
||||
// This is why the redirects are handled manually.
|
||||
@ -35,7 +45,7 @@ describe('A Solid server with IDP', (): void => {
|
||||
let factory: HttpServerFactory;
|
||||
const redirectUrl = 'http://mockedredirect/';
|
||||
const oidcIssuer = baseUrl;
|
||||
const card = new URL('profile/card', baseUrl).href;
|
||||
const card = urljoin(baseUrl, 'profile/card');
|
||||
const webId = `${card}#me`;
|
||||
const email = 'test@test.com';
|
||||
const password = 'password!';
|
||||
@ -52,9 +62,7 @@ describe('A Solid server with IDP', (): void => {
|
||||
'urn:solid-server:test:Instances',
|
||||
getTestConfigPath('server-memory.json'),
|
||||
{
|
||||
'urn:solid-server:default:variable:port': port,
|
||||
'urn:solid-server:default:variable:baseUrl': baseUrl,
|
||||
'urn:solid-server:default:variable:podTemplateFolder': joinFilePath(__dirname, '../assets/templates'),
|
||||
'urn:solid-server:default:variable:idpTemplateFolder': joinFilePath(__dirname, '../../templates/idp'),
|
||||
},
|
||||
) as Record<string, any>;
|
||||
@ -79,27 +87,16 @@ describe('A Solid server with IDP', (): void => {
|
||||
});
|
||||
|
||||
describe('doing registration', (): void => {
|
||||
let state: IdentityTestState;
|
||||
let nextUrl: string;
|
||||
let formBody: string;
|
||||
let registrationTriple: string;
|
||||
|
||||
beforeAll(async(): Promise<void> => {
|
||||
state = new IdentityTestState(baseUrl, redirectUrl, oidcIssuer);
|
||||
|
||||
// We will need this twice
|
||||
formBody = stringify({ email, webId, password, confirmPassword: password, remember: 'yes' });
|
||||
});
|
||||
|
||||
it('initializes the session and finds the registration URL.', async(): Promise<void> => {
|
||||
const url = await state.startSession();
|
||||
const { register } = await state.parseLoginPage(url);
|
||||
expect(typeof register).toBe('string');
|
||||
nextUrl = (await state.extractFormUrl(register)).url;
|
||||
formBody = stringify({ email, webId, password, confirmPassword: password, register: 'ok' });
|
||||
});
|
||||
|
||||
it('sends the form once to receive the registration triple.', async(): Promise<void> => {
|
||||
const res = await state.fetchIdp(nextUrl, 'POST', formBody, APPLICATION_X_WWW_FORM_URLENCODED);
|
||||
const res = await postForm(`${baseUrl}idp/register`, formBody);
|
||||
expect(res.status).toBe(200);
|
||||
// eslint-disable-next-line newline-per-chained-call
|
||||
registrationTriple = load(await res.text())('form div label').first().text().trim().split('\n')[0];
|
||||
@ -119,15 +116,14 @@ describe('A Solid server with IDP', (): void => {
|
||||
expect(res.status).toBe(205);
|
||||
});
|
||||
|
||||
it('sends the form again once the registration token was added.', async(): Promise<void> => {
|
||||
const res = await state.fetchIdp(nextUrl, 'POST', formBody, APPLICATION_X_WWW_FORM_URLENCODED);
|
||||
expect(res.status).toBe(302);
|
||||
nextUrl = res.headers.get('location')!;
|
||||
});
|
||||
|
||||
it('will be redirected internally and logged in.', async(): Promise<void> => {
|
||||
await state.handleLoginRedirect(nextUrl);
|
||||
expect(state.session.info?.webId).toBe(webId);
|
||||
it('sends the form again to successfully register.', async(): Promise<void> => {
|
||||
const res = await postForm(`${baseUrl}idp/register`, formBody);
|
||||
expect(res.status).toBe(200);
|
||||
const text = await res.text();
|
||||
expect(text).toMatch(new RegExp(`You can now identify as .*${webId}.*with our IDP using ${email}`, 'u'));
|
||||
expect(text).toMatch(new RegExp(`Make sure you add the triple
|
||||
\\s*<${webId}> <http://www.w3.org/ns/solid/terms#oidcIssuer> <${baseUrl}>\\.
|
||||
\\s*to your WebID profile\\.`, 'mu'));
|
||||
});
|
||||
});
|
||||
|
||||
@ -193,22 +189,10 @@ describe('A Solid server with IDP', (): void => {
|
||||
});
|
||||
|
||||
describe('resetting password', (): void => {
|
||||
let state: IdentityTestState;
|
||||
let nextUrl: string;
|
||||
|
||||
beforeAll(async(): Promise<void> => {
|
||||
state = new IdentityTestState(baseUrl, redirectUrl, oidcIssuer);
|
||||
});
|
||||
|
||||
it('initializes the session and finds the forgot password URL.', async(): Promise<void> => {
|
||||
const url = await state.startSession();
|
||||
const { forgotPassword } = await state.parseLoginPage(url);
|
||||
expect(typeof forgotPassword).toBe('string');
|
||||
nextUrl = (await state.extractFormUrl(forgotPassword)).url;
|
||||
});
|
||||
|
||||
it('sends the corresponding email address through the form to get a mail.', async(): Promise<void> => {
|
||||
const res = await state.fetchIdp(nextUrl, 'POST', stringify({ email }), APPLICATION_X_WWW_FORM_URLENCODED);
|
||||
const res = await postForm(`${baseUrl}idp/forgotpassword`, stringify({ email }));
|
||||
expect(res.status).toBe(200);
|
||||
expect(load(await res.text())('form div p').first().text().trim())
|
||||
.toBe('If your account exists, an email has been sent with a link to reset your password.');
|
||||
@ -222,13 +206,23 @@ describe('A Solid server with IDP', (): void => {
|
||||
});
|
||||
|
||||
it('resets the password through the given link.', async(): Promise<void> => {
|
||||
const { url, body } = await state.extractFormUrl(nextUrl);
|
||||
// Extract the submit URL from the reset password form
|
||||
let res = await fetch(nextUrl);
|
||||
expect(res.status).toBe(200);
|
||||
const text = await res.text();
|
||||
const relative = load(text)('form').attr('action');
|
||||
expect(typeof relative).toBe('string');
|
||||
|
||||
const recordId = load(body)('input[name="recordId"]').attr('value');
|
||||
const recordId = load(text)('input[name="recordId"]').attr('value');
|
||||
expect(typeof recordId).toBe('string');
|
||||
|
||||
// POST the new password
|
||||
const formData = stringify({ password: password2, confirmPassword: password2, recordId });
|
||||
const res = await state.fetchIdp(url, 'POST', formData, APPLICATION_X_WWW_FORM_URLENCODED);
|
||||
res = await fetch(new URL(relative!, baseUrl).href, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': APPLICATION_X_WWW_FORM_URLENCODED },
|
||||
body: formData,
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
expect(await res.text()).toContain('Your password was successfully reset.');
|
||||
});
|
||||
@ -261,4 +255,97 @@ describe('A Solid server with IDP', (): void => {
|
||||
expect(state.session.info?.webId).toBe(webId);
|
||||
});
|
||||
});
|
||||
|
||||
describe('creating pods without registering', (): void => {
|
||||
let formBody: string;
|
||||
let registrationTriple: string;
|
||||
const podName = 'myPod';
|
||||
|
||||
beforeAll(async(): Promise<void> => {
|
||||
// We will need this twice
|
||||
formBody = stringify({ email, webId, podName, createPod: 'ok' });
|
||||
});
|
||||
|
||||
it('sends the form once to receive the registration triple.', async(): Promise<void> => {
|
||||
const res = await postForm(`${baseUrl}idp/register`, formBody);
|
||||
expect(res.status).toBe(200);
|
||||
// eslint-disable-next-line newline-per-chained-call
|
||||
registrationTriple = load(await res.text())('form div label').first().text().trim().split('\n')[0];
|
||||
expect(registrationTriple).toMatch(new RegExp(
|
||||
`^<${webId}> <http://www.w3.org/ns/solid/terms#oidcIssuerRegistrationToken> "[^"]+"\\s*\\.\\s*$`,
|
||||
'u',
|
||||
));
|
||||
});
|
||||
|
||||
it('updates the webId with the registration token.', async(): Promise<void> => {
|
||||
const patchBody = `INSERT DATA { ${registrationTriple} }`;
|
||||
const res = await fetch(webId, {
|
||||
method: 'PATCH',
|
||||
headers: { 'content-type': 'application/sparql-update' },
|
||||
body: patchBody,
|
||||
});
|
||||
expect(res.status).toBe(205);
|
||||
});
|
||||
|
||||
it('sends the form again to successfully register.', async(): Promise<void> => {
|
||||
const res = await postForm(`${baseUrl}idp/register`, formBody);
|
||||
expect(res.status).toBe(200);
|
||||
const text = await res.text();
|
||||
expect(text).toMatch(new RegExp(`Your new pod has been created and can be found at.*${baseUrl}${podName}/`, 'u'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('creating a new WebID', (): void => {
|
||||
const podName = 'alice';
|
||||
const newMail = 'alice@test.email';
|
||||
let newWebId: string;
|
||||
let podLocation: string;
|
||||
let state: IdentityTestState;
|
||||
|
||||
const formBody = stringify({
|
||||
email: newMail, password, confirmPassword: password, podName, createWebId: 'ok', register: 'ok', createPod: 'ok',
|
||||
});
|
||||
|
||||
it('sends the form to create the WebID and register.', async(): Promise<void> => {
|
||||
const res = await postForm(`${baseUrl}idp/register`, formBody);
|
||||
expect(res.status).toBe(200);
|
||||
const text = await res.text();
|
||||
|
||||
const matchWebId = /Your new WebID is [^>]+>([^<]+)/u.exec(text);
|
||||
expect(matchWebId).toBeDefined();
|
||||
expect(matchWebId).toHaveLength(2);
|
||||
newWebId = matchWebId![1];
|
||||
expect(text).toMatch(new RegExp(`You can now identify as .*${newWebId}.*with our IDP using ${newMail}`, 'u'));
|
||||
|
||||
const matchPod = /Your new pod has been created and can be found at [^>]+>([^<]+)/u.exec(text);
|
||||
expect(matchPod).toBeDefined();
|
||||
expect(matchPod).toHaveLength(2);
|
||||
podLocation = matchPod![1];
|
||||
expect(newWebId.startsWith(podLocation)).toBe(true);
|
||||
expect(podLocation.startsWith(baseUrl)).toBe(true);
|
||||
});
|
||||
|
||||
it('initializes the session and logs in.', async(): Promise<void> => {
|
||||
state = new IdentityTestState(baseUrl, redirectUrl, oidcIssuer);
|
||||
const url = await state.startSession();
|
||||
const { login } = await state.parseLoginPage(url);
|
||||
expect(typeof login).toBe('string');
|
||||
await state.login(login, newMail, password);
|
||||
expect(state.session.info?.webId).toBe(newWebId);
|
||||
});
|
||||
|
||||
it('can only write to the new profile when using the logged in session.', async(): Promise<void> => {
|
||||
const patchOptions = {
|
||||
method: 'PATCH',
|
||||
headers: { 'content-type': 'application/sparql-update' },
|
||||
body: `INSERT DATA { <> <http://www.w3.org/2000/01/rdf-schema#label> "A cool WebID." }`,
|
||||
};
|
||||
|
||||
let res = await fetch(newWebId, patchOptions);
|
||||
expect(res.status).toBe(401);
|
||||
|
||||
res = await state.session.fetch(newWebId, patchOptions);
|
||||
expect(res.status).toBe(205);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,7 +1,10 @@
|
||||
import type { Server } from 'http';
|
||||
import { stringify } from 'querystring';
|
||||
import fetch from 'cross-fetch';
|
||||
import type { Initializer } from '../../src/init/Initializer';
|
||||
import type { HttpServerFactory } from '../../src/server/HttpServerFactory';
|
||||
import type { WrappedExpiringStorage } from '../../src/storage/keyvalue/WrappedExpiringStorage';
|
||||
import { joinFilePath } from '../../src/util/PathUtil';
|
||||
import { getPort } from '../util/Util';
|
||||
import { getPresetConfigPath, getTestConfigPath, getTestFolder, instantiateFromConfig, removeFolder } from './Config';
|
||||
|
||||
@ -25,15 +28,16 @@ describe.each(stores)('A subdomain server with %s', (name, { storeConfig, teardo
|
||||
let server: Server;
|
||||
let initializer: Initializer;
|
||||
let factory: HttpServerFactory;
|
||||
const settings = { login: 'alice', webId: 'http://test.com/#alice', name: 'Alice Bob' };
|
||||
let expiringStorage: WrappedExpiringStorage<any, any>;
|
||||
const settings = { podName: 'alice', webId: 'http://test.com/#alice', email: 'alice@test.email', createPod: true };
|
||||
const podHost = `alice.localhost:${port}`;
|
||||
const podUrl = `http://${podHost}/`;
|
||||
|
||||
beforeAll(async(): Promise<void> => {
|
||||
const variables: Record<string, any> = {
|
||||
'urn:solid-server:default:variable:port': port,
|
||||
'urn:solid-server:default:variable:baseUrl': baseUrl,
|
||||
'urn:solid-server:default:variable:rootFilePath': rootFilePath,
|
||||
'urn:solid-server:default:variable:idpTemplateFolder': joinFilePath(__dirname, '../../templates/idp'),
|
||||
};
|
||||
|
||||
// Create and initialize the server
|
||||
@ -45,13 +49,14 @@ describe.each(stores)('A subdomain server with %s', (name, { storeConfig, teardo
|
||||
],
|
||||
variables,
|
||||
) as Record<string, any>;
|
||||
({ factory, initializer } = instances);
|
||||
({ factory, initializer, expiringStorage } = instances);
|
||||
|
||||
await initializer.handleSafe();
|
||||
server = factory.startServer(port);
|
||||
});
|
||||
|
||||
afterAll(async(): Promise<void> => {
|
||||
expiringStorage.finalize();
|
||||
await new Promise((resolve, reject): void => {
|
||||
server.close((error): void => error ? reject(error) : resolve());
|
||||
});
|
||||
@ -83,15 +88,13 @@ describe.each(stores)('A subdomain server with %s', (name, { storeConfig, teardo
|
||||
|
||||
describe('handling pods', (): void => {
|
||||
it('creates pods in a subdomain.', async(): Promise<void> => {
|
||||
const res = await fetch(`${baseUrl}pods`, {
|
||||
const res = await fetch(`${baseUrl}idp/register`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(settings),
|
||||
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
||||
body: stringify(settings),
|
||||
});
|
||||
expect(res.status).toBe(201);
|
||||
expect(res.headers.get('location')).toBe(podUrl);
|
||||
expect(res.status).toBe(200);
|
||||
await expect(res.text()).resolves.toContain(podUrl);
|
||||
});
|
||||
|
||||
it('can fetch the created pod in a subdomain.', async(): Promise<void> => {
|
||||
@ -145,14 +148,14 @@ describe.each(stores)('A subdomain server with %s', (name, { storeConfig, teardo
|
||||
});
|
||||
|
||||
it('should not be able to create a pod with the same name.', async(): Promise<void> => {
|
||||
const res = await fetch(`${baseUrl}pods`, {
|
||||
const res = await fetch(`${baseUrl}idp/register`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(settings),
|
||||
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
||||
body: stringify(settings),
|
||||
});
|
||||
expect(res.status).toBe(409);
|
||||
// 200 due to there only being a HTML solution right now that only returns 200
|
||||
expect(res.status).toBe(200);
|
||||
await expect(res.text()).resolves.toContain(`There already is a resource at ${podUrl}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -5,6 +5,9 @@
|
||||
"files-scs:config/http/middleware/no-websockets.json",
|
||||
"files-scs:config/http/server-factory/no-websockets.json",
|
||||
"files-scs:config/http/static/default.json",
|
||||
"files-scs:config/identity/email/default.json",
|
||||
"files-scs:config/identity/handler/default.json",
|
||||
"files-scs:config/identity/ownership/unsafe-no-check.json",
|
||||
"files-scs:config/init/handler/default.json",
|
||||
"files-scs:config/ldp/authentication/debug-auth-header.json",
|
||||
"files-scs:config/ldp/authorization/webacl.json",
|
||||
@ -37,12 +40,13 @@
|
||||
{
|
||||
"RecordObject:_record_key": "factory",
|
||||
"RecordObject:_record_value": { "@id": "urn:solid-server:default:ServerFactory" }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"@id": "urn:solid-server:default:IdentityProviderHandler",
|
||||
"@type": "UnsupportedAsyncHandler"
|
||||
"comment": "Timer needs to be stopped when tests are finished.",
|
||||
"RecordObject:_record_key": "expiringStorage",
|
||||
"RecordObject:_record_value": { "@id": "urn:solid-server:default:ExpiringIdpStorage" }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"@id": "urn:solid-server:default:ResourcesGenerator",
|
||||
|
@ -54,10 +54,6 @@
|
||||
"args_emailConfig_port": 587,
|
||||
"args_emailConfig_auth_user": "alice@example.email",
|
||||
"args_emailConfig_auth_pass": "NYEaCsqV7aVStRCbmC"
|
||||
},
|
||||
{
|
||||
"@id": "urn:solid-server:default:ResourcesGenerator",
|
||||
"TemplatedResourcesGenerator:_templateFolder": "$PACKAGE_ROOT/test/assets/templates"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -5,6 +5,9 @@
|
||||
"files-scs:config/http/middleware/no-websockets.json",
|
||||
"files-scs:config/http/server-factory/no-websockets.json",
|
||||
"files-scs:config/http/static/default.json",
|
||||
"files-scs:config/identity/email/default.json",
|
||||
"files-scs:config/identity/handler/default.json",
|
||||
"files-scs:config/identity/ownership/unsafe-no-check.json",
|
||||
"files-scs:config/init/handler/default.json",
|
||||
"files-scs:config/ldp/authentication/debug-auth-header.json",
|
||||
"files-scs:config/ldp/authorization/webacl.json",
|
||||
@ -33,20 +36,16 @@
|
||||
"RecordObject:_record_key": "initializer",
|
||||
"RecordObject:_record_value": { "@id": "urn:solid-server:default:ParallelInitializer" }
|
||||
},
|
||||
{
|
||||
"RecordObject:_record_key": "store",
|
||||
"RecordObject:_record_value": { "@id": "urn:solid-server:default:ResourceStore" }
|
||||
},
|
||||
{
|
||||
"RecordObject:_record_key": "factory",
|
||||
"RecordObject:_record_value": { "@id": "urn:solid-server:default:ServerFactory" }
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
"@id": "urn:solid-server:default:IdentityProviderHandler",
|
||||
"@type": "UnsupportedAsyncHandler"
|
||||
"comment": "Timer needs to be stopped when tests are finished.",
|
||||
"RecordObject:_record_key": "expiringStorage",
|
||||
"RecordObject:_record_value": { "@id": "urn:solid-server:default:ExpiringIdpStorage" }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -1,92 +1,260 @@
|
||||
import type { Provider } from 'oidc-provider';
|
||||
import urljoin from 'url-join';
|
||||
import {
|
||||
RegistrationHandler,
|
||||
} from '../../../../../../src/identity/interaction/email-password/handler/RegistrationHandler';
|
||||
import type { AccountStore } from '../../../../../../src/identity/interaction/email-password/storage/AccountStore';
|
||||
import type { InteractionCompleter } from '../../../../../../src/identity/interaction/util/InteractionCompleter';
|
||||
import type { OwnershipValidator } from '../../../../../../src/identity/interaction/util/OwnershipValidator';
|
||||
import { IdpInteractionError } from '../../../../../../src/identity/interaction/util/IdpInteractionError';
|
||||
import type { OwnershipValidator } from '../../../../../../src/identity/ownership/OwnershipValidator';
|
||||
import type { ResourceIdentifier } from '../../../../../../src/ldp/representation/ResourceIdentifier';
|
||||
import type { IdentifierGenerator } from '../../../../../../src/pods/generate/IdentifierGenerator';
|
||||
import type { PodManager } from '../../../../../../src/pods/PodManager';
|
||||
import type { HttpRequest } from '../../../../../../src/server/HttpRequest';
|
||||
import type { HttpResponse } from '../../../../../../src/server/HttpResponse';
|
||||
import type { RenderHandler } from '../../../../../../src/server/util/RenderHandler';
|
||||
import { createPostFormRequest } from './Util';
|
||||
|
||||
describe('A RegistrationHandler', (): void => {
|
||||
// "Correct" values for easy object creation
|
||||
const webId = 'http://alice.test.com/card#me';
|
||||
const email = 'alice@test.email';
|
||||
const password = 'superSecretPassword';
|
||||
const confirmPassword = password;
|
||||
const podName = 'alice';
|
||||
// Strings instead of booleans because this is form data
|
||||
const createWebId = 'true';
|
||||
const register = 'true';
|
||||
const createPod = 'true';
|
||||
|
||||
let request: HttpRequest;
|
||||
const response: HttpResponse = {} as any;
|
||||
const provider: Provider = {} as any;
|
||||
|
||||
const baseUrl = 'http://test.com/';
|
||||
const webIdSuffix = '/profile/card';
|
||||
let identifierGenerator: IdentifierGenerator;
|
||||
let ownershipValidator: OwnershipValidator;
|
||||
let accountStore: AccountStore;
|
||||
let interactionCompleter: InteractionCompleter;
|
||||
let podManager: PodManager;
|
||||
let responseHandler: RenderHandler<NodeJS.Dict<any>>;
|
||||
let handler: RegistrationHandler;
|
||||
|
||||
beforeEach(async(): Promise<void> => {
|
||||
identifierGenerator = {
|
||||
generate: jest.fn((name: string): ResourceIdentifier => ({ path: `${baseUrl}${name}/` })),
|
||||
};
|
||||
|
||||
ownershipValidator = {
|
||||
handleSafe: jest.fn(),
|
||||
} as any;
|
||||
|
||||
accountStore = {
|
||||
create: jest.fn(),
|
||||
verify: jest.fn(),
|
||||
deleteAccount: jest.fn(),
|
||||
} as any;
|
||||
|
||||
interactionCompleter = {
|
||||
podManager = {
|
||||
createPod: jest.fn(),
|
||||
};
|
||||
|
||||
responseHandler = {
|
||||
handleSafe: jest.fn(),
|
||||
} as any;
|
||||
|
||||
handler = new RegistrationHandler({
|
||||
ownershipValidator,
|
||||
baseUrl,
|
||||
webIdSuffix,
|
||||
identifierGenerator,
|
||||
accountStore,
|
||||
interactionCompleter,
|
||||
ownershipValidator,
|
||||
podManager,
|
||||
responseHandler,
|
||||
});
|
||||
});
|
||||
|
||||
it('errors on non-string emails.', async(): Promise<void> => {
|
||||
request = createPostFormRequest({});
|
||||
await expect(handler.handle({ request, response, provider })).rejects.toThrow('Email required');
|
||||
request = createPostFormRequest({ email: [ 'email', 'email2' ]});
|
||||
await expect(handler.handle({ request, response, provider })).rejects.toThrow('Email required');
|
||||
describe('validating data', (): void => {
|
||||
it('rejects array inputs.', async(): Promise<void> => {
|
||||
request = createPostFormRequest({ data: [ 'a', 'b' ]});
|
||||
await expect(handler.handle({ request, response })).rejects.toThrow('Multiple values found for key data');
|
||||
});
|
||||
|
||||
it('errors on invalid emails.', async(): Promise<void> => {
|
||||
request = createPostFormRequest({ email: undefined });
|
||||
await expect(handler.handle({ request, response })).rejects.toThrow('A valid e-mail address is required');
|
||||
|
||||
request = createPostFormRequest({ email: '' });
|
||||
await expect(handler.handle({ request, response })).rejects.toThrow('A valid e-mail address is required');
|
||||
|
||||
request = createPostFormRequest({ email: 'invalidEmail' });
|
||||
const prom = handler.handle({ request, response, provider });
|
||||
await expect(prom).rejects.toThrow('Invalid email');
|
||||
await expect(prom).rejects.toThrow(expect.objectContaining({ prefilled: { }}));
|
||||
await expect(handler.handle({ request, response })).rejects.toThrow('A valid e-mail address is required');
|
||||
});
|
||||
|
||||
it('errors on non-string webIds.', async(): Promise<void> => {
|
||||
request = createPostFormRequest({ email });
|
||||
let prom = handler.handle({ request, response, provider });
|
||||
await expect(prom).rejects.toThrow('WebId required');
|
||||
await expect(prom).rejects.toThrow(expect.objectContaining({ prefilled: { email }}));
|
||||
request = createPostFormRequest({ email, webId: [ 'a', 'b' ]});
|
||||
prom = handler.handle({ request, response, provider });
|
||||
await expect(prom).rejects.toThrow('WebId required');
|
||||
await expect(prom).rejects.toThrow(expect.objectContaining({ prefilled: { email }}));
|
||||
it('errors when an unnecessary WebID is provided.', async(): Promise<void> => {
|
||||
request = createPostFormRequest({ email, webId, createWebId });
|
||||
await expect(handler.handle({ request, response }))
|
||||
.rejects.toThrow('A WebID should only be provided when no new one is being created');
|
||||
});
|
||||
|
||||
it('errors on invalid passwords.', async(): Promise<void> => {
|
||||
request = createPostFormRequest({ email, webId, password: 'password!', confirmPassword: 'bad' });
|
||||
const prom = handler.handle({ request, response, provider });
|
||||
await expect(prom).rejects.toThrow('Password and confirmation do not match');
|
||||
await expect(prom).rejects.toThrow(expect.objectContaining({ prefilled: { email, webId }}));
|
||||
it('errors when a required WebID is not valid.', async(): Promise<void> => {
|
||||
request = createPostFormRequest({ email, webId: undefined });
|
||||
await expect(handler.handle({ request, response }))
|
||||
.rejects.toThrow('A WebID is required if no new one is being created');
|
||||
|
||||
request = createPostFormRequest({ email, webId: '' });
|
||||
await expect(handler.handle({ request, response }))
|
||||
.rejects.toThrow('A WebID is required if no new one is being created');
|
||||
});
|
||||
|
||||
it('throws an IdpInteractionError if there is a problem.', async(): Promise<void> => {
|
||||
request = createPostFormRequest({ email, webId, password: 'password!', confirmPassword: 'password!' });
|
||||
(accountStore.create as jest.Mock).mockRejectedValueOnce(new Error('create failed!'));
|
||||
const prom = handler.handle({ request, response, provider });
|
||||
await expect(prom).rejects.toThrow('create failed!');
|
||||
await expect(prom).rejects.toThrow(expect.objectContaining({ prefilled: { email, webId }}));
|
||||
it('errors when an unnecessary password is provided.', async(): Promise<void> => {
|
||||
request = createPostFormRequest({ email, webId, password });
|
||||
await expect(handler.handle({ request, response }))
|
||||
.rejects.toThrow('A password should only be provided when registering');
|
||||
});
|
||||
|
||||
it('calls the OidcInteractionCompleter when done.', async(): Promise<void> => {
|
||||
request = createPostFormRequest({ email, webId, password: 'password!', confirmPassword: 'password!' });
|
||||
await expect(handler.handle({ request, response, provider })).resolves.toBeUndefined();
|
||||
it('errors on invalid passwords when registering.', async(): Promise<void> => {
|
||||
request = createPostFormRequest({ email, webId, password, confirmPassword: 'bad', register });
|
||||
await expect(handler.handle({ request, response })).rejects.toThrow('Password and confirmation do not match');
|
||||
});
|
||||
|
||||
it('errors when an unnecessary pod name is provided.', async(): Promise<void> => {
|
||||
request = createPostFormRequest({ email, webId, podName });
|
||||
await expect(handler.handle({ request, response }))
|
||||
.rejects.toThrow('A pod name should only be provided when creating a pod and/or WebID');
|
||||
});
|
||||
|
||||
it('errors on invalid pod names when required.', async(): Promise<void> => {
|
||||
request = createPostFormRequest({ email, podName: undefined, createWebId });
|
||||
await expect(handler.handle({ request, response }))
|
||||
.rejects.toThrow('A pod name is required when creating a pod and/or WebID');
|
||||
|
||||
request = createPostFormRequest({ email, webId, podName: '', createPod });
|
||||
await expect(handler.handle({ request, response }))
|
||||
.rejects.toThrow('A pod name is required when creating a pod and/or WebID');
|
||||
});
|
||||
|
||||
it('errors when trying to create a WebID without registering or creating a pod.', async(): Promise<void> => {
|
||||
request = createPostFormRequest({ email, podName, createWebId });
|
||||
await expect(handler.handle({ request, response }))
|
||||
.rejects.toThrow('Creating a WebID is only possible when also registering and creating a pod');
|
||||
|
||||
request = createPostFormRequest({ email, podName, password, confirmPassword, createWebId, register });
|
||||
await expect(handler.handle({ request, response }))
|
||||
.rejects.toThrow('Creating a WebID is only possible when also registering and creating a pod');
|
||||
|
||||
request = createPostFormRequest({ email, podName, createWebId, createPod });
|
||||
await expect(handler.handle({ request, response }))
|
||||
.rejects.toThrow('Creating a WebID is only possible when also registering and creating a pod');
|
||||
});
|
||||
|
||||
it('errors when no option is chosen.', async(): Promise<void> => {
|
||||
request = createPostFormRequest({ email, webId });
|
||||
await expect(handler.handle({ request, response })).rejects.toThrow('At least one option needs to be chosen');
|
||||
});
|
||||
});
|
||||
|
||||
describe('handling data', (): void => {
|
||||
it('can register a user.', async(): Promise<void> => {
|
||||
request = createPostFormRequest({ email, webId, password, confirmPassword, register });
|
||||
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
|
||||
|
||||
expect(ownershipValidator.handleSafe).toHaveBeenCalledTimes(1);
|
||||
expect(ownershipValidator.handleSafe).toHaveBeenLastCalledWith({ webId });
|
||||
expect(accountStore.create).toHaveBeenCalledTimes(1);
|
||||
expect(accountStore.create).toHaveBeenLastCalledWith(email, webId, 'password!');
|
||||
expect(interactionCompleter.handleSafe).toHaveBeenCalledTimes(1);
|
||||
expect(interactionCompleter.handleSafe)
|
||||
.toHaveBeenLastCalledWith({ request, response, provider, webId, shouldRemember: false });
|
||||
expect(accountStore.create).toHaveBeenLastCalledWith(email, webId, password);
|
||||
expect(accountStore.verify).toHaveBeenCalledTimes(1);
|
||||
expect(accountStore.verify).toHaveBeenLastCalledWith(email);
|
||||
|
||||
expect(identifierGenerator.generate).toHaveBeenCalledTimes(0);
|
||||
expect(accountStore.deleteAccount).toHaveBeenCalledTimes(0);
|
||||
expect(podManager.createPod).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('can create a pod.', async(): Promise<void> => {
|
||||
const params = { email, webId, podName, createPod };
|
||||
request = createPostFormRequest(params);
|
||||
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
|
||||
|
||||
expect(ownershipValidator.handleSafe).toHaveBeenCalledTimes(1);
|
||||
expect(ownershipValidator.handleSafe).toHaveBeenLastCalledWith({ webId });
|
||||
expect(identifierGenerator.generate).toHaveBeenCalledTimes(1);
|
||||
expect(identifierGenerator.generate).toHaveBeenLastCalledWith(podName);
|
||||
expect(podManager.createPod).toHaveBeenCalledTimes(1);
|
||||
expect(podManager.createPod).toHaveBeenLastCalledWith({ path: `${baseUrl}${podName}/` }, params);
|
||||
|
||||
expect(accountStore.create).toHaveBeenCalledTimes(0);
|
||||
expect(accountStore.verify).toHaveBeenCalledTimes(0);
|
||||
expect(accountStore.deleteAccount).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('adds an oidcIssuer to the data when doing both IDP registration and pod creation.', async(): Promise<void> => {
|
||||
const params = { email, webId, password, confirmPassword, podName, register, createPod };
|
||||
request = createPostFormRequest(params);
|
||||
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
|
||||
|
||||
expect(ownershipValidator.handleSafe).toHaveBeenCalledTimes(1);
|
||||
expect(ownershipValidator.handleSafe).toHaveBeenLastCalledWith({ webId });
|
||||
expect(accountStore.create).toHaveBeenCalledTimes(1);
|
||||
expect(accountStore.create).toHaveBeenLastCalledWith(email, webId, password);
|
||||
expect(identifierGenerator.generate).toHaveBeenCalledTimes(1);
|
||||
expect(identifierGenerator.generate).toHaveBeenLastCalledWith(podName);
|
||||
(params as any).oidcIssuer = baseUrl;
|
||||
expect(podManager.createPod).toHaveBeenCalledTimes(1);
|
||||
expect(podManager.createPod).toHaveBeenLastCalledWith({ path: `${baseUrl}${podName}/` }, params);
|
||||
expect(accountStore.verify).toHaveBeenCalledTimes(1);
|
||||
expect(accountStore.verify).toHaveBeenLastCalledWith(email);
|
||||
|
||||
expect(accountStore.deleteAccount).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('deletes the created account if pod generation fails.', async(): Promise<void> => {
|
||||
const params = { email, webId, password, confirmPassword, podName, register, createPod };
|
||||
request = createPostFormRequest(params);
|
||||
(podManager.createPod as jest.Mock).mockRejectedValueOnce(new Error('pod error'));
|
||||
await expect(handler.handle({ request, response })).rejects.toThrow('pod error');
|
||||
|
||||
expect(ownershipValidator.handleSafe).toHaveBeenCalledTimes(1);
|
||||
expect(ownershipValidator.handleSafe).toHaveBeenLastCalledWith({ webId });
|
||||
expect(accountStore.create).toHaveBeenCalledTimes(1);
|
||||
expect(accountStore.create).toHaveBeenLastCalledWith(email, webId, password);
|
||||
expect(identifierGenerator.generate).toHaveBeenCalledTimes(1);
|
||||
expect(identifierGenerator.generate).toHaveBeenLastCalledWith(podName);
|
||||
(params as any).oidcIssuer = baseUrl;
|
||||
expect(podManager.createPod).toHaveBeenCalledTimes(1);
|
||||
expect(podManager.createPod).toHaveBeenLastCalledWith({ path: `${baseUrl}${podName}/` }, params);
|
||||
expect(accountStore.deleteAccount).toHaveBeenCalledTimes(1);
|
||||
expect(accountStore.deleteAccount).toHaveBeenLastCalledWith(email);
|
||||
|
||||
expect(accountStore.verify).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('can create a WebID with an account and pod.', async(): Promise<void> => {
|
||||
const params = { email, password, confirmPassword, podName, createWebId, register, createPod };
|
||||
request = createPostFormRequest(params);
|
||||
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
|
||||
|
||||
const generatedWebID = urljoin(baseUrl, podName, webIdSuffix);
|
||||
|
||||
expect(identifierGenerator.generate).toHaveBeenCalledTimes(1);
|
||||
expect(identifierGenerator.generate).toHaveBeenLastCalledWith(podName);
|
||||
expect(accountStore.create).toHaveBeenCalledTimes(1);
|
||||
expect(accountStore.create).toHaveBeenLastCalledWith(email, generatedWebID, password);
|
||||
expect(accountStore.verify).toHaveBeenCalledTimes(1);
|
||||
expect(accountStore.verify).toHaveBeenLastCalledWith(email);
|
||||
const podParams = { ...params, oidcIssuer: baseUrl, webId: generatedWebID };
|
||||
expect(podManager.createPod).toHaveBeenCalledTimes(1);
|
||||
expect(podManager.createPod).toHaveBeenLastCalledWith({ path: `${baseUrl}${podName}/` }, podParams);
|
||||
|
||||
expect(ownershipValidator.handleSafe).toHaveBeenCalledTimes(0);
|
||||
expect(accountStore.deleteAccount).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('throws an IdpInteractionError with all data prefilled if something goes wrong.', async(): Promise<void> => {
|
||||
const params = { email, webId, podName, createPod };
|
||||
request = createPostFormRequest(params);
|
||||
(podManager.createPod as jest.Mock).mockRejectedValueOnce(new Error('pod error'));
|
||||
const prom = handler.handle({ request, response });
|
||||
await expect(prom).rejects.toThrow('pod error');
|
||||
await expect(prom).rejects.toThrow(IdpInteractionError);
|
||||
await expect(prom).rejects.toThrow(expect.objectContaining({ prefilled: params }));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,6 +1,5 @@
|
||||
import type { ResourceIdentifier } from '../../../src/ldp/representation/ResourceIdentifier';
|
||||
import { ConfigPodManager } from '../../../src/pods/ConfigPodManager';
|
||||
import type { IdentifierGenerator } from '../../../src/pods/generate/IdentifierGenerator';
|
||||
import type { PodGenerator } from '../../../src/pods/generate/PodGenerator';
|
||||
import type { Resource, ResourcesGenerator } from '../../../src/pods/generate/ResourcesGenerator';
|
||||
import type { PodSettings } from '../../../src/pods/settings/PodSettings';
|
||||
@ -10,9 +9,6 @@ import type { ResourceStore } from '../../../src/storage/ResourceStore';
|
||||
describe('A ConfigPodManager', (): void => {
|
||||
let settings: PodSettings;
|
||||
const base = 'http://test.com/';
|
||||
const idGenerator: IdentifierGenerator = {
|
||||
generate: (slug: string): ResourceIdentifier => ({ path: `${base}${slug}/` }),
|
||||
};
|
||||
let store: ResourceStore;
|
||||
let podGenerator: PodGenerator;
|
||||
let routingStorage: KeyValueStorage<string, ResourceStore>;
|
||||
@ -50,12 +46,12 @@ describe('A ConfigPodManager', (): void => {
|
||||
set: async(key: ResourceIdentifier, value: ResourceStore): Promise<any> => map.set(key, value),
|
||||
} as any;
|
||||
|
||||
manager = new ConfigPodManager(idGenerator, podGenerator, resourcesGenerator, routingStorage);
|
||||
manager = new ConfigPodManager(podGenerator, resourcesGenerator, routingStorage);
|
||||
});
|
||||
|
||||
it('creates a pod and returns the newly generated identifier.', async(): Promise<void> => {
|
||||
const identifier = { path: `${base}alice/` };
|
||||
await expect(manager.createPod(settings)).resolves.toEqual(identifier);
|
||||
await expect(manager.createPod(identifier, settings)).resolves.toBeUndefined();
|
||||
expect(podGenerator.generate).toHaveBeenCalledTimes(1);
|
||||
expect(podGenerator.generate).toHaveBeenLastCalledWith(identifier, settings);
|
||||
expect(resourcesGenerator.generate).toHaveBeenCalledTimes(1);
|
||||
|
@ -1,5 +1,3 @@
|
||||
import type { ResourceIdentifier } from '../../../src/ldp/representation/ResourceIdentifier';
|
||||
import type { IdentifierGenerator } from '../../../src/pods/generate/IdentifierGenerator';
|
||||
import type { Resource, ResourcesGenerator } from '../../../src/pods/generate/ResourcesGenerator';
|
||||
import { GeneratedPodManager } from '../../../src/pods/GeneratedPodManager';
|
||||
import type { PodSettings } from '../../../src/pods/settings/PodSettings';
|
||||
@ -11,9 +9,6 @@ describe('A GeneratedPodManager', (): void => {
|
||||
let settings: PodSettings;
|
||||
let store: jest.Mocked<ResourceStore>;
|
||||
let generatorData: Resource[];
|
||||
const idGenerator: IdentifierGenerator = {
|
||||
generate: (slug: string): ResourceIdentifier => ({ path: `${base}${slug}/` }),
|
||||
};
|
||||
let resGenerator: ResourcesGenerator;
|
||||
let manager: GeneratedPodManager;
|
||||
|
||||
@ -37,18 +32,18 @@ describe('A GeneratedPodManager', (): void => {
|
||||
yield* generatorData;
|
||||
}),
|
||||
};
|
||||
manager = new GeneratedPodManager(store, idGenerator, resGenerator);
|
||||
manager = new GeneratedPodManager(store, resGenerator);
|
||||
});
|
||||
|
||||
it('throws an error if the generate identifier is not available.', async(): Promise<void> => {
|
||||
store.resourceExists.mockResolvedValueOnce(true);
|
||||
const result = manager.createPod(settings);
|
||||
const result = manager.createPod({ path: `${base}user/` }, settings);
|
||||
await expect(result).rejects.toThrow(`There already is a resource at ${base}user/`);
|
||||
await expect(result).rejects.toThrow(ConflictHttpError);
|
||||
});
|
||||
|
||||
it('generates an identifier and writes containers before writing the resources in them.', async(): Promise<void> => {
|
||||
await expect(manager.createPod(settings)).resolves.toEqual({ path: `${base}${settings.login}/` });
|
||||
await expect(manager.createPod({ path: `${base}${settings.login}/` }, settings)).resolves.toBeUndefined();
|
||||
|
||||
expect(store.setRepresentation).toHaveBeenCalledTimes(3);
|
||||
expect(store.setRepresentation).toHaveBeenNthCalledWith(1, { path: '/path/' }, '/');
|
||||
|
Loading…
x
Reference in New Issue
Block a user