feat: Use RequestParser and ResponseWriter for IDP

This commit is contained in:
Joachim Van Herwegen 2021-08-05 10:34:41 +02:00
parent a7a22bf43a
commit 7b7040a196
19 changed files with 264 additions and 194 deletions

View File

@ -19,10 +19,13 @@
{ {
"@id": "urn:solid-server:default:IdentityProviderHttpHandler", "@id": "urn:solid-server:default:IdentityProviderHttpHandler",
"@type": "IdentityProviderHttpHandler", "@type": "IdentityProviderHttpHandler",
"baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" },
"idpPath": "/idp", "idpPath": "/idp",
"requestParser": { "@id": "urn:solid-server:default:RequestParser" },
"providerFactory": { "@id": "urn:solid-server:default:IdentityProviderFactory" }, "providerFactory": { "@id": "urn:solid-server:default:IdentityProviderFactory" },
"templateHandler": { "templateHandler": {
"@type": "TemplateHandler", "@type": "TemplateHandler",
"responseWriter": { "@id": "urn:solid-server:default:ResponseWriter" },
"templateEngine": { "templateEngine": {
"comment": "Renders the specific page and embeds it into the main HTML body.", "comment": "Renders the specific page and embeds it into the main HTML body.",
"@type": "ChainedTemplateEngine", "@type": "ChainedTemplateEngine",
@ -35,7 +38,7 @@
{ {
"comment": "Will embed the result of the first engine into the main HTML template.", "comment": "Will embed the result of the first engine into the main HTML template.",
"@type": "EjsTemplateEngine", "@type": "EjsTemplateEngine",
"template": "@css:templates/main.html.ejs", "template": "@css:templates/main.html.ejs"
} }
] ]
} }

View File

@ -1,5 +1,9 @@
import urljoin from 'url-join';
import type { ErrorHandler } from '../ldp/http/ErrorHandler'; import type { ErrorHandler } from '../ldp/http/ErrorHandler';
import type { RequestParser } from '../ldp/http/RequestParser';
import type { ResponseWriter } from '../ldp/http/ResponseWriter'; import type { ResponseWriter } from '../ldp/http/ResponseWriter';
import type { Operation } from '../ldp/operations/Operation';
import type { RepresentationPreferences } from '../ldp/representation/RepresentationPreferences';
import { getLoggerFor } from '../logging/LogUtil'; import { getLoggerFor } from '../logging/LogUtil';
import type { HttpHandlerInput } from '../server/HttpHandler'; import type { HttpHandlerInput } from '../server/HttpHandler';
import { HttpHandler } from '../server/HttpHandler'; import { HttpHandler } from '../server/HttpHandler';
@ -65,7 +69,8 @@ export class InteractionRoute {
export class IdentityProviderHttpHandler extends HttpHandler { export class IdentityProviderHttpHandler extends HttpHandler {
protected readonly logger = getLoggerFor(this); protected readonly logger = getLoggerFor(this);
private readonly idpPath: string; private readonly baseUrl: string;
private readonly requestParser: RequestParser;
private readonly providerFactory: ProviderFactory; private readonly providerFactory: ProviderFactory;
private readonly interactionRoutes: InteractionRoute[]; private readonly interactionRoutes: InteractionRoute[];
private readonly templateHandler: TemplateHandler; private readonly templateHandler: TemplateHandler;
@ -74,7 +79,9 @@ export class IdentityProviderHttpHandler extends HttpHandler {
private readonly responseWriter: ResponseWriter; private readonly responseWriter: ResponseWriter;
/** /**
* @param baseUrl - Base URL of the server.
* @param idpPath - Relative path of the IDP entry point. * @param idpPath - Relative path of the IDP entry point.
* @param requestParser - Used for parsing requests.
* @param providerFactory - Used to generate the OIDC provider. * @param providerFactory - Used to generate the OIDC provider.
* @param interactionRoutes - All routes handling the custom IDP behaviour. * @param interactionRoutes - All routes handling the custom IDP behaviour.
* @param templateHandler - Used for rendering responses. * @param templateHandler - Used for rendering responses.
@ -83,7 +90,9 @@ export class IdentityProviderHttpHandler extends HttpHandler {
* @param responseWriter - Renders error responses. * @param responseWriter - Renders error responses.
*/ */
public constructor( public constructor(
baseUrl: string,
idpPath: string, idpPath: string,
requestParser: RequestParser,
providerFactory: ProviderFactory, providerFactory: ProviderFactory,
interactionRoutes: InteractionRoute[], interactionRoutes: InteractionRoute[],
templateHandler: TemplateHandler, templateHandler: TemplateHandler,
@ -92,11 +101,9 @@ export class IdentityProviderHttpHandler extends HttpHandler {
responseWriter: ResponseWriter, responseWriter: ResponseWriter,
) { ) {
super(); super();
if (!idpPath.startsWith('/')) {
throw new Error('idpPath needs to start with a /');
}
// Trimming trailing slashes so the relative URL starts with a slash after slicing this off // Trimming trailing slashes so the relative URL starts with a slash after slicing this off
this.idpPath = trimTrailingSlashes(idpPath); this.baseUrl = trimTrailingSlashes(urljoin(baseUrl, idpPath));
this.requestParser = requestParser;
this.providerFactory = providerFactory; this.providerFactory = providerFactory;
this.interactionRoutes = interactionRoutes; this.interactionRoutes = interactionRoutes;
this.templateHandler = templateHandler; this.templateHandler = templateHandler;
@ -106,20 +113,24 @@ export class IdentityProviderHttpHandler extends HttpHandler {
} }
public async handle({ request, response }: HttpHandlerInput): Promise<void> { public async handle({ request, response }: HttpHandlerInput): Promise<void> {
let preferences: RepresentationPreferences = { type: { 'text/plain': 1 }};
try { try {
await this.handleRequest(request, response); // It is important that this RequestParser does not read out the Request body stream.
// Otherwise we can't pass it anymore to the OIDC library when needed.
const operation = await this.requestParser.handleSafe(request);
({ preferences } = operation);
await this.handleOperation(operation, request, response);
} catch (error: unknown) { } catch (error: unknown) {
assertError(error); assertError(error);
// Setting preferences to text/plain since we didn't parse accept headers, see #764 const result = await this.errorHandler.handleSafe({ error, preferences });
const result = await this.errorHandler.handleSafe({ error, preferences: { type: { 'text/plain': 1 }}});
await this.responseWriter.handleSafe({ response, result }); await this.responseWriter.handleSafe({ response, result });
} }
} }
/** /**
* Finds the matching route and resolves the request. * Finds the matching route and resolves the operation.
*/ */
private async handleRequest(request: HttpRequest, response: HttpResponse): Promise<void> { private async handleOperation(operation: Operation, request: HttpRequest, response: HttpResponse): Promise<void> {
// This being defined means we're in an OIDC session // This being defined means we're in an OIDC session
let oidcInteraction: Interaction | undefined; let oidcInteraction: Interaction | undefined;
try { try {
@ -130,25 +141,42 @@ export class IdentityProviderHttpHandler extends HttpHandler {
} }
// If our own interaction handler does not support the input, it is either invalid or a request for the OIDC library // If our own interaction handler does not support the input, it is either invalid or a request for the OIDC library
const route = await this.findRoute(request, oidcInteraction); const route = await this.findRoute(operation, oidcInteraction);
if (!route) { if (!route) {
// Make sure the request stream still works in case the RequestParser read it
const provider = await this.providerFactory.getProvider(); const provider = await this.providerFactory.getProvider();
this.logger.debug(`Sending request to oidc-provider: ${request.url}`); this.logger.debug(`Sending request to oidc-provider: ${request.url}`);
return provider.callback(request, response); return provider.callback(request, response);
} }
await this.resolveRoute(request, response, route, oidcInteraction); const { result, templateFile } = await this.resolveRoute(operation, route, oidcInteraction);
if (result.type === 'complete') {
if (!oidcInteraction) {
// Once https://github.com/solid/community-server/pull/898 is merged
// we want to assign an error code here to have a more thorough explanation
throw new BadRequestHttpError(
'This action can only be executed as part of an authentication flow. It should not be used directly.',
);
}
// We need the original request object for the OIDC library
return await this.interactionCompleter.handleSafe({ ...result.details, request, response });
}
if (result.type === 'response' && templateFile) {
return await this.handleTemplateResponse(response, templateFile, result.details, oidcInteraction);
}
throw new BadRequestHttpError(`Unsupported request: ${operation.method} ${operation.target.path}`);
} }
/** /**
* Finds a route that supports the given request. * Finds a route that supports the given request.
*/ */
private async findRoute(request: HttpRequest, oidcInteraction?: Interaction): Promise<InteractionRoute | undefined> { private async findRoute(operation: Operation, oidcInteraction?: Interaction): Promise<InteractionRoute | undefined> {
if (!request.url || !request.url.startsWith(this.idpPath)) { if (!operation.target.path.startsWith(this.baseUrl)) {
// This is either an invalid request or a call to the .well-known configuration // This is either an invalid request or a call to the .well-known configuration
return; return;
} }
const pathName = request.url.slice(this.idpPath.length); const pathName = operation.target.path.slice(this.baseUrl.length);
let route = this.getRouteMatch(pathName); let route = this.getRouteMatch(pathName);
// In case the request targets the IDP entry point the prompt determines where to go // In case the request targets the IDP entry point the prompt determines where to go
@ -164,42 +192,32 @@ export class IdentityProviderHttpHandler extends HttpHandler {
* *
* GET requests go to the templateHandler, POST requests to the specific InteractionHandler of the route. * GET requests go to the templateHandler, POST requests to the specific InteractionHandler of the route.
*/ */
private async resolveRoute(request: HttpRequest, response: HttpResponse, route: InteractionRoute, private async resolveRoute(operation: Operation, route: InteractionRoute, oidcInteraction?: Interaction):
oidcInteraction?: Interaction): Promise<void> { Promise<{ result: InteractionHandlerResult; templateFile?: string }> {
if (request.method === 'GET') { if (operation.method === 'GET') {
return await this.handleTemplateResponse( // .ejs templates errors on undefined variables
response, route.viewTemplate, { errorMessage: '', prefilled: {}}, oidcInteraction, return {
); result: { type: 'response', details: { errorMessage: '', prefilled: {}}},
templateFile: route.viewTemplate,
};
} }
if (request.method === 'POST') { if (operation.method === 'POST') {
let result: InteractionHandlerResult;
try { try {
result = await route.handler.handleSafe({ request, oidcInteraction }); const result = await route.handler.handleSafe({ operation, oidcInteraction });
return { result, templateFile: route.responseTemplate };
} catch (error: unknown) { } catch (error: unknown) {
// Render error in the view // Render error in the view
const prefilled = IdpInteractionError.isInstance(error) ? error.prefilled : {}; const prefilled = IdpInteractionError.isInstance(error) ? error.prefilled : {};
const errorMessage = createErrorMessage(error); const errorMessage = createErrorMessage(error);
return await this.handleTemplateResponse( return {
response, route.viewTemplate, { errorMessage, prefilled }, oidcInteraction, result: { type: 'response', details: { errorMessage, prefilled }},
); templateFile: route.viewTemplate,
} };
if (result.type === 'complete') {
if (!oidcInteraction) {
// Once https://github.com/solid/community-server/pull/898 is merged
// we want to assign an error code here to have a more thorough explanation
throw new BadRequestHttpError(
'This action can only be executed as part of an authentication flow. It should not be used directly.',
);
}
return await this.interactionCompleter.handleSafe({ ...result.details, request, response });
}
if (result.type === 'response' && route.responseTemplate) {
return await this.handleTemplateResponse(response, route.responseTemplate, result.details, oidcInteraction);
} }
} }
throw new BadRequestHttpError(`Unsupported request: ${request.method} ${request.url}`);
throw new BadRequestHttpError(`Unsupported request: ${operation.method} ${operation.target.path}`);
} }
private async handleTemplateResponse(response: HttpResponse, templateFile: string, data?: NodeJS.Dict<any>, private async handleTemplateResponse(response: HttpResponse, templateFile: string, data?: NodeJS.Dict<any>,

View File

@ -7,12 +7,12 @@ import { getFormDataRequestBody } from './util/FormDataUtil';
* Simple InteractionHttpHandler that sends the session accountId to the InteractionCompleter as webId. * Simple InteractionHttpHandler that sends the session accountId to the InteractionCompleter as webId.
*/ */
export class SessionHttpHandler extends InteractionHandler { export class SessionHttpHandler extends InteractionHandler {
public async handle({ request, oidcInteraction }: InteractionHandlerInput): Promise<InteractionCompleteResult> { public async handle({ operation, oidcInteraction }: InteractionHandlerInput): Promise<InteractionCompleteResult> {
if (!oidcInteraction?.session) { if (!oidcInteraction?.session) {
throw new NotImplementedHttpError('Only interactions with a valid session are supported.'); throw new NotImplementedHttpError('Only interactions with a valid session are supported.');
} }
const { remember } = await getFormDataRequestBody(request); const { remember } = await getFormDataRequestBody(operation);
return { return {
type: 'complete', type: 'complete',
details: { webId: oidcInteraction.session.accountId, shouldRemember: Boolean(remember) }, details: { webId: oidcInteraction.session.accountId, shouldRemember: Boolean(remember) },

View File

@ -39,10 +39,10 @@ export class ForgotPasswordHandler extends InteractionHandler {
this.emailSender = args.emailSender; this.emailSender = args.emailSender;
} }
public async handle({ request }: InteractionHandlerInput): Promise<InteractionResponseResult<{ email: string }>> { public async handle({ operation }: InteractionHandlerInput): Promise<InteractionResponseResult<{ email: string }>> {
try { try {
// Validate incoming data // Validate incoming data
const { email } = await getFormDataRequestBody(request); const { email } = await getFormDataRequestBody(operation);
assert(typeof email === 'string' && email.length > 0, 'Email required'); assert(typeof email === 'string' && email.length > 0, 'Email required');
await this.resetPassword(email); await this.resetPassword(email);

View File

@ -1,5 +1,5 @@
import type { KoaContextWithOIDC } from 'oidc-provider'; import type { KoaContextWithOIDC } from 'oidc-provider';
import type { HttpRequest } from '../../../../server/HttpRequest'; import type { Operation } from '../../../../ldp/operations/Operation';
import { AsyncHandler } from '../../../../util/handlers/AsyncHandler'; import { AsyncHandler } from '../../../../util/handlers/AsyncHandler';
import type { InteractionCompleterParams } from '../../util/InteractionCompleter'; import type { InteractionCompleterParams } from '../../util/InteractionCompleter';
@ -8,9 +8,9 @@ export type Interaction = KoaContextWithOIDC['oidc']['entities']['Interaction'];
export interface InteractionHandlerInput { export interface InteractionHandlerInput {
/** /**
* The request being made. * The operation to execute
*/ */
request: HttpRequest; operation: Operation;
/** /**
* Will be defined if the OIDC library expects us to resolve an interaction it can't handle itself, * Will be defined if the OIDC library expects us to resolve an interaction it can't handle itself,
* such as logging a user in. * such as logging a user in.

View File

@ -1,6 +1,6 @@
import assert from 'assert'; import assert from 'assert';
import type { Operation } from '../../../../ldp/operations/Operation';
import { getLoggerFor } from '../../../../logging/LogUtil'; import { getLoggerFor } from '../../../../logging/LogUtil';
import type { HttpRequest } from '../../../../server/HttpRequest';
import { getFormDataRequestBody } from '../../util/FormDataUtil'; import { getFormDataRequestBody } from '../../util/FormDataUtil';
import { throwIdpInteractionError } from '../EmailPasswordUtil'; import { throwIdpInteractionError } from '../EmailPasswordUtil';
import type { AccountStore } from '../storage/AccountStore'; import type { AccountStore } from '../storage/AccountStore';
@ -20,8 +20,8 @@ export class LoginHandler extends InteractionHandler {
this.accountStore = accountStore; this.accountStore = accountStore;
} }
public async handle({ request }: InteractionHandlerInput): Promise<InteractionCompleteResult> { public async handle({ operation }: InteractionHandlerInput): Promise<InteractionCompleteResult> {
const { email, password, remember } = await this.parseInput(request); const { email, password, remember } = await this.parseInput(operation);
try { try {
// Try to log in, will error if email/password combination is invalid // Try to log in, will error if email/password combination is invalid
const webId = await this.accountStore.authenticate(email, password); const webId = await this.accountStore.authenticate(email, password);
@ -40,10 +40,10 @@ export class LoginHandler extends InteractionHandler {
* Will throw an {@link IdpInteractionError} in case something is wrong. * Will throw an {@link IdpInteractionError} in case something is wrong.
* All relevant data that was correct up to that point will be prefilled. * All relevant data that was correct up to that point will be prefilled.
*/ */
private async parseInput(request: HttpRequest): Promise<{ email: string; password: string; remember: boolean }> { private async parseInput(operation: Operation): Promise<{ email: string; password: string; remember: boolean }> {
const prefilled: Record<string, string> = {}; const prefilled: Record<string, string> = {};
try { try {
const { email, password, remember } = await getFormDataRequestBody(request); const { email, password, remember } = await getFormDataRequestBody(operation);
assert(typeof email === 'string' && email.length > 0, 'Email required'); assert(typeof email === 'string' && email.length > 0, 'Email required');
prefilled.email = email; prefilled.email = email;
assert(typeof password === 'string' && password.length > 0, 'Password required'); assert(typeof password === 'string' && password.length > 0, 'Password required');

View File

@ -1,11 +1,11 @@
import assert from 'assert'; import assert from 'assert';
import urljoin from 'url-join'; import urljoin from 'url-join';
import type { Operation } from '../../../../ldp/operations/Operation';
import type { ResourceIdentifier } from '../../../../ldp/representation/ResourceIdentifier'; import type { ResourceIdentifier } from '../../../../ldp/representation/ResourceIdentifier';
import { getLoggerFor } from '../../../../logging/LogUtil'; import { getLoggerFor } from '../../../../logging/LogUtil';
import type { IdentifierGenerator } from '../../../../pods/generate/IdentifierGenerator'; import type { IdentifierGenerator } from '../../../../pods/generate/IdentifierGenerator';
import type { PodManager } from '../../../../pods/PodManager'; import type { PodManager } from '../../../../pods/PodManager';
import type { PodSettings } from '../../../../pods/settings/PodSettings'; import type { PodSettings } from '../../../../pods/settings/PodSettings';
import type { HttpRequest } from '../../../../server/HttpRequest';
import type { OwnershipValidator } from '../../../ownership/OwnershipValidator'; import type { OwnershipValidator } from '../../../ownership/OwnershipValidator';
import { getFormDataRequestBody } from '../../util/FormDataUtil'; import { getFormDataRequestBody } from '../../util/FormDataUtil';
import { assertPassword, throwIdpInteractionError } from '../EmailPasswordUtil'; import { assertPassword, throwIdpInteractionError } from '../EmailPasswordUtil';
@ -102,8 +102,9 @@ export class RegistrationHandler extends InteractionHandler {
this.podManager = args.podManager; this.podManager = args.podManager;
} }
public async handle({ request }: InteractionHandlerInput): Promise<InteractionResponseResult<RegistrationResponse>> { public async handle({ operation }: InteractionHandlerInput):
const result = await this.parseInput(request); Promise<InteractionResponseResult<RegistrationResponse>> {
const result = await this.parseInput(operation);
try { try {
const details = await this.register(result); const details = await this.register(result);
@ -184,8 +185,8 @@ export class RegistrationHandler extends InteractionHandler {
/** /**
* Parses the input request into a `ParseResult`. * Parses the input request into a `ParseResult`.
*/ */
private async parseInput(request: HttpRequest): Promise<ParsedInput> { private async parseInput(operation: Operation): Promise<ParsedInput> {
const parsed = await getFormDataRequestBody(request); const parsed = await getFormDataRequestBody(operation);
const prefilled: Record<string, string> = {}; const prefilled: Record<string, string> = {};
try { try {
for (const [ key, value ] of Object.entries(parsed)) { for (const [ key, value ] of Object.entries(parsed)) {

View File

@ -20,12 +20,12 @@ export class ResetPasswordHandler extends InteractionHandler {
this.accountStore = accountStore; this.accountStore = accountStore;
} }
public async handle({ request }: InteractionHandlerInput): Promise<InteractionResponseResult> { public async handle({ operation }: InteractionHandlerInput): Promise<InteractionResponseResult> {
try { try {
// Extract record ID from request URL // Extract record ID from request URL
const recordId = /\/([^/]+)$/u.exec(request.url!)?.[1]; const recordId = /\/([^/]+)$/u.exec(operation.target.path)?.[1];
// Validate input data // Validate input data
const { password, confirmPassword } = await getFormDataRequestBody(request); const { password, confirmPassword } = await getFormDataRequestBody(operation);
assert( assert(
typeof recordId === 'string' && recordId.length > 0, typeof recordId === 'string' && recordId.length > 0,
'Invalid request. Open the link from your email again', 'Invalid request. Open the link from your email again',

View File

@ -1,17 +1,17 @@
import type { ParsedUrlQuery } from 'querystring'; import type { ParsedUrlQuery } from 'querystring';
import { parse } from 'querystring'; import { parse } from 'querystring';
import type { HttpRequest } from '../../../server/HttpRequest'; import type { Operation } from '../../../ldp/operations/Operation';
import { APPLICATION_X_WWW_FORM_URLENCODED } from '../../../util/ContentTypes'; import { APPLICATION_X_WWW_FORM_URLENCODED } from '../../../util/ContentTypes';
import { UnsupportedMediaTypeHttpError } from '../../../util/errors/UnsupportedMediaTypeHttpError'; import { UnsupportedMediaTypeHttpError } from '../../../util/errors/UnsupportedMediaTypeHttpError';
import { readableToString } from '../../../util/StreamUtil'; import { readableToString } from '../../../util/StreamUtil';
/** /**
* Takes in a request and parses its body as 'application/x-www-form-urlencoded' * Takes in an operation and parses its body as 'application/x-www-form-urlencoded'
*/ */
export async function getFormDataRequestBody(request: HttpRequest): Promise<ParsedUrlQuery> { export async function getFormDataRequestBody(operation: Operation): Promise<ParsedUrlQuery> {
if (request.headers['content-type'] !== APPLICATION_X_WWW_FORM_URLENCODED) { if (operation.body?.metadata.contentType !== APPLICATION_X_WWW_FORM_URLENCODED) {
throw new UnsupportedMediaTypeHttpError(); throw new UnsupportedMediaTypeHttpError();
} }
const body = await readableToString(request); const body = await readableToString(operation.body.data);
return parse(body); return parse(body);
} }

View File

@ -1,4 +1,8 @@
import type { ResponseDescription } from '../../ldp/http/response/ResponseDescription';
import type { ResponseWriter } from '../../ldp/http/ResponseWriter';
import { RepresentationMetadata } from '../../ldp/representation/RepresentationMetadata';
import { AsyncHandler } from '../../util/handlers/AsyncHandler'; import { AsyncHandler } from '../../util/handlers/AsyncHandler';
import { guardedStreamFrom } from '../../util/StreamUtil';
import type { TemplateEngine } from '../../util/templates/TemplateEngine'; import type { TemplateEngine } from '../../util/templates/TemplateEngine';
import type { HttpResponse } from '../HttpResponse'; import type { HttpResponse } from '../HttpResponse';
import Dict = NodeJS.Dict; import Dict = NodeJS.Dict;
@ -8,11 +12,13 @@ import Dict = NodeJS.Dict;
*/ */
export class TemplateHandler<T extends Dict<any> = Dict<any>> export class TemplateHandler<T extends Dict<any> = Dict<any>>
extends AsyncHandler<{ response: HttpResponse; templateFile: string; contents: T }> { extends AsyncHandler<{ response: HttpResponse; templateFile: string; contents: T }> {
private readonly responseWriter: ResponseWriter;
private readonly templateEngine: TemplateEngine; private readonly templateEngine: TemplateEngine;
private readonly contentType: string; private readonly contentType: string;
public constructor(templateEngine: TemplateEngine, contentType = 'text/html') { public constructor(responseWriter: ResponseWriter, templateEngine: TemplateEngine, contentType = 'text/html') {
super(); super();
this.responseWriter = responseWriter;
this.templateEngine = templateEngine; this.templateEngine = templateEngine;
this.contentType = contentType; this.contentType = contentType;
} }
@ -20,8 +26,11 @@ export class TemplateHandler<T extends Dict<any> = Dict<any>>
public async handle({ response, templateFile, contents }: public async handle({ response, templateFile, contents }:
{ response: HttpResponse; templateFile: string; contents: T }): Promise<void> { { response: HttpResponse; templateFile: string; contents: T }): Promise<void> {
const rendered = await this.templateEngine.render(contents, { templateFile }); const rendered = await this.templateEngine.render(contents, { templateFile });
// eslint-disable-next-line @typescript-eslint/naming-convention const result: ResponseDescription = {
response.writeHead(200, { 'Content-Type': this.contentType }); statusCode: 200,
response.end(rendered); data: guardedStreamFrom(rendered),
metadata: new RepresentationMetadata(this.contentType),
};
await this.responseWriter.handleSafe({ response, result });
} }
} }

View File

@ -1,11 +1,15 @@
import type { Provider } from 'oidc-provider'; import type { Provider } from 'oidc-provider';
import urljoin from 'url-join';
import type { ProviderFactory } from '../../../src/identity/configuration/ProviderFactory'; import type { ProviderFactory } from '../../../src/identity/configuration/ProviderFactory';
import { InteractionRoute, IdentityProviderHttpHandler } from '../../../src/identity/IdentityProviderHttpHandler'; import { InteractionRoute, IdentityProviderHttpHandler } from '../../../src/identity/IdentityProviderHttpHandler';
import type { InteractionHandler } from '../../../src/identity/interaction/email-password/handler/InteractionHandler'; import type { InteractionHandler } from '../../../src/identity/interaction/email-password/handler/InteractionHandler';
import { IdpInteractionError } from '../../../src/identity/interaction/util/IdpInteractionError'; import { IdpInteractionError } from '../../../src/identity/interaction/util/IdpInteractionError';
import type { InteractionCompleter } from '../../../src/identity/interaction/util/InteractionCompleter'; import type { InteractionCompleter } from '../../../src/identity/interaction/util/InteractionCompleter';
import type { ErrorHandler } from '../../../src/ldp/http/ErrorHandler'; import type { ErrorHandler } from '../../../src/ldp/http/ErrorHandler';
import type { RequestParser } from '../../../src/ldp/http/RequestParser';
import type { ResponseWriter } from '../../../src/ldp/http/ResponseWriter'; import type { ResponseWriter } from '../../../src/ldp/http/ResponseWriter';
import type { Operation } from '../../../src/ldp/operations/Operation';
import { BasicRepresentation } from '../../../src/ldp/representation/BasicRepresentation';
import type { HttpRequest } from '../../../src/server/HttpRequest'; import type { HttpRequest } from '../../../src/server/HttpRequest';
import type { HttpResponse } from '../../../src/server/HttpResponse'; import type { HttpResponse } from '../../../src/server/HttpResponse';
import type { TemplateHandler } from '../../../src/server/util/TemplateHandler'; import type { TemplateHandler } from '../../../src/server/util/TemplateHandler';
@ -13,9 +17,11 @@ import { BadRequestHttpError } from '../../../src/util/errors/BadRequestHttpErro
import { InternalServerError } from '../../../src/util/errors/InternalServerError'; import { InternalServerError } from '../../../src/util/errors/InternalServerError';
describe('An IdentityProviderHttpHandler', (): void => { describe('An IdentityProviderHttpHandler', (): void => {
const baseUrl = 'http://test.com/';
const idpPath = '/idp'; const idpPath = '/idp';
let request: HttpRequest; let request: HttpRequest;
const response: HttpResponse = {} as any; const response: HttpResponse = {} as any;
let requestParser: jest.Mocked<RequestParser>;
let providerFactory: jest.Mocked<ProviderFactory>; let providerFactory: jest.Mocked<ProviderFactory>;
let routes: { response: InteractionRoute; complete: InteractionRoute }; let routes: { response: InteractionRoute; complete: InteractionRoute };
let interactionCompleter: jest.Mocked<InteractionCompleter>; let interactionCompleter: jest.Mocked<InteractionCompleter>;
@ -26,7 +32,16 @@ describe('An IdentityProviderHttpHandler', (): void => {
let handler: IdentityProviderHttpHandler; let handler: IdentityProviderHttpHandler;
beforeEach(async(): Promise<void> => { beforeEach(async(): Promise<void> => {
request = { url: '/idp', method: 'GET' } as any; request = { url: '/idp', method: 'GET', headers: {}} as any;
requestParser = {
handleSafe: jest.fn(async(req: HttpRequest): Promise<Operation> => ({
target: { path: urljoin(baseUrl, req.url!) },
method: req.method!,
body: new BasicRepresentation('', req.headers['content-type'] ?? 'text/plain'),
preferences: { type: { 'text/html': 1 }},
})),
} as any;
provider = { provider = {
callback: jest.fn(), callback: jest.fn(),
@ -56,7 +71,9 @@ describe('An IdentityProviderHttpHandler', (): void => {
responseWriter = { handleSafe: jest.fn() } as any; responseWriter = { handleSafe: jest.fn() } as any;
handler = new IdentityProviderHttpHandler( handler = new IdentityProviderHttpHandler(
baseUrl,
idpPath, idpPath,
requestParser,
providerFactory, providerFactory,
Object.values(routes), Object.values(routes),
templateHandler, templateHandler,
@ -66,12 +83,6 @@ describe('An IdentityProviderHttpHandler', (): void => {
); );
}); });
it('errors if the idpPath does not start with a slash.', async(): Promise<void> => {
expect((): any => new IdentityProviderHttpHandler(
'idp', providerFactory, [], templateHandler, interactionCompleter, errorHandler, responseWriter,
)).toThrow('idpPath needs to start with a /');
});
it('calls the provider if there is no matching route.', async(): Promise<void> => { it('calls the provider if there is no matching route.', async(): Promise<void> => {
request.url = 'invalid'; request.url = 'invalid';
await expect(handler.handle({ request, response })).resolves.toBeUndefined(); await expect(handler.handle({ request, response })).resolves.toBeUndefined();
@ -92,8 +103,9 @@ describe('An IdentityProviderHttpHandler', (): void => {
request.url = '/idp/routeResponse'; request.url = '/idp/routeResponse';
request.method = 'POST'; request.method = 'POST';
await expect(handler.handle({ request, response })).resolves.toBeUndefined(); await expect(handler.handle({ request, response })).resolves.toBeUndefined();
const operation = await requestParser.handleSafe.mock.results[0].value;
expect(routes.response.handler.handleSafe).toHaveBeenCalledTimes(1); expect(routes.response.handler.handleSafe).toHaveBeenCalledTimes(1);
expect(routes.response.handler.handleSafe).toHaveBeenLastCalledWith({ request }); expect(routes.response.handler.handleSafe).toHaveBeenLastCalledWith({ operation });
expect(templateHandler.handleSafe).toHaveBeenCalledTimes(1); expect(templateHandler.handleSafe).toHaveBeenCalledTimes(1);
expect(templateHandler.handleSafe).toHaveBeenLastCalledWith( expect(templateHandler.handleSafe).toHaveBeenLastCalledWith(
{ response, templateFile: routes.response.responseTemplate, contents: { key: 'val', authenticating: false }}, { response, templateFile: routes.response.responseTemplate, contents: { key: 'val', authenticating: false }},
@ -107,8 +119,9 @@ describe('An IdentityProviderHttpHandler', (): void => {
provider.interactionDetails.mockResolvedValueOnce(oidcInteraction); provider.interactionDetails.mockResolvedValueOnce(oidcInteraction);
(routes.response.handler as jest.Mocked<InteractionHandler>).handleSafe.mockResolvedValueOnce({ type: 'response' }); (routes.response.handler as jest.Mocked<InteractionHandler>).handleSafe.mockResolvedValueOnce({ type: 'response' });
await expect(handler.handle({ request, response })).resolves.toBeUndefined(); await expect(handler.handle({ request, response })).resolves.toBeUndefined();
const operation = await requestParser.handleSafe.mock.results[0].value;
expect(routes.response.handler.handleSafe).toHaveBeenCalledTimes(1); expect(routes.response.handler.handleSafe).toHaveBeenCalledTimes(1);
expect(routes.response.handler.handleSafe).toHaveBeenLastCalledWith({ request, oidcInteraction }); expect(routes.response.handler.handleSafe).toHaveBeenLastCalledWith({ operation, oidcInteraction });
expect(templateHandler.handleSafe).toHaveBeenCalledTimes(1); expect(templateHandler.handleSafe).toHaveBeenCalledTimes(1);
expect(templateHandler.handleSafe).toHaveBeenLastCalledWith( expect(templateHandler.handleSafe).toHaveBeenLastCalledWith(
{ response, templateFile: routes.response.responseTemplate, contents: { authenticating: true }}, { response, templateFile: routes.response.responseTemplate, contents: { authenticating: true }},
@ -120,15 +133,16 @@ describe('An IdentityProviderHttpHandler', (): void => {
request.method = 'POST'; request.method = 'POST';
errorHandler.handleSafe.mockResolvedValueOnce({ statusCode: 400 }); errorHandler.handleSafe.mockResolvedValueOnce({ statusCode: 400 });
await expect(handler.handle({ request, response })).resolves.toBeUndefined(); await expect(handler.handle({ request, response })).resolves.toBeUndefined();
const operation = await requestParser.handleSafe.mock.results[0].value;
expect(routes.complete.handler.handleSafe).toHaveBeenCalledTimes(1); expect(routes.complete.handler.handleSafe).toHaveBeenCalledTimes(1);
expect(routes.complete.handler.handleSafe).toHaveBeenLastCalledWith({ request }); expect(routes.complete.handler.handleSafe).toHaveBeenLastCalledWith({ operation });
expect(interactionCompleter.handleSafe).toHaveBeenCalledTimes(0); expect(interactionCompleter.handleSafe).toHaveBeenCalledTimes(0);
const error = new BadRequestHttpError( const error = new BadRequestHttpError(
'This action can only be executed as part of an authentication flow. It should not be used directly.', 'This action can only be executed as part of an authentication flow. It should not be used directly.',
); );
expect(errorHandler.handleSafe).toHaveBeenCalledTimes(1); expect(errorHandler.handleSafe).toHaveBeenCalledTimes(1);
expect(errorHandler.handleSafe).toHaveBeenLastCalledWith({ error, preferences: { type: { 'text/plain': 1 }}}); expect(errorHandler.handleSafe).toHaveBeenLastCalledWith({ error, preferences: { type: { 'text/html': 1 }}});
expect(responseWriter.handleSafe).toHaveBeenCalledTimes(1); expect(responseWriter.handleSafe).toHaveBeenCalledTimes(1);
expect(responseWriter.handleSafe).toHaveBeenLastCalledWith({ response, result: { statusCode: 400 }}); expect(responseWriter.handleSafe).toHaveBeenLastCalledWith({ response, result: { statusCode: 400 }});
}); });
@ -139,8 +153,9 @@ describe('An IdentityProviderHttpHandler', (): void => {
const oidcInteraction = { session: { accountId: 'account' }} as any; const oidcInteraction = { session: { accountId: 'account' }} as any;
provider.interactionDetails.mockResolvedValueOnce(oidcInteraction); provider.interactionDetails.mockResolvedValueOnce(oidcInteraction);
await expect(handler.handle({ request, response })).resolves.toBeUndefined(); await expect(handler.handle({ request, response })).resolves.toBeUndefined();
const operation = await requestParser.handleSafe.mock.results[0].value;
expect(routes.complete.handler.handleSafe).toHaveBeenCalledTimes(1); expect(routes.complete.handler.handleSafe).toHaveBeenCalledTimes(1);
expect(routes.complete.handler.handleSafe).toHaveBeenLastCalledWith({ request, oidcInteraction }); expect(routes.complete.handler.handleSafe).toHaveBeenLastCalledWith({ operation, oidcInteraction });
expect(interactionCompleter.handleSafe).toHaveBeenCalledTimes(1); expect(interactionCompleter.handleSafe).toHaveBeenCalledTimes(1);
expect(interactionCompleter.handleSafe).toHaveBeenLastCalledWith({ request, response, webId: 'webId' }); expect(interactionCompleter.handleSafe).toHaveBeenLastCalledWith({ request, response, webId: 'webId' });
}); });
@ -151,9 +166,10 @@ describe('An IdentityProviderHttpHandler', (): void => {
const oidcInteraction = { prompt: { name: 'other' }}; const oidcInteraction = { prompt: { name: 'other' }};
provider.interactionDetails.mockResolvedValueOnce(oidcInteraction as any); provider.interactionDetails.mockResolvedValueOnce(oidcInteraction as any);
await expect(handler.handle({ request, response })).resolves.toBeUndefined(); await expect(handler.handle({ request, response })).resolves.toBeUndefined();
const operation = await requestParser.handleSafe.mock.results[0].value;
expect(routes.response.handler.handleSafe).toHaveBeenCalledTimes(0); expect(routes.response.handler.handleSafe).toHaveBeenCalledTimes(0);
expect(routes.complete.handler.handleSafe).toHaveBeenCalledTimes(1); expect(routes.complete.handler.handleSafe).toHaveBeenCalledTimes(1);
expect(routes.complete.handler.handleSafe).toHaveBeenLastCalledWith({ request, oidcInteraction }); expect(routes.complete.handler.handleSafe).toHaveBeenLastCalledWith({ operation, oidcInteraction });
}); });
it('uses the default route for requests to the root IDP without (matching) prompt.', async(): Promise<void> => { it('uses the default route for requests to the root IDP without (matching) prompt.', async(): Promise<void> => {
@ -162,8 +178,9 @@ describe('An IdentityProviderHttpHandler', (): void => {
const oidcInteraction = { prompt: { name: 'notSupported' }}; const oidcInteraction = { prompt: { name: 'notSupported' }};
provider.interactionDetails.mockResolvedValueOnce(oidcInteraction as any); provider.interactionDetails.mockResolvedValueOnce(oidcInteraction as any);
await expect(handler.handle({ request, response })).resolves.toBeUndefined(); await expect(handler.handle({ request, response })).resolves.toBeUndefined();
const operation = await requestParser.handleSafe.mock.results[0].value;
expect(routes.response.handler.handleSafe).toHaveBeenCalledTimes(1); expect(routes.response.handler.handleSafe).toHaveBeenCalledTimes(1);
expect(routes.response.handler.handleSafe).toHaveBeenLastCalledWith({ request, oidcInteraction }); expect(routes.response.handler.handleSafe).toHaveBeenLastCalledWith({ operation, oidcInteraction });
expect(routes.complete.handler.handleSafe).toHaveBeenCalledTimes(0); expect(routes.complete.handler.handleSafe).toHaveBeenCalledTimes(0);
}); });
@ -203,7 +220,7 @@ describe('An IdentityProviderHttpHandler', (): void => {
errorHandler.handleSafe.mockResolvedValueOnce({ statusCode: 500 }); errorHandler.handleSafe.mockResolvedValueOnce({ statusCode: 500 });
await expect(handler.handle({ request, response })).resolves.toBeUndefined(); await expect(handler.handle({ request, response })).resolves.toBeUndefined();
expect(errorHandler.handleSafe).toHaveBeenCalledTimes(1); expect(errorHandler.handleSafe).toHaveBeenCalledTimes(1);
expect(errorHandler.handleSafe).toHaveBeenLastCalledWith({ error, preferences: { type: { 'text/plain': 1 }}}); expect(errorHandler.handleSafe).toHaveBeenLastCalledWith({ error, preferences: { type: { 'text/html': 1 }}});
expect(responseWriter.handleSafe).toHaveBeenCalledTimes(1); expect(responseWriter.handleSafe).toHaveBeenCalledTimes(1);
expect(responseWriter.handleSafe).toHaveBeenLastCalledWith({ response, result: { statusCode: 500 }}); expect(responseWriter.handleSafe).toHaveBeenLastCalledWith({ response, result: { statusCode: 500 }});
}); });
@ -211,11 +228,11 @@ describe('An IdentityProviderHttpHandler', (): void => {
it('can only resolve GET/POST requests.', async(): Promise<void> => { it('can only resolve GET/POST requests.', async(): Promise<void> => {
request.url = '/idp/routeResponse'; request.url = '/idp/routeResponse';
request.method = 'DELETE'; request.method = 'DELETE';
const error = new BadRequestHttpError('Unsupported request: DELETE /idp/routeResponse'); const error = new BadRequestHttpError('Unsupported request: DELETE http://test.com/idp/routeResponse');
errorHandler.handleSafe.mockResolvedValueOnce({ statusCode: 500 }); errorHandler.handleSafe.mockResolvedValueOnce({ statusCode: 500 });
await expect(handler.handle({ request, response })).resolves.toBeUndefined(); await expect(handler.handle({ request, response })).resolves.toBeUndefined();
expect(errorHandler.handleSafe).toHaveBeenCalledTimes(1); expect(errorHandler.handleSafe).toHaveBeenCalledTimes(1);
expect(errorHandler.handleSafe).toHaveBeenLastCalledWith({ error, preferences: { type: { 'text/plain': 1 }}}); expect(errorHandler.handleSafe).toHaveBeenLastCalledWith({ error, preferences: { type: { 'text/html': 1 }}});
expect(responseWriter.handleSafe).toHaveBeenCalledTimes(1); expect(responseWriter.handleSafe).toHaveBeenCalledTimes(1);
expect(responseWriter.handleSafe).toHaveBeenLastCalledWith({ response, result: { statusCode: 500 }}); expect(responseWriter.handleSafe).toHaveBeenLastCalledWith({ response, result: { statusCode: 500 }});
}); });
@ -224,18 +241,26 @@ describe('An IdentityProviderHttpHandler', (): void => {
request.url = '/idp/routeResponse'; request.url = '/idp/routeResponse';
request.method = 'POST'; request.method = 'POST';
(routes.response as any).responseTemplate = undefined; (routes.response as any).responseTemplate = undefined;
const error = new BadRequestHttpError('Unsupported request: POST /idp/routeResponse'); const error = new BadRequestHttpError('Unsupported request: POST http://test.com/idp/routeResponse');
errorHandler.handleSafe.mockResolvedValueOnce({ statusCode: 500 }); errorHandler.handleSafe.mockResolvedValueOnce({ statusCode: 500 });
await expect(handler.handle({ request, response })).resolves.toBeUndefined(); await expect(handler.handle({ request, response })).resolves.toBeUndefined();
expect(errorHandler.handleSafe).toHaveBeenCalledTimes(1); expect(errorHandler.handleSafe).toHaveBeenCalledTimes(1);
expect(errorHandler.handleSafe).toHaveBeenLastCalledWith({ error, preferences: { type: { 'text/plain': 1 }}}); expect(errorHandler.handleSafe).toHaveBeenLastCalledWith({ error, preferences: { type: { 'text/html': 1 }}});
expect(responseWriter.handleSafe).toHaveBeenCalledTimes(1); expect(responseWriter.handleSafe).toHaveBeenCalledTimes(1);
expect(responseWriter.handleSafe).toHaveBeenLastCalledWith({ response, result: { statusCode: 500 }}); expect(responseWriter.handleSafe).toHaveBeenLastCalledWith({ response, result: { statusCode: 500 }});
}); });
it('errors if no route is configured for the default prompt.', async(): Promise<void> => { it('errors if no route is configured for the default prompt.', async(): Promise<void> => {
handler = new IdentityProviderHttpHandler( handler = new IdentityProviderHttpHandler(
idpPath, providerFactory, [], templateHandler, interactionCompleter, errorHandler, responseWriter, baseUrl,
idpPath,
requestParser,
providerFactory,
[],
templateHandler,
interactionCompleter,
errorHandler,
responseWriter,
); );
request.url = '/idp'; request.url = '/idp';
provider.interactionDetails.mockResolvedValueOnce({ prompt: { name: 'other' }} as any); provider.interactionDetails.mockResolvedValueOnce({ prompt: { name: 'other' }} as any);
@ -243,7 +268,7 @@ describe('An IdentityProviderHttpHandler', (): void => {
errorHandler.handleSafe.mockResolvedValueOnce({ statusCode: 500 }); errorHandler.handleSafe.mockResolvedValueOnce({ statusCode: 500 });
await expect(handler.handle({ request, response })).resolves.toBeUndefined(); await expect(handler.handle({ request, response })).resolves.toBeUndefined();
expect(errorHandler.handleSafe).toHaveBeenCalledTimes(1); expect(errorHandler.handleSafe).toHaveBeenCalledTimes(1);
expect(errorHandler.handleSafe).toHaveBeenLastCalledWith({ error, preferences: { type: { 'text/plain': 1 }}}); expect(errorHandler.handleSafe).toHaveBeenLastCalledWith({ error, preferences: { type: { 'text/html': 1 }}});
expect(responseWriter.handleSafe).toHaveBeenCalledTimes(1); expect(responseWriter.handleSafe).toHaveBeenCalledTimes(1);
expect(responseWriter.handleSafe).toHaveBeenLastCalledWith({ response, result: { statusCode: 500 }}); expect(responseWriter.handleSafe).toHaveBeenLastCalledWith({ response, result: { statusCode: 500 }});
}); });

View File

@ -1,7 +1,7 @@
import type { Interaction } from '../../../../src/identity/interaction/email-password/handler/InteractionHandler'; import type { Interaction } from '../../../../src/identity/interaction/email-password/handler/InteractionHandler';
import { SessionHttpHandler } from '../../../../src/identity/interaction/SessionHttpHandler'; import { SessionHttpHandler } from '../../../../src/identity/interaction/SessionHttpHandler';
import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError'; import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError';
import { createPostFormRequest } from './email-password/handler/Util'; import { createPostFormOperation } from './email-password/handler/Util';
describe('A SessionHttpHandler', (): void => { describe('A SessionHttpHandler', (): void => {
const webId = 'http://test.com/id#me'; const webId = 'http://test.com/id#me';
@ -16,14 +16,14 @@ describe('A SessionHttpHandler', (): void => {
it('requires a defined oidcInteraction with a session.', async(): Promise<void> => { it('requires a defined oidcInteraction with a session.', async(): Promise<void> => {
oidcInteraction!.session = undefined; oidcInteraction!.session = undefined;
await expect(handler.handle({ request: {} as any, oidcInteraction })).rejects.toThrow(NotImplementedHttpError); await expect(handler.handle({ operation: {} as any, oidcInteraction })).rejects.toThrow(NotImplementedHttpError);
await expect(handler.handle({ request: {} as any })).rejects.toThrow(NotImplementedHttpError); await expect(handler.handle({ operation: {} as any })).rejects.toThrow(NotImplementedHttpError);
}); });
it('returns an InteractionCompleteResult when done.', async(): Promise<void> => { it('returns an InteractionCompleteResult when done.', async(): Promise<void> => {
const request = createPostFormRequest({ remember: true }); const operation = createPostFormOperation({ remember: true });
await expect(handler.handle({ request, oidcInteraction })).resolves.toEqual({ await expect(handler.handle({ operation, oidcInteraction })).resolves.toEqual({
details: { webId, shouldRemember: true }, details: { webId, shouldRemember: true },
type: 'complete', type: 'complete',
}); });

View File

@ -3,12 +3,12 @@ import {
} from '../../../../../../src/identity/interaction/email-password/handler/ForgotPasswordHandler'; } from '../../../../../../src/identity/interaction/email-password/handler/ForgotPasswordHandler';
import type { AccountStore } from '../../../../../../src/identity/interaction/email-password/storage/AccountStore'; import type { AccountStore } from '../../../../../../src/identity/interaction/email-password/storage/AccountStore';
import type { EmailSender } from '../../../../../../src/identity/interaction/util/EmailSender'; import type { EmailSender } from '../../../../../../src/identity/interaction/util/EmailSender';
import type { HttpRequest } from '../../../../../../src/server/HttpRequest'; import type { Operation } from '../../../../../../src/ldp/operations/Operation';
import type { TemplateEngine } from '../../../../../../src/util/templates/TemplateEngine'; import type { TemplateEngine } from '../../../../../../src/util/templates/TemplateEngine';
import { createPostFormRequest } from './Util'; import { createPostFormOperation } from './Util';
describe('A ForgotPasswordHandler', (): void => { describe('A ForgotPasswordHandler', (): void => {
let request: HttpRequest; let operation: Operation;
const email = 'test@test.email'; const email = 'test@test.email';
const recordId = '123456'; const recordId = '123456';
const html = `<a href="/base/idp/resetpassword/${recordId}">Reset Password</a>`; const html = `<a href="/base/idp/resetpassword/${recordId}">Reset Password</a>`;
@ -20,7 +20,7 @@ describe('A ForgotPasswordHandler', (): void => {
let handler: ForgotPasswordHandler; let handler: ForgotPasswordHandler;
beforeEach(async(): Promise<void> => { beforeEach(async(): Promise<void> => {
request = createPostFormRequest({ email }); operation = createPostFormOperation({ email });
accountStore = { accountStore = {
generateForgotPasswordRecord: jest.fn().mockResolvedValue(recordId), generateForgotPasswordRecord: jest.fn().mockResolvedValue(recordId),
@ -44,21 +44,21 @@ describe('A ForgotPasswordHandler', (): void => {
}); });
it('errors on non-string emails.', async(): Promise<void> => { it('errors on non-string emails.', async(): Promise<void> => {
request = createPostFormRequest({}); operation = createPostFormOperation({});
await expect(handler.handle({ request })).rejects.toThrow('Email required'); await expect(handler.handle({ operation })).rejects.toThrow('Email required');
request = createPostFormRequest({ email: [ 'email', 'email2' ]}); operation = createPostFormOperation({ email: [ 'email', 'email2' ]});
await expect(handler.handle({ request })).rejects.toThrow('Email required'); await expect(handler.handle({ operation })).rejects.toThrow('Email required');
}); });
it('does not send a mail if a ForgotPassword record could not be generated.', async(): Promise<void> => { it('does not send a mail if a ForgotPassword record could not be generated.', async(): Promise<void> => {
(accountStore.generateForgotPasswordRecord as jest.Mock).mockRejectedValueOnce('error'); (accountStore.generateForgotPasswordRecord as jest.Mock).mockRejectedValueOnce('error');
await expect(handler.handle({ request })).resolves await expect(handler.handle({ operation })).resolves
.toEqual({ type: 'response', details: { email }}); .toEqual({ type: 'response', details: { email }});
expect(emailSender.handleSafe).toHaveBeenCalledTimes(0); expect(emailSender.handleSafe).toHaveBeenCalledTimes(0);
}); });
it('sends a mail if a ForgotPassword record could be generated.', async(): Promise<void> => { it('sends a mail if a ForgotPassword record could be generated.', async(): Promise<void> => {
await expect(handler.handle({ request })).resolves await expect(handler.handle({ operation })).resolves
.toEqual({ type: 'response', details: { email }}); .toEqual({ type: 'response', details: { email }});
expect(emailSender.handleSafe).toHaveBeenCalledTimes(1); expect(emailSender.handleSafe).toHaveBeenCalledTimes(1);
expect(emailSender.handleSafe).toHaveBeenLastCalledWith({ expect(emailSender.handleSafe).toHaveBeenLastCalledWith({

View File

@ -3,7 +3,7 @@ import type {
} from '../../../../../../src/identity/interaction/email-password/handler/InteractionHandler'; } from '../../../../../../src/identity/interaction/email-password/handler/InteractionHandler';
import { LoginHandler } from '../../../../../../src/identity/interaction/email-password/handler/LoginHandler'; import { LoginHandler } from '../../../../../../src/identity/interaction/email-password/handler/LoginHandler';
import type { AccountStore } from '../../../../../../src/identity/interaction/email-password/storage/AccountStore'; import type { AccountStore } from '../../../../../../src/identity/interaction/email-password/storage/AccountStore';
import { createPostFormRequest } from './Util'; import { createPostFormOperation } from './Util';
describe('A LoginHandler', (): void => { describe('A LoginHandler', (): void => {
const webId = 'http://alice.test.com/card#me'; const webId = 'http://alice.test.com/card#me';
@ -23,29 +23,29 @@ describe('A LoginHandler', (): void => {
}); });
it('errors on invalid emails.', async(): Promise<void> => { it('errors on invalid emails.', async(): Promise<void> => {
input.request = createPostFormRequest({}); input.operation = createPostFormOperation({});
let prom = handler.handle(input); let prom = handler.handle(input);
await expect(prom).rejects.toThrow('Email required'); await expect(prom).rejects.toThrow('Email required');
await expect(prom).rejects.toThrow(expect.objectContaining({ prefilled: {}})); await expect(prom).rejects.toThrow(expect.objectContaining({ prefilled: {}}));
input.request = createPostFormRequest({ email: [ 'a', 'b' ]}); input.operation = createPostFormOperation({ email: [ 'a', 'b' ]});
prom = handler.handle(input); prom = handler.handle(input);
await expect(prom).rejects.toThrow('Email required'); await expect(prom).rejects.toThrow('Email required');
await expect(prom).rejects.toThrow(expect.objectContaining({ prefilled: { }})); await expect(prom).rejects.toThrow(expect.objectContaining({ prefilled: { }}));
}); });
it('errors on invalid passwords.', async(): Promise<void> => { it('errors on invalid passwords.', async(): Promise<void> => {
input.request = createPostFormRequest({ email }); input.operation = createPostFormOperation({ email });
let prom = handler.handle(input); let prom = handler.handle(input);
await expect(prom).rejects.toThrow('Password required'); await expect(prom).rejects.toThrow('Password required');
await expect(prom).rejects.toThrow(expect.objectContaining({ prefilled: { email }})); await expect(prom).rejects.toThrow(expect.objectContaining({ prefilled: { email }}));
input.request = createPostFormRequest({ email, password: [ 'a', 'b' ]}); input.operation = createPostFormOperation({ email, password: [ 'a', 'b' ]});
prom = handler.handle(input); prom = handler.handle(input);
await expect(prom).rejects.toThrow('Password required'); await expect(prom).rejects.toThrow('Password required');
await expect(prom).rejects.toThrow(expect.objectContaining({ prefilled: { email }})); await expect(prom).rejects.toThrow(expect.objectContaining({ prefilled: { email }}));
}); });
it('throws an IdpInteractionError if there is a problem.', async(): Promise<void> => { it('throws an IdpInteractionError if there is a problem.', async(): Promise<void> => {
input.request = createPostFormRequest({ email, password: 'password!' }); input.operation = createPostFormOperation({ email, password: 'password!' });
(storageAdapter.authenticate as jest.Mock).mockRejectedValueOnce(new Error('auth failed!')); (storageAdapter.authenticate as jest.Mock).mockRejectedValueOnce(new Error('auth failed!'));
const prom = handler.handle(input); const prom = handler.handle(input);
await expect(prom).rejects.toThrow('auth failed!'); await expect(prom).rejects.toThrow('auth failed!');
@ -53,7 +53,7 @@ describe('A LoginHandler', (): void => {
}); });
it('returns an InteractionCompleteResult when done.', async(): Promise<void> => { it('returns an InteractionCompleteResult when done.', async(): Promise<void> => {
input.request = createPostFormRequest({ email, password: 'password!' }); input.operation = createPostFormOperation({ email, password: 'password!' });
await expect(handler.handle(input)).resolves.toEqual({ await expect(handler.handle(input)).resolves.toEqual({
type: 'complete', type: 'complete',
details: { webId, shouldRemember: false }, details: { webId, shouldRemember: false },

View File

@ -5,12 +5,12 @@ import {
import type { AccountStore } from '../../../../../../src/identity/interaction/email-password/storage/AccountStore'; import type { AccountStore } from '../../../../../../src/identity/interaction/email-password/storage/AccountStore';
import { IdpInteractionError } from '../../../../../../src/identity/interaction/util/IdpInteractionError'; import { IdpInteractionError } from '../../../../../../src/identity/interaction/util/IdpInteractionError';
import type { OwnershipValidator } from '../../../../../../src/identity/ownership/OwnershipValidator'; import type { OwnershipValidator } from '../../../../../../src/identity/ownership/OwnershipValidator';
import type { Operation } from '../../../../../../src/ldp/operations/Operation';
import type { ResourceIdentifier } from '../../../../../../src/ldp/representation/ResourceIdentifier'; import type { ResourceIdentifier } from '../../../../../../src/ldp/representation/ResourceIdentifier';
import type { IdentifierGenerator } from '../../../../../../src/pods/generate/IdentifierGenerator'; import type { IdentifierGenerator } from '../../../../../../src/pods/generate/IdentifierGenerator';
import type { PodManager } from '../../../../../../src/pods/PodManager'; import type { PodManager } from '../../../../../../src/pods/PodManager';
import type { PodSettings } from '../../../../../../src/pods/settings/PodSettings'; import type { PodSettings } from '../../../../../../src/pods/settings/PodSettings';
import type { HttpRequest } from '../../../../../../src/server/HttpRequest'; import { createPostFormOperation } from './Util';
import { createPostFormRequest } from './Util';
describe('A RegistrationHandler', (): void => { describe('A RegistrationHandler', (): void => {
// "Correct" values for easy object creation // "Correct" values for easy object creation
@ -25,7 +25,7 @@ describe('A RegistrationHandler', (): void => {
const register = 'true'; const register = 'true';
const createPod = 'true'; const createPod = 'true';
let request: HttpRequest; let operation: Operation;
const baseUrl = 'http://test.com/'; const baseUrl = 'http://test.com/';
const webIdSuffix = '/profile/card'; const webIdSuffix = '/profile/card';
@ -69,80 +69,80 @@ describe('A RegistrationHandler', (): void => {
describe('validating data', (): void => { describe('validating data', (): void => {
it('rejects array inputs.', async(): Promise<void> => { it('rejects array inputs.', async(): Promise<void> => {
request = createPostFormRequest({ mydata: [ 'a', 'b' ]}); operation = createPostFormOperation({ mydata: [ 'a', 'b' ]});
await expect(handler.handle({ request })) await expect(handler.handle({ operation }))
.rejects.toThrow('Unexpected multiple values for mydata.'); .rejects.toThrow('Unexpected multiple values for mydata.');
}); });
it('errors on invalid emails.', async(): Promise<void> => { it('errors on invalid emails.', async(): Promise<void> => {
request = createPostFormRequest({ email: undefined }); operation = createPostFormOperation({ email: undefined });
await expect(handler.handle({ request })) await expect(handler.handle({ operation }))
.rejects.toThrow('Please enter a valid e-mail address.'); .rejects.toThrow('Please enter a valid e-mail address.');
request = createPostFormRequest({ email: '' }); operation = createPostFormOperation({ email: '' });
await expect(handler.handle({ request })) await expect(handler.handle({ operation }))
.rejects.toThrow('Please enter a valid e-mail address.'); .rejects.toThrow('Please enter a valid e-mail address.');
request = createPostFormRequest({ email: 'invalidEmail' }); operation = createPostFormOperation({ email: 'invalidEmail' });
await expect(handler.handle({ request })) await expect(handler.handle({ operation }))
.rejects.toThrow('Please enter a valid e-mail address.'); .rejects.toThrow('Please enter a valid e-mail address.');
}); });
it('errors when a required WebID is not valid.', async(): Promise<void> => { it('errors when a required WebID is not valid.', async(): Promise<void> => {
request = createPostFormRequest({ email, register, webId: undefined }); operation = createPostFormOperation({ email, register, webId: undefined });
await expect(handler.handle({ request })) await expect(handler.handle({ operation }))
.rejects.toThrow('Please enter a valid WebID.'); .rejects.toThrow('Please enter a valid WebID.');
request = createPostFormRequest({ email, register, webId: '' }); operation = createPostFormOperation({ email, register, webId: '' });
await expect(handler.handle({ request })) await expect(handler.handle({ operation }))
.rejects.toThrow('Please enter a valid WebID.'); .rejects.toThrow('Please enter a valid WebID.');
}); });
it('errors on invalid passwords when registering.', async(): Promise<void> => { it('errors on invalid passwords when registering.', async(): Promise<void> => {
request = createPostFormRequest({ email, webId, password, confirmPassword: 'bad', register }); operation = createPostFormOperation({ email, webId, password, confirmPassword: 'bad', register });
await expect(handler.handle({ request })) await expect(handler.handle({ operation }))
.rejects.toThrow('Your password and confirmation did not match.'); .rejects.toThrow('Your password and confirmation did not match.');
}); });
it('errors on invalid pod names when required.', async(): Promise<void> => { it('errors on invalid pod names when required.', async(): Promise<void> => {
request = createPostFormRequest({ email, webId, createPod, podName: undefined }); operation = createPostFormOperation({ email, webId, createPod, podName: undefined });
await expect(handler.handle({ request })) await expect(handler.handle({ operation }))
.rejects.toThrow('Please specify a Pod name.'); .rejects.toThrow('Please specify a Pod name.');
request = createPostFormRequest({ email, webId, createPod, podName: ' ' }); operation = createPostFormOperation({ email, webId, createPod, podName: ' ' });
await expect(handler.handle({ request })) await expect(handler.handle({ operation }))
.rejects.toThrow('Please specify a Pod name.'); .rejects.toThrow('Please specify a Pod name.');
request = createPostFormRequest({ email, webId, createWebId }); operation = createPostFormOperation({ email, webId, createWebId });
await expect(handler.handle({ request })) await expect(handler.handle({ operation }))
.rejects.toThrow('Please specify a Pod name.'); .rejects.toThrow('Please specify a Pod name.');
}); });
it('errors when trying to create a WebID without registering or creating a pod.', async(): Promise<void> => { it('errors when trying to create a WebID without registering or creating a pod.', async(): Promise<void> => {
request = createPostFormRequest({ email, podName, createWebId }); operation = createPostFormOperation({ email, podName, createWebId });
await expect(handler.handle({ request })) await expect(handler.handle({ operation }))
.rejects.toThrow('Please enter a password.'); .rejects.toThrow('Please enter a password.');
request = createPostFormRequest({ email, podName, createWebId, createPod }); operation = createPostFormOperation({ email, podName, createWebId, createPod });
await expect(handler.handle({ request })) await expect(handler.handle({ operation }))
.rejects.toThrow('Please enter a password.'); .rejects.toThrow('Please enter a password.');
request = createPostFormRequest({ email, podName, createWebId, createPod, register }); operation = createPostFormOperation({ email, podName, createWebId, createPod, register });
await expect(handler.handle({ request })) await expect(handler.handle({ operation }))
.rejects.toThrow('Please enter a password.'); .rejects.toThrow('Please enter a password.');
}); });
it('errors when no option is chosen.', async(): Promise<void> => { it('errors when no option is chosen.', async(): Promise<void> => {
request = createPostFormRequest({ email, webId }); operation = createPostFormOperation({ email, webId });
await expect(handler.handle({ request })) await expect(handler.handle({ operation }))
.rejects.toThrow('Please register for a WebID or create a Pod.'); .rejects.toThrow('Please register for a WebID or create a Pod.');
}); });
}); });
describe('handling data', (): void => { describe('handling data', (): void => {
it('can register a user.', async(): Promise<void> => { it('can register a user.', async(): Promise<void> => {
request = createPostFormRequest({ email, webId, password, confirmPassword, register }); operation = createPostFormOperation({ email, webId, password, confirmPassword, register });
await expect(handler.handle({ request })).resolves.toEqual({ await expect(handler.handle({ operation })).resolves.toEqual({
details: { details: {
email, email,
webId, webId,
@ -168,8 +168,8 @@ describe('A RegistrationHandler', (): void => {
it('can create a pod.', async(): Promise<void> => { it('can create a pod.', async(): Promise<void> => {
const params = { email, webId, podName, createPod }; const params = { email, webId, podName, createPod };
request = createPostFormRequest(params); operation = createPostFormOperation(params);
await expect(handler.handle({ request })).resolves.toEqual({ await expect(handler.handle({ operation })).resolves.toEqual({
details: { details: {
email, email,
webId, webId,
@ -197,8 +197,8 @@ describe('A RegistrationHandler', (): void => {
it('adds an oidcIssuer to the data when doing both IDP registration and pod creation.', async(): Promise<void> => { 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 }; const params = { email, webId, password, confirmPassword, podName, register, createPod };
podSettings.oidcIssuer = baseUrl; podSettings.oidcIssuer = baseUrl;
request = createPostFormRequest(params); operation = createPostFormOperation(params);
await expect(handler.handle({ request })).resolves.toEqual({ await expect(handler.handle({ operation })).resolves.toEqual({
details: { details: {
email, email,
webId, webId,
@ -228,9 +228,9 @@ describe('A RegistrationHandler', (): void => {
it('deletes the created account if pod generation fails.', async(): Promise<void> => { it('deletes the created account if pod generation fails.', async(): Promise<void> => {
const params = { email, webId, password, confirmPassword, podName, register, createPod }; const params = { email, webId, password, confirmPassword, podName, register, createPod };
podSettings.oidcIssuer = baseUrl; podSettings.oidcIssuer = baseUrl;
request = createPostFormRequest(params); operation = createPostFormOperation(params);
(podManager.createPod as jest.Mock).mockRejectedValueOnce(new Error('pod error')); (podManager.createPod as jest.Mock).mockRejectedValueOnce(new Error('pod error'));
await expect(handler.handle({ request })).rejects.toThrow('pod error'); await expect(handler.handle({ operation })).rejects.toThrow('pod error');
expect(ownershipValidator.handleSafe).toHaveBeenCalledTimes(1); expect(ownershipValidator.handleSafe).toHaveBeenCalledTimes(1);
expect(ownershipValidator.handleSafe).toHaveBeenLastCalledWith({ webId }); expect(ownershipValidator.handleSafe).toHaveBeenLastCalledWith({ webId });
@ -252,8 +252,8 @@ describe('A RegistrationHandler', (): void => {
podSettings.webId = generatedWebID; podSettings.webId = generatedWebID;
podSettings.oidcIssuer = baseUrl; podSettings.oidcIssuer = baseUrl;
request = createPostFormRequest(params); operation = createPostFormOperation(params);
await expect(handler.handle({ request })).resolves.toEqual({ await expect(handler.handle({ operation })).resolves.toEqual({
details: { details: {
email, email,
webId: generatedWebID, webId: generatedWebID,
@ -281,9 +281,9 @@ describe('A RegistrationHandler', (): void => {
it('throws an IdpInteractionError with all data prefilled if something goes wrong.', async(): Promise<void> => { it('throws an IdpInteractionError with all data prefilled if something goes wrong.', async(): Promise<void> => {
const params = { email, webId, podName, createPod }; const params = { email, webId, podName, createPod };
request = createPostFormRequest(params); operation = createPostFormOperation(params);
(podManager.createPod as jest.Mock).mockRejectedValueOnce(new Error('pod error')); (podManager.createPod as jest.Mock).mockRejectedValueOnce(new Error('pod error'));
const prom = handler.handle({ request }); const prom = handler.handle({ operation });
await expect(prom).rejects.toThrow('pod error'); await expect(prom).rejects.toThrow('pod error');
await expect(prom).rejects.toThrow(IdpInteractionError); await expect(prom).rejects.toThrow(IdpInteractionError);
// Using the cleaned input for prefilled // Using the cleaned input for prefilled

View File

@ -2,11 +2,11 @@ import {
ResetPasswordHandler, ResetPasswordHandler,
} from '../../../../../../src/identity/interaction/email-password/handler/ResetPasswordHandler'; } from '../../../../../../src/identity/interaction/email-password/handler/ResetPasswordHandler';
import type { AccountStore } from '../../../../../../src/identity/interaction/email-password/storage/AccountStore'; import type { AccountStore } from '../../../../../../src/identity/interaction/email-password/storage/AccountStore';
import type { HttpRequest } from '../../../../../../src/server/HttpRequest'; import type { Operation } from '../../../../../../src/ldp/operations/Operation';
import { createPostFormRequest } from './Util'; import { createPostFormOperation } from './Util';
describe('A ResetPasswordHandler', (): void => { describe('A ResetPasswordHandler', (): void => {
let request: HttpRequest; let operation: Operation;
const recordId = '123456'; const recordId = '123456';
const url = `/resetURL/${recordId}`; const url = `/resetURL/${recordId}`;
const email = 'alice@test.email'; const email = 'alice@test.email';
@ -25,28 +25,28 @@ describe('A ResetPasswordHandler', (): void => {
it('errors for non-string recordIds.', async(): Promise<void> => { it('errors for non-string recordIds.', async(): Promise<void> => {
const errorMessage = 'Invalid request. Open the link from your email again'; const errorMessage = 'Invalid request. Open the link from your email again';
request = createPostFormRequest({}); operation = createPostFormOperation({});
await expect(handler.handle({ request })).rejects.toThrow(errorMessage); await expect(handler.handle({ operation })).rejects.toThrow(errorMessage);
request = createPostFormRequest({}, ''); operation = createPostFormOperation({}, '');
await expect(handler.handle({ request })).rejects.toThrow(errorMessage); await expect(handler.handle({ operation })).rejects.toThrow(errorMessage);
}); });
it('errors for invalid passwords.', async(): Promise<void> => { it('errors for invalid passwords.', async(): Promise<void> => {
const errorMessage = 'Your password and confirmation did not match.'; const errorMessage = 'Your password and confirmation did not match.';
request = createPostFormRequest({ password: 'password!', confirmPassword: 'otherPassword!' }, url); operation = createPostFormOperation({ password: 'password!', confirmPassword: 'otherPassword!' }, url);
await expect(handler.handle({ request })).rejects.toThrow(errorMessage); await expect(handler.handle({ operation })).rejects.toThrow(errorMessage);
}); });
it('errors for invalid emails.', async(): Promise<void> => { it('errors for invalid emails.', async(): Promise<void> => {
const errorMessage = 'This reset password link is no longer valid.'; const errorMessage = 'This reset password link is no longer valid.';
request = createPostFormRequest({ password: 'password!', confirmPassword: 'password!' }, url); operation = createPostFormOperation({ password: 'password!', confirmPassword: 'password!' }, url);
(accountStore.getForgotPasswordRecord as jest.Mock).mockResolvedValueOnce(undefined); (accountStore.getForgotPasswordRecord as jest.Mock).mockResolvedValueOnce(undefined);
await expect(handler.handle({ request })).rejects.toThrow(errorMessage); await expect(handler.handle({ operation })).rejects.toThrow(errorMessage);
}); });
it('renders a message on success.', async(): Promise<void> => { it('renders a message on success.', async(): Promise<void> => {
request = createPostFormRequest({ password: 'password!', confirmPassword: 'password!' }, url); operation = createPostFormOperation({ password: 'password!', confirmPassword: 'password!' }, url);
await expect(handler.handle({ request })).resolves.toEqual({ type: 'response' }); await expect(handler.handle({ operation })).resolves.toEqual({ type: 'response' });
expect(accountStore.getForgotPasswordRecord).toHaveBeenCalledTimes(1); expect(accountStore.getForgotPasswordRecord).toHaveBeenCalledTimes(1);
expect(accountStore.getForgotPasswordRecord).toHaveBeenLastCalledWith(recordId); expect(accountStore.getForgotPasswordRecord).toHaveBeenLastCalledWith(recordId);
expect(accountStore.deleteForgotPasswordRecord).toHaveBeenCalledTimes(1); expect(accountStore.deleteForgotPasswordRecord).toHaveBeenCalledTimes(1);
@ -57,8 +57,8 @@ describe('A ResetPasswordHandler', (): void => {
it('has a default error for non-native errors.', async(): Promise<void> => { it('has a default error for non-native errors.', async(): Promise<void> => {
const errorMessage = 'Unknown error: not native'; const errorMessage = 'Unknown error: not native';
request = createPostFormRequest({ password: 'password!', confirmPassword: 'password!' }, url); operation = createPostFormOperation({ password: 'password!', confirmPassword: 'password!' }, url);
(accountStore.getForgotPasswordRecord as jest.Mock).mockRejectedValueOnce('not native'); (accountStore.getForgotPasswordRecord as jest.Mock).mockRejectedValueOnce('not native');
await expect(handler.handle({ request })).rejects.toThrow(errorMessage); await expect(handler.handle({ operation })).rejects.toThrow(errorMessage);
}); });
}); });

View File

@ -1,6 +1,6 @@
import { stringify } from 'querystring'; import { stringify } from 'querystring';
import type { HttpRequest } from '../../../../../../src/server/HttpRequest'; import type { Operation } from '../../../../../../src/ldp/operations/Operation';
import { guardedStreamFrom } from '../../../../../../src/util/StreamUtil'; import { BasicRepresentation } from '../../../../../../src/ldp/representation/BasicRepresentation';
/** /**
* Creates a mock HttpRequest which is a stream of an object encoded as application/x-www-form-urlencoded * Creates a mock HttpRequest which is a stream of an object encoded as application/x-www-form-urlencoded
@ -8,9 +8,11 @@ import { guardedStreamFrom } from '../../../../../../src/util/StreamUtil';
* @param data - Object to encode. * @param data - Object to encode.
* @param url - URL value of the request. * @param url - URL value of the request.
*/ */
export function createPostFormRequest(data: NodeJS.Dict<any>, url?: string): HttpRequest { export function createPostFormOperation(data: NodeJS.Dict<any>, url?: string): Operation {
const request = guardedStreamFrom(stringify(data)) as HttpRequest; return {
request.headers = { 'content-type': 'application/x-www-form-urlencoded' }; method: 'POST',
request.url = url; preferences: {},
return request; target: { path: url ?? 'http://test.com/' },
body: new BasicRepresentation(stringify(data), 'application/x-www-form-urlencoded'),
};
} }

View File

@ -2,9 +2,9 @@ import { stringify } from 'querystring';
import { import {
getFormDataRequestBody, getFormDataRequestBody,
} from '../../../../../src/identity/interaction/util/FormDataUtil'; } from '../../../../../src/identity/interaction/util/FormDataUtil';
import type { HttpRequest } from '../../../../../src/server/HttpRequest'; import type { Operation } from '../../../../../src/ldp/operations/Operation';
import { BasicRepresentation } from '../../../../../src/ldp/representation/BasicRepresentation';
import { UnsupportedMediaTypeHttpError } from '../../../../../src/util/errors/UnsupportedMediaTypeHttpError'; import { UnsupportedMediaTypeHttpError } from '../../../../../src/util/errors/UnsupportedMediaTypeHttpError';
import { guardedStreamFrom } from '../../../../../src/util/StreamUtil';
describe('FormDataUtil', (): void => { describe('FormDataUtil', (): void => {
describe('#getFormDataRequestBody', (): void => { describe('#getFormDataRequestBody', (): void => {
@ -15,9 +15,13 @@ describe('FormDataUtil', (): void => {
it('converts the body to an object.', async(): Promise<void> => { it('converts the body to an object.', async(): Promise<void> => {
const data = { test: 'test!', moreTest: '!TEST!' }; const data = { test: 'test!', moreTest: '!TEST!' };
const stream = guardedStreamFrom(stringify(data)) as HttpRequest; const operation: Operation = {
stream.headers = { 'content-type': 'application/x-www-form-urlencoded' }; method: 'GET',
await expect(getFormDataRequestBody(stream)).resolves.toEqual(data); preferences: {},
target: { path: '' },
body: new BasicRepresentation(stringify(data), 'application/x-www-form-urlencoded'),
};
await expect(getFormDataRequestBody(operation)).resolves.toEqual(data);
}); });
}); });
}); });

View File

@ -1,31 +1,39 @@
import { createResponse } from 'node-mocks-http'; import { readableToString } from '../../../../src';
import type { HttpResponse } from '../../../../src'; import type { ResponseDescription, ResponseWriter, HttpResponse } from '../../../../src';
import { TemplateHandler } from '../../../../src/server/util/TemplateHandler'; import { TemplateHandler } from '../../../../src/server/util/TemplateHandler';
import type { TemplateEngine } from '../../../../src/util/templates/TemplateEngine'; import type { TemplateEngine } from '../../../../src/util/templates/TemplateEngine';
describe('A TemplateHandler', (): void => { describe('A TemplateHandler', (): void => {
const contents = { contents: 'contents' }; const contents = { contents: 'contents' };
const templateFile = '/templates/main.html.ejs'; const templateFile = '/templates/main.html.ejs';
let responseWriter: jest.Mocked<ResponseWriter>;
let templateEngine: jest.Mocked<TemplateEngine>; let templateEngine: jest.Mocked<TemplateEngine>;
let response: HttpResponse; const response: HttpResponse = {} as any;
beforeEach((): void => { beforeEach((): void => {
responseWriter = {
handleSafe: jest.fn(),
} as any;
templateEngine = { templateEngine = {
render: jest.fn().mockResolvedValue('rendered'), render: jest.fn().mockResolvedValue('rendered'),
}; };
response = createResponse() as HttpResponse;
}); });
it('renders the template in the response.', async(): Promise<void> => { it('renders the template in the response.', async(): Promise<void> => {
const handler = new TemplateHandler(templateEngine); const handler = new TemplateHandler(responseWriter, templateEngine);
await handler.handle({ response, contents, templateFile }); await handler.handle({ response, contents, templateFile });
expect(templateEngine.render).toHaveBeenCalledTimes(1); expect(templateEngine.render).toHaveBeenCalledTimes(1);
expect(templateEngine.render).toHaveBeenCalledWith(contents, { templateFile }); expect(templateEngine.render).toHaveBeenCalledWith(contents, { templateFile });
expect(response.getHeaders()).toHaveProperty('content-type', 'text/html'); expect(responseWriter.handleSafe).toHaveBeenCalledTimes(1);
expect((response as any)._isEndCalled()).toBe(true); const input: { response: HttpResponse; result: ResponseDescription } = responseWriter.handleSafe.mock.calls[0][0];
expect((response as any)._getData()).toBe('rendered');
expect((response as any)._getStatusCode()).toBe(200); expect(input.response).toBe(response);
expect(input.result.statusCode).toBe(200);
expect(input.result.metadata?.contentType).toBe('text/html');
await expect(readableToString(input.result.data!)).resolves.toBe('rendered');
}); });
}); });