mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
feat: Use RequestParser and ResponseWriter for IDP
This commit is contained in:
parent
a7a22bf43a
commit
7b7040a196
@ -19,10 +19,13 @@
|
||||
{
|
||||
"@id": "urn:solid-server:default:IdentityProviderHttpHandler",
|
||||
"@type": "IdentityProviderHttpHandler",
|
||||
"baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" },
|
||||
"idpPath": "/idp",
|
||||
"requestParser": { "@id": "urn:solid-server:default:RequestParser" },
|
||||
"providerFactory": { "@id": "urn:solid-server:default:IdentityProviderFactory" },
|
||||
"templateHandler": {
|
||||
"@type": "TemplateHandler",
|
||||
"responseWriter": { "@id": "urn:solid-server:default:ResponseWriter" },
|
||||
"templateEngine": {
|
||||
"comment": "Renders the specific page and embeds it into the main HTML body.",
|
||||
"@type": "ChainedTemplateEngine",
|
||||
@ -35,7 +38,7 @@
|
||||
{
|
||||
"comment": "Will embed the result of the first engine into the main HTML template.",
|
||||
"@type": "EjsTemplateEngine",
|
||||
"template": "@css:templates/main.html.ejs",
|
||||
"template": "@css:templates/main.html.ejs"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -1,5 +1,9 @@
|
||||
import urljoin from 'url-join';
|
||||
import type { ErrorHandler } from '../ldp/http/ErrorHandler';
|
||||
import type { RequestParser } from '../ldp/http/RequestParser';
|
||||
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 type { HttpHandlerInput } from '../server/HttpHandler';
|
||||
import { HttpHandler } from '../server/HttpHandler';
|
||||
@ -65,7 +69,8 @@ export class InteractionRoute {
|
||||
export class IdentityProviderHttpHandler extends HttpHandler {
|
||||
protected readonly logger = getLoggerFor(this);
|
||||
|
||||
private readonly idpPath: string;
|
||||
private readonly baseUrl: string;
|
||||
private readonly requestParser: RequestParser;
|
||||
private readonly providerFactory: ProviderFactory;
|
||||
private readonly interactionRoutes: InteractionRoute[];
|
||||
private readonly templateHandler: TemplateHandler;
|
||||
@ -74,7 +79,9 @@ export class IdentityProviderHttpHandler extends HttpHandler {
|
||||
private readonly responseWriter: ResponseWriter;
|
||||
|
||||
/**
|
||||
* @param baseUrl - Base URL of the server.
|
||||
* @param idpPath - Relative path of the IDP entry point.
|
||||
* @param requestParser - Used for parsing requests.
|
||||
* @param providerFactory - Used to generate the OIDC provider.
|
||||
* @param interactionRoutes - All routes handling the custom IDP behaviour.
|
||||
* @param templateHandler - Used for rendering responses.
|
||||
@ -83,7 +90,9 @@ export class IdentityProviderHttpHandler extends HttpHandler {
|
||||
* @param responseWriter - Renders error responses.
|
||||
*/
|
||||
public constructor(
|
||||
baseUrl: string,
|
||||
idpPath: string,
|
||||
requestParser: RequestParser,
|
||||
providerFactory: ProviderFactory,
|
||||
interactionRoutes: InteractionRoute[],
|
||||
templateHandler: TemplateHandler,
|
||||
@ -92,11 +101,9 @@ export class IdentityProviderHttpHandler extends HttpHandler {
|
||||
responseWriter: ResponseWriter,
|
||||
) {
|
||||
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
|
||||
this.idpPath = trimTrailingSlashes(idpPath);
|
||||
this.baseUrl = trimTrailingSlashes(urljoin(baseUrl, idpPath));
|
||||
this.requestParser = requestParser;
|
||||
this.providerFactory = providerFactory;
|
||||
this.interactionRoutes = interactionRoutes;
|
||||
this.templateHandler = templateHandler;
|
||||
@ -106,20 +113,24 @@ export class IdentityProviderHttpHandler extends HttpHandler {
|
||||
}
|
||||
|
||||
public async handle({ request, response }: HttpHandlerInput): Promise<void> {
|
||||
let preferences: RepresentationPreferences = { type: { 'text/plain': 1 }};
|
||||
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) {
|
||||
assertError(error);
|
||||
// Setting preferences to text/plain since we didn't parse accept headers, see #764
|
||||
const result = await this.errorHandler.handleSafe({ error, preferences: { type: { 'text/plain': 1 }}});
|
||||
const result = await this.errorHandler.handleSafe({ error, preferences });
|
||||
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
|
||||
let oidcInteraction: Interaction | undefined;
|
||||
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
|
||||
const route = await this.findRoute(request, oidcInteraction);
|
||||
const route = await this.findRoute(operation, oidcInteraction);
|
||||
if (!route) {
|
||||
// Make sure the request stream still works in case the RequestParser read it
|
||||
const provider = await this.providerFactory.getProvider();
|
||||
this.logger.debug(`Sending request to oidc-provider: ${request.url}`);
|
||||
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.
|
||||
*/
|
||||
private async findRoute(request: HttpRequest, oidcInteraction?: Interaction): Promise<InteractionRoute | undefined> {
|
||||
if (!request.url || !request.url.startsWith(this.idpPath)) {
|
||||
private async findRoute(operation: Operation, oidcInteraction?: Interaction): Promise<InteractionRoute | undefined> {
|
||||
if (!operation.target.path.startsWith(this.baseUrl)) {
|
||||
// This is either an invalid request or a call to the .well-known configuration
|
||||
return;
|
||||
}
|
||||
const pathName = request.url.slice(this.idpPath.length);
|
||||
const pathName = operation.target.path.slice(this.baseUrl.length);
|
||||
let route = this.getRouteMatch(pathName);
|
||||
|
||||
// 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.
|
||||
*/
|
||||
private async resolveRoute(request: HttpRequest, response: HttpResponse, route: InteractionRoute,
|
||||
oidcInteraction?: Interaction): Promise<void> {
|
||||
if (request.method === 'GET') {
|
||||
return await this.handleTemplateResponse(
|
||||
response, route.viewTemplate, { errorMessage: '', prefilled: {}}, oidcInteraction,
|
||||
);
|
||||
private async resolveRoute(operation: Operation, route: InteractionRoute, oidcInteraction?: Interaction):
|
||||
Promise<{ result: InteractionHandlerResult; templateFile?: string }> {
|
||||
if (operation.method === 'GET') {
|
||||
// .ejs templates errors on undefined variables
|
||||
return {
|
||||
result: { type: 'response', details: { errorMessage: '', prefilled: {}}},
|
||||
templateFile: route.viewTemplate,
|
||||
};
|
||||
}
|
||||
|
||||
if (request.method === 'POST') {
|
||||
let result: InteractionHandlerResult;
|
||||
if (operation.method === 'POST') {
|
||||
try {
|
||||
result = await route.handler.handleSafe({ request, oidcInteraction });
|
||||
const result = await route.handler.handleSafe({ operation, oidcInteraction });
|
||||
return { result, templateFile: route.responseTemplate };
|
||||
} catch (error: unknown) {
|
||||
// Render error in the view
|
||||
const prefilled = IdpInteractionError.isInstance(error) ? error.prefilled : {};
|
||||
const errorMessage = createErrorMessage(error);
|
||||
return await this.handleTemplateResponse(
|
||||
response, route.viewTemplate, { errorMessage, prefilled }, oidcInteraction,
|
||||
);
|
||||
return {
|
||||
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>,
|
||||
|
@ -7,12 +7,12 @@ import { getFormDataRequestBody } from './util/FormDataUtil';
|
||||
* Simple InteractionHttpHandler that sends the session accountId to the InteractionCompleter as webId.
|
||||
*/
|
||||
export class SessionHttpHandler extends InteractionHandler {
|
||||
public async handle({ request, oidcInteraction }: InteractionHandlerInput): Promise<InteractionCompleteResult> {
|
||||
public async handle({ operation, oidcInteraction }: InteractionHandlerInput): Promise<InteractionCompleteResult> {
|
||||
if (!oidcInteraction?.session) {
|
||||
throw new NotImplementedHttpError('Only interactions with a valid session are supported.');
|
||||
}
|
||||
|
||||
const { remember } = await getFormDataRequestBody(request);
|
||||
const { remember } = await getFormDataRequestBody(operation);
|
||||
return {
|
||||
type: 'complete',
|
||||
details: { webId: oidcInteraction.session.accountId, shouldRemember: Boolean(remember) },
|
||||
|
@ -39,10 +39,10 @@ export class ForgotPasswordHandler extends InteractionHandler {
|
||||
this.emailSender = args.emailSender;
|
||||
}
|
||||
|
||||
public async handle({ request }: InteractionHandlerInput): Promise<InteractionResponseResult<{ email: string }>> {
|
||||
public async handle({ operation }: InteractionHandlerInput): Promise<InteractionResponseResult<{ email: string }>> {
|
||||
try {
|
||||
// Validate incoming data
|
||||
const { email } = await getFormDataRequestBody(request);
|
||||
const { email } = await getFormDataRequestBody(operation);
|
||||
assert(typeof email === 'string' && email.length > 0, 'Email required');
|
||||
|
||||
await this.resetPassword(email);
|
||||
|
@ -1,5 +1,5 @@
|
||||
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 type { InteractionCompleterParams } from '../../util/InteractionCompleter';
|
||||
|
||||
@ -8,9 +8,9 @@ export type Interaction = KoaContextWithOIDC['oidc']['entities']['Interaction'];
|
||||
|
||||
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,
|
||||
* such as logging a user in.
|
||||
|
@ -1,6 +1,6 @@
|
||||
import assert from 'assert';
|
||||
import type { Operation } from '../../../../ldp/operations/Operation';
|
||||
import { getLoggerFor } from '../../../../logging/LogUtil';
|
||||
import type { HttpRequest } from '../../../../server/HttpRequest';
|
||||
import { getFormDataRequestBody } from '../../util/FormDataUtil';
|
||||
import { throwIdpInteractionError } from '../EmailPasswordUtil';
|
||||
import type { AccountStore } from '../storage/AccountStore';
|
||||
@ -20,8 +20,8 @@ export class LoginHandler extends InteractionHandler {
|
||||
this.accountStore = accountStore;
|
||||
}
|
||||
|
||||
public async handle({ request }: InteractionHandlerInput): Promise<InteractionCompleteResult> {
|
||||
const { email, password, remember } = await this.parseInput(request);
|
||||
public async handle({ operation }: InteractionHandlerInput): Promise<InteractionCompleteResult> {
|
||||
const { email, password, remember } = await this.parseInput(operation);
|
||||
try {
|
||||
// Try to log in, will error if email/password combination is invalid
|
||||
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.
|
||||
* 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> = {};
|
||||
try {
|
||||
const { email, password, remember } = await getFormDataRequestBody(request);
|
||||
const { email, password, remember } = await getFormDataRequestBody(operation);
|
||||
assert(typeof email === 'string' && email.length > 0, 'Email required');
|
||||
prefilled.email = email;
|
||||
assert(typeof password === 'string' && password.length > 0, 'Password required');
|
||||
|
@ -1,11 +1,11 @@
|
||||
import assert from 'assert';
|
||||
import urljoin from 'url-join';
|
||||
import type { Operation } from '../../../../ldp/operations/Operation';
|
||||
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 { PodSettings } from '../../../../pods/settings/PodSettings';
|
||||
import type { HttpRequest } from '../../../../server/HttpRequest';
|
||||
import type { OwnershipValidator } from '../../../ownership/OwnershipValidator';
|
||||
import { getFormDataRequestBody } from '../../util/FormDataUtil';
|
||||
import { assertPassword, throwIdpInteractionError } from '../EmailPasswordUtil';
|
||||
@ -102,8 +102,9 @@ export class RegistrationHandler extends InteractionHandler {
|
||||
this.podManager = args.podManager;
|
||||
}
|
||||
|
||||
public async handle({ request }: InteractionHandlerInput): Promise<InteractionResponseResult<RegistrationResponse>> {
|
||||
const result = await this.parseInput(request);
|
||||
public async handle({ operation }: InteractionHandlerInput):
|
||||
Promise<InteractionResponseResult<RegistrationResponse>> {
|
||||
const result = await this.parseInput(operation);
|
||||
|
||||
try {
|
||||
const details = await this.register(result);
|
||||
@ -184,8 +185,8 @@ export class RegistrationHandler extends InteractionHandler {
|
||||
/**
|
||||
* Parses the input request into a `ParseResult`.
|
||||
*/
|
||||
private async parseInput(request: HttpRequest): Promise<ParsedInput> {
|
||||
const parsed = await getFormDataRequestBody(request);
|
||||
private async parseInput(operation: Operation): Promise<ParsedInput> {
|
||||
const parsed = await getFormDataRequestBody(operation);
|
||||
const prefilled: Record<string, string> = {};
|
||||
try {
|
||||
for (const [ key, value ] of Object.entries(parsed)) {
|
||||
|
@ -20,12 +20,12 @@ export class ResetPasswordHandler extends InteractionHandler {
|
||||
this.accountStore = accountStore;
|
||||
}
|
||||
|
||||
public async handle({ request }: InteractionHandlerInput): Promise<InteractionResponseResult> {
|
||||
public async handle({ operation }: InteractionHandlerInput): Promise<InteractionResponseResult> {
|
||||
try {
|
||||
// Extract record ID from request URL
|
||||
const recordId = /\/([^/]+)$/u.exec(request.url!)?.[1];
|
||||
const recordId = /\/([^/]+)$/u.exec(operation.target.path)?.[1];
|
||||
// Validate input data
|
||||
const { password, confirmPassword } = await getFormDataRequestBody(request);
|
||||
const { password, confirmPassword } = await getFormDataRequestBody(operation);
|
||||
assert(
|
||||
typeof recordId === 'string' && recordId.length > 0,
|
||||
'Invalid request. Open the link from your email again',
|
||||
|
@ -1,17 +1,17 @@
|
||||
import type { ParsedUrlQuery } 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 { UnsupportedMediaTypeHttpError } from '../../../util/errors/UnsupportedMediaTypeHttpError';
|
||||
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> {
|
||||
if (request.headers['content-type'] !== APPLICATION_X_WWW_FORM_URLENCODED) {
|
||||
export async function getFormDataRequestBody(operation: Operation): Promise<ParsedUrlQuery> {
|
||||
if (operation.body?.metadata.contentType !== APPLICATION_X_WWW_FORM_URLENCODED) {
|
||||
throw new UnsupportedMediaTypeHttpError();
|
||||
}
|
||||
const body = await readableToString(request);
|
||||
const body = await readableToString(operation.body.data);
|
||||
return parse(body);
|
||||
}
|
||||
|
@ -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 { guardedStreamFrom } from '../../util/StreamUtil';
|
||||
import type { TemplateEngine } from '../../util/templates/TemplateEngine';
|
||||
import type { HttpResponse } from '../HttpResponse';
|
||||
import Dict = NodeJS.Dict;
|
||||
@ -8,11 +12,13 @@ import Dict = NodeJS.Dict;
|
||||
*/
|
||||
export class TemplateHandler<T extends Dict<any> = Dict<any>>
|
||||
extends AsyncHandler<{ response: HttpResponse; templateFile: string; contents: T }> {
|
||||
private readonly responseWriter: ResponseWriter;
|
||||
private readonly templateEngine: TemplateEngine;
|
||||
private readonly contentType: string;
|
||||
|
||||
public constructor(templateEngine: TemplateEngine, contentType = 'text/html') {
|
||||
public constructor(responseWriter: ResponseWriter, templateEngine: TemplateEngine, contentType = 'text/html') {
|
||||
super();
|
||||
this.responseWriter = responseWriter;
|
||||
this.templateEngine = templateEngine;
|
||||
this.contentType = contentType;
|
||||
}
|
||||
@ -20,8 +26,11 @@ export class TemplateHandler<T extends Dict<any> = Dict<any>>
|
||||
public async handle({ response, templateFile, contents }:
|
||||
{ response: HttpResponse; templateFile: string; contents: T }): Promise<void> {
|
||||
const rendered = await this.templateEngine.render(contents, { templateFile });
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
response.writeHead(200, { 'Content-Type': this.contentType });
|
||||
response.end(rendered);
|
||||
const result: ResponseDescription = {
|
||||
statusCode: 200,
|
||||
data: guardedStreamFrom(rendered),
|
||||
metadata: new RepresentationMetadata(this.contentType),
|
||||
};
|
||||
await this.responseWriter.handleSafe({ response, result });
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,15 @@
|
||||
import type { Provider } from 'oidc-provider';
|
||||
import urljoin from 'url-join';
|
||||
import type { ProviderFactory } from '../../../src/identity/configuration/ProviderFactory';
|
||||
import { InteractionRoute, IdentityProviderHttpHandler } from '../../../src/identity/IdentityProviderHttpHandler';
|
||||
import type { InteractionHandler } from '../../../src/identity/interaction/email-password/handler/InteractionHandler';
|
||||
import { IdpInteractionError } from '../../../src/identity/interaction/util/IdpInteractionError';
|
||||
import type { InteractionCompleter } from '../../../src/identity/interaction/util/InteractionCompleter';
|
||||
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 { Operation } from '../../../src/ldp/operations/Operation';
|
||||
import { BasicRepresentation } from '../../../src/ldp/representation/BasicRepresentation';
|
||||
import type { HttpRequest } from '../../../src/server/HttpRequest';
|
||||
import type { HttpResponse } from '../../../src/server/HttpResponse';
|
||||
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';
|
||||
|
||||
describe('An IdentityProviderHttpHandler', (): void => {
|
||||
const baseUrl = 'http://test.com/';
|
||||
const idpPath = '/idp';
|
||||
let request: HttpRequest;
|
||||
const response: HttpResponse = {} as any;
|
||||
let requestParser: jest.Mocked<RequestParser>;
|
||||
let providerFactory: jest.Mocked<ProviderFactory>;
|
||||
let routes: { response: InteractionRoute; complete: InteractionRoute };
|
||||
let interactionCompleter: jest.Mocked<InteractionCompleter>;
|
||||
@ -26,7 +32,16 @@ describe('An IdentityProviderHttpHandler', (): void => {
|
||||
let handler: IdentityProviderHttpHandler;
|
||||
|
||||
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 = {
|
||||
callback: jest.fn(),
|
||||
@ -56,7 +71,9 @@ describe('An IdentityProviderHttpHandler', (): void => {
|
||||
responseWriter = { handleSafe: jest.fn() } as any;
|
||||
|
||||
handler = new IdentityProviderHttpHandler(
|
||||
baseUrl,
|
||||
idpPath,
|
||||
requestParser,
|
||||
providerFactory,
|
||||
Object.values(routes),
|
||||
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> => {
|
||||
request.url = 'invalid';
|
||||
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
|
||||
@ -92,8 +103,9 @@ describe('An IdentityProviderHttpHandler', (): void => {
|
||||
request.url = '/idp/routeResponse';
|
||||
request.method = 'POST';
|
||||
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).toHaveBeenLastCalledWith({ request });
|
||||
expect(routes.response.handler.handleSafe).toHaveBeenLastCalledWith({ operation });
|
||||
expect(templateHandler.handleSafe).toHaveBeenCalledTimes(1);
|
||||
expect(templateHandler.handleSafe).toHaveBeenLastCalledWith(
|
||||
{ response, templateFile: routes.response.responseTemplate, contents: { key: 'val', authenticating: false }},
|
||||
@ -107,8 +119,9 @@ describe('An IdentityProviderHttpHandler', (): void => {
|
||||
provider.interactionDetails.mockResolvedValueOnce(oidcInteraction);
|
||||
(routes.response.handler as jest.Mocked<InteractionHandler>).handleSafe.mockResolvedValueOnce({ type: 'response' });
|
||||
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).toHaveBeenLastCalledWith({ request, oidcInteraction });
|
||||
expect(routes.response.handler.handleSafe).toHaveBeenLastCalledWith({ operation, oidcInteraction });
|
||||
expect(templateHandler.handleSafe).toHaveBeenCalledTimes(1);
|
||||
expect(templateHandler.handleSafe).toHaveBeenLastCalledWith(
|
||||
{ response, templateFile: routes.response.responseTemplate, contents: { authenticating: true }},
|
||||
@ -120,15 +133,16 @@ describe('An IdentityProviderHttpHandler', (): void => {
|
||||
request.method = 'POST';
|
||||
errorHandler.handleSafe.mockResolvedValueOnce({ statusCode: 400 });
|
||||
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).toHaveBeenLastCalledWith({ request });
|
||||
expect(routes.complete.handler.handleSafe).toHaveBeenLastCalledWith({ operation });
|
||||
expect(interactionCompleter.handleSafe).toHaveBeenCalledTimes(0);
|
||||
|
||||
const error = new BadRequestHttpError(
|
||||
'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).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).toHaveBeenLastCalledWith({ response, result: { statusCode: 400 }});
|
||||
});
|
||||
@ -139,8 +153,9 @@ describe('An IdentityProviderHttpHandler', (): void => {
|
||||
const oidcInteraction = { session: { accountId: 'account' }} as any;
|
||||
provider.interactionDetails.mockResolvedValueOnce(oidcInteraction);
|
||||
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).toHaveBeenLastCalledWith({ request, oidcInteraction });
|
||||
expect(routes.complete.handler.handleSafe).toHaveBeenLastCalledWith({ operation, oidcInteraction });
|
||||
expect(interactionCompleter.handleSafe).toHaveBeenCalledTimes(1);
|
||||
expect(interactionCompleter.handleSafe).toHaveBeenLastCalledWith({ request, response, webId: 'webId' });
|
||||
});
|
||||
@ -151,9 +166,10 @@ describe('An IdentityProviderHttpHandler', (): void => {
|
||||
const oidcInteraction = { prompt: { name: 'other' }};
|
||||
provider.interactionDetails.mockResolvedValueOnce(oidcInteraction as any);
|
||||
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.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> => {
|
||||
@ -162,8 +178,9 @@ describe('An IdentityProviderHttpHandler', (): void => {
|
||||
const oidcInteraction = { prompt: { name: 'notSupported' }};
|
||||
provider.interactionDetails.mockResolvedValueOnce(oidcInteraction as any);
|
||||
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).toHaveBeenLastCalledWith({ request, oidcInteraction });
|
||||
expect(routes.response.handler.handleSafe).toHaveBeenLastCalledWith({ operation, oidcInteraction });
|
||||
expect(routes.complete.handler.handleSafe).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
@ -203,7 +220,7 @@ describe('An IdentityProviderHttpHandler', (): void => {
|
||||
errorHandler.handleSafe.mockResolvedValueOnce({ statusCode: 500 });
|
||||
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
|
||||
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).toHaveBeenLastCalledWith({ response, result: { statusCode: 500 }});
|
||||
});
|
||||
@ -211,11 +228,11 @@ describe('An IdentityProviderHttpHandler', (): void => {
|
||||
it('can only resolve GET/POST requests.', async(): Promise<void> => {
|
||||
request.url = '/idp/routeResponse';
|
||||
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 });
|
||||
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
|
||||
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).toHaveBeenLastCalledWith({ response, result: { statusCode: 500 }});
|
||||
});
|
||||
@ -224,18 +241,26 @@ describe('An IdentityProviderHttpHandler', (): void => {
|
||||
request.url = '/idp/routeResponse';
|
||||
request.method = 'POST';
|
||||
(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 });
|
||||
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
|
||||
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).toHaveBeenLastCalledWith({ response, result: { statusCode: 500 }});
|
||||
});
|
||||
|
||||
it('errors if no route is configured for the default prompt.', async(): Promise<void> => {
|
||||
handler = new IdentityProviderHttpHandler(
|
||||
idpPath, providerFactory, [], templateHandler, interactionCompleter, errorHandler, responseWriter,
|
||||
baseUrl,
|
||||
idpPath,
|
||||
requestParser,
|
||||
providerFactory,
|
||||
[],
|
||||
templateHandler,
|
||||
interactionCompleter,
|
||||
errorHandler,
|
||||
responseWriter,
|
||||
);
|
||||
request.url = '/idp';
|
||||
provider.interactionDetails.mockResolvedValueOnce({ prompt: { name: 'other' }} as any);
|
||||
@ -243,7 +268,7 @@ describe('An IdentityProviderHttpHandler', (): void => {
|
||||
errorHandler.handleSafe.mockResolvedValueOnce({ statusCode: 500 });
|
||||
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
|
||||
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).toHaveBeenLastCalledWith({ response, result: { statusCode: 500 }});
|
||||
});
|
||||
|
@ -1,7 +1,7 @@
|
||||
import type { Interaction } from '../../../../src/identity/interaction/email-password/handler/InteractionHandler';
|
||||
import { SessionHttpHandler } from '../../../../src/identity/interaction/SessionHttpHandler';
|
||||
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 => {
|
||||
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> => {
|
||||
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> => {
|
||||
const request = createPostFormRequest({ remember: true });
|
||||
await expect(handler.handle({ request, oidcInteraction })).resolves.toEqual({
|
||||
const operation = createPostFormOperation({ remember: true });
|
||||
await expect(handler.handle({ operation, oidcInteraction })).resolves.toEqual({
|
||||
details: { webId, shouldRemember: true },
|
||||
type: 'complete',
|
||||
});
|
||||
|
@ -3,12 +3,12 @@ import {
|
||||
} from '../../../../../../src/identity/interaction/email-password/handler/ForgotPasswordHandler';
|
||||
import type { AccountStore } from '../../../../../../src/identity/interaction/email-password/storage/AccountStore';
|
||||
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 { createPostFormRequest } from './Util';
|
||||
import { createPostFormOperation } from './Util';
|
||||
|
||||
describe('A ForgotPasswordHandler', (): void => {
|
||||
let request: HttpRequest;
|
||||
let operation: Operation;
|
||||
const email = 'test@test.email';
|
||||
const recordId = '123456';
|
||||
const html = `<a href="/base/idp/resetpassword/${recordId}">Reset Password</a>`;
|
||||
@ -20,7 +20,7 @@ describe('A ForgotPasswordHandler', (): void => {
|
||||
let handler: ForgotPasswordHandler;
|
||||
|
||||
beforeEach(async(): Promise<void> => {
|
||||
request = createPostFormRequest({ email });
|
||||
operation = createPostFormOperation({ email });
|
||||
|
||||
accountStore = {
|
||||
generateForgotPasswordRecord: jest.fn().mockResolvedValue(recordId),
|
||||
@ -44,21 +44,21 @@ describe('A ForgotPasswordHandler', (): void => {
|
||||
});
|
||||
|
||||
it('errors on non-string emails.', async(): Promise<void> => {
|
||||
request = createPostFormRequest({});
|
||||
await expect(handler.handle({ request })).rejects.toThrow('Email required');
|
||||
request = createPostFormRequest({ email: [ 'email', 'email2' ]});
|
||||
await expect(handler.handle({ request })).rejects.toThrow('Email required');
|
||||
operation = createPostFormOperation({});
|
||||
await expect(handler.handle({ operation })).rejects.toThrow('Email required');
|
||||
operation = createPostFormOperation({ email: [ 'email', 'email2' ]});
|
||||
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> => {
|
||||
(accountStore.generateForgotPasswordRecord as jest.Mock).mockRejectedValueOnce('error');
|
||||
await expect(handler.handle({ request })).resolves
|
||||
await expect(handler.handle({ operation })).resolves
|
||||
.toEqual({ type: 'response', details: { email }});
|
||||
expect(emailSender.handleSafe).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
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 }});
|
||||
expect(emailSender.handleSafe).toHaveBeenCalledTimes(1);
|
||||
expect(emailSender.handleSafe).toHaveBeenLastCalledWith({
|
||||
|
@ -3,7 +3,7 @@ import type {
|
||||
} from '../../../../../../src/identity/interaction/email-password/handler/InteractionHandler';
|
||||
import { LoginHandler } from '../../../../../../src/identity/interaction/email-password/handler/LoginHandler';
|
||||
import type { AccountStore } from '../../../../../../src/identity/interaction/email-password/storage/AccountStore';
|
||||
import { createPostFormRequest } from './Util';
|
||||
import { createPostFormOperation } from './Util';
|
||||
|
||||
describe('A LoginHandler', (): void => {
|
||||
const webId = 'http://alice.test.com/card#me';
|
||||
@ -23,29 +23,29 @@ describe('A LoginHandler', (): void => {
|
||||
});
|
||||
|
||||
it('errors on invalid emails.', async(): Promise<void> => {
|
||||
input.request = createPostFormRequest({});
|
||||
input.operation = createPostFormOperation({});
|
||||
let prom = handler.handle(input);
|
||||
await expect(prom).rejects.toThrow('Email required');
|
||||
await expect(prom).rejects.toThrow(expect.objectContaining({ prefilled: {}}));
|
||||
input.request = createPostFormRequest({ email: [ 'a', 'b' ]});
|
||||
input.operation = createPostFormOperation({ email: [ 'a', 'b' ]});
|
||||
prom = handler.handle(input);
|
||||
await expect(prom).rejects.toThrow('Email required');
|
||||
await expect(prom).rejects.toThrow(expect.objectContaining({ prefilled: { }}));
|
||||
});
|
||||
|
||||
it('errors on invalid passwords.', async(): Promise<void> => {
|
||||
input.request = createPostFormRequest({ email });
|
||||
input.operation = createPostFormOperation({ email });
|
||||
let prom = handler.handle(input);
|
||||
await expect(prom).rejects.toThrow('Password required');
|
||||
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);
|
||||
await expect(prom).rejects.toThrow('Password required');
|
||||
await expect(prom).rejects.toThrow(expect.objectContaining({ prefilled: { email }}));
|
||||
});
|
||||
|
||||
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!'));
|
||||
const prom = handler.handle(input);
|
||||
await expect(prom).rejects.toThrow('auth failed!');
|
||||
@ -53,7 +53,7 @@ describe('A LoginHandler', (): 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({
|
||||
type: 'complete',
|
||||
details: { webId, shouldRemember: false },
|
||||
|
@ -5,12 +5,12 @@ import {
|
||||
import type { AccountStore } from '../../../../../../src/identity/interaction/email-password/storage/AccountStore';
|
||||
import { IdpInteractionError } from '../../../../../../src/identity/interaction/util/IdpInteractionError';
|
||||
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 { IdentifierGenerator } from '../../../../../../src/pods/generate/IdentifierGenerator';
|
||||
import type { PodManager } from '../../../../../../src/pods/PodManager';
|
||||
import type { PodSettings } from '../../../../../../src/pods/settings/PodSettings';
|
||||
import type { HttpRequest } from '../../../../../../src/server/HttpRequest';
|
||||
import { createPostFormRequest } from './Util';
|
||||
import { createPostFormOperation } from './Util';
|
||||
|
||||
describe('A RegistrationHandler', (): void => {
|
||||
// "Correct" values for easy object creation
|
||||
@ -25,7 +25,7 @@ describe('A RegistrationHandler', (): void => {
|
||||
const register = 'true';
|
||||
const createPod = 'true';
|
||||
|
||||
let request: HttpRequest;
|
||||
let operation: Operation;
|
||||
|
||||
const baseUrl = 'http://test.com/';
|
||||
const webIdSuffix = '/profile/card';
|
||||
@ -69,80 +69,80 @@ describe('A RegistrationHandler', (): void => {
|
||||
|
||||
describe('validating data', (): void => {
|
||||
it('rejects array inputs.', async(): Promise<void> => {
|
||||
request = createPostFormRequest({ mydata: [ 'a', 'b' ]});
|
||||
await expect(handler.handle({ request }))
|
||||
operation = createPostFormOperation({ mydata: [ 'a', 'b' ]});
|
||||
await expect(handler.handle({ operation }))
|
||||
.rejects.toThrow('Unexpected multiple values for mydata.');
|
||||
});
|
||||
|
||||
it('errors on invalid emails.', async(): Promise<void> => {
|
||||
request = createPostFormRequest({ email: undefined });
|
||||
await expect(handler.handle({ request }))
|
||||
operation = createPostFormOperation({ email: undefined });
|
||||
await expect(handler.handle({ operation }))
|
||||
.rejects.toThrow('Please enter a valid e-mail address.');
|
||||
|
||||
request = createPostFormRequest({ email: '' });
|
||||
await expect(handler.handle({ request }))
|
||||
operation = createPostFormOperation({ email: '' });
|
||||
await expect(handler.handle({ operation }))
|
||||
.rejects.toThrow('Please enter a valid e-mail address.');
|
||||
|
||||
request = createPostFormRequest({ email: 'invalidEmail' });
|
||||
await expect(handler.handle({ request }))
|
||||
operation = createPostFormOperation({ email: 'invalidEmail' });
|
||||
await expect(handler.handle({ operation }))
|
||||
.rejects.toThrow('Please enter a valid e-mail address.');
|
||||
});
|
||||
|
||||
it('errors when a required WebID is not valid.', async(): Promise<void> => {
|
||||
request = createPostFormRequest({ email, register, webId: undefined });
|
||||
await expect(handler.handle({ request }))
|
||||
operation = createPostFormOperation({ email, register, webId: undefined });
|
||||
await expect(handler.handle({ operation }))
|
||||
.rejects.toThrow('Please enter a valid WebID.');
|
||||
|
||||
request = createPostFormRequest({ email, register, webId: '' });
|
||||
await expect(handler.handle({ request }))
|
||||
operation = createPostFormOperation({ email, register, webId: '' });
|
||||
await expect(handler.handle({ operation }))
|
||||
.rejects.toThrow('Please enter a valid WebID.');
|
||||
});
|
||||
|
||||
it('errors on invalid passwords when registering.', async(): Promise<void> => {
|
||||
request = createPostFormRequest({ email, webId, password, confirmPassword: 'bad', register });
|
||||
await expect(handler.handle({ request }))
|
||||
operation = createPostFormOperation({ email, webId, password, confirmPassword: 'bad', register });
|
||||
await expect(handler.handle({ operation }))
|
||||
.rejects.toThrow('Your password and confirmation did not match.');
|
||||
});
|
||||
|
||||
it('errors on invalid pod names when required.', async(): Promise<void> => {
|
||||
request = createPostFormRequest({ email, webId, createPod, podName: undefined });
|
||||
await expect(handler.handle({ request }))
|
||||
operation = createPostFormOperation({ email, webId, createPod, podName: undefined });
|
||||
await expect(handler.handle({ operation }))
|
||||
.rejects.toThrow('Please specify a Pod name.');
|
||||
|
||||
request = createPostFormRequest({ email, webId, createPod, podName: ' ' });
|
||||
await expect(handler.handle({ request }))
|
||||
operation = createPostFormOperation({ email, webId, createPod, podName: ' ' });
|
||||
await expect(handler.handle({ operation }))
|
||||
.rejects.toThrow('Please specify a Pod name.');
|
||||
|
||||
request = createPostFormRequest({ email, webId, createWebId });
|
||||
await expect(handler.handle({ request }))
|
||||
operation = createPostFormOperation({ email, webId, createWebId });
|
||||
await expect(handler.handle({ operation }))
|
||||
.rejects.toThrow('Please specify a Pod name.');
|
||||
});
|
||||
|
||||
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 }))
|
||||
operation = createPostFormOperation({ email, podName, createWebId });
|
||||
await expect(handler.handle({ operation }))
|
||||
.rejects.toThrow('Please enter a password.');
|
||||
|
||||
request = createPostFormRequest({ email, podName, createWebId, createPod });
|
||||
await expect(handler.handle({ request }))
|
||||
operation = createPostFormOperation({ email, podName, createWebId, createPod });
|
||||
await expect(handler.handle({ operation }))
|
||||
.rejects.toThrow('Please enter a password.');
|
||||
|
||||
request = createPostFormRequest({ email, podName, createWebId, createPod, register });
|
||||
await expect(handler.handle({ request }))
|
||||
operation = createPostFormOperation({ email, podName, createWebId, createPod, register });
|
||||
await expect(handler.handle({ operation }))
|
||||
.rejects.toThrow('Please enter a password.');
|
||||
});
|
||||
|
||||
it('errors when no option is chosen.', async(): Promise<void> => {
|
||||
request = createPostFormRequest({ email, webId });
|
||||
await expect(handler.handle({ request }))
|
||||
operation = createPostFormOperation({ email, webId });
|
||||
await expect(handler.handle({ operation }))
|
||||
.rejects.toThrow('Please register for a WebID or create a Pod.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('handling data', (): void => {
|
||||
it('can register a user.', async(): Promise<void> => {
|
||||
request = createPostFormRequest({ email, webId, password, confirmPassword, register });
|
||||
await expect(handler.handle({ request })).resolves.toEqual({
|
||||
operation = createPostFormOperation({ email, webId, password, confirmPassword, register });
|
||||
await expect(handler.handle({ operation })).resolves.toEqual({
|
||||
details: {
|
||||
email,
|
||||
webId,
|
||||
@ -168,8 +168,8 @@ describe('A RegistrationHandler', (): void => {
|
||||
|
||||
it('can create a pod.', async(): Promise<void> => {
|
||||
const params = { email, webId, podName, createPod };
|
||||
request = createPostFormRequest(params);
|
||||
await expect(handler.handle({ request })).resolves.toEqual({
|
||||
operation = createPostFormOperation(params);
|
||||
await expect(handler.handle({ operation })).resolves.toEqual({
|
||||
details: {
|
||||
email,
|
||||
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> => {
|
||||
const params = { email, webId, password, confirmPassword, podName, register, createPod };
|
||||
podSettings.oidcIssuer = baseUrl;
|
||||
request = createPostFormRequest(params);
|
||||
await expect(handler.handle({ request })).resolves.toEqual({
|
||||
operation = createPostFormOperation(params);
|
||||
await expect(handler.handle({ operation })).resolves.toEqual({
|
||||
details: {
|
||||
email,
|
||||
webId,
|
||||
@ -228,9 +228,9 @@ describe('A RegistrationHandler', (): void => {
|
||||
it('deletes the created account if pod generation fails.', async(): Promise<void> => {
|
||||
const params = { email, webId, password, confirmPassword, podName, register, createPod };
|
||||
podSettings.oidcIssuer = baseUrl;
|
||||
request = createPostFormRequest(params);
|
||||
operation = createPostFormOperation(params);
|
||||
(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).toHaveBeenLastCalledWith({ webId });
|
||||
@ -252,8 +252,8 @@ describe('A RegistrationHandler', (): void => {
|
||||
podSettings.webId = generatedWebID;
|
||||
podSettings.oidcIssuer = baseUrl;
|
||||
|
||||
request = createPostFormRequest(params);
|
||||
await expect(handler.handle({ request })).resolves.toEqual({
|
||||
operation = createPostFormOperation(params);
|
||||
await expect(handler.handle({ operation })).resolves.toEqual({
|
||||
details: {
|
||||
email,
|
||||
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> => {
|
||||
const params = { email, webId, podName, createPod };
|
||||
request = createPostFormRequest(params);
|
||||
operation = createPostFormOperation(params);
|
||||
(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(IdpInteractionError);
|
||||
// Using the cleaned input for prefilled
|
||||
|
@ -2,11 +2,11 @@ import {
|
||||
ResetPasswordHandler,
|
||||
} from '../../../../../../src/identity/interaction/email-password/handler/ResetPasswordHandler';
|
||||
import type { AccountStore } from '../../../../../../src/identity/interaction/email-password/storage/AccountStore';
|
||||
import type { HttpRequest } from '../../../../../../src/server/HttpRequest';
|
||||
import { createPostFormRequest } from './Util';
|
||||
import type { Operation } from '../../../../../../src/ldp/operations/Operation';
|
||||
import { createPostFormOperation } from './Util';
|
||||
|
||||
describe('A ResetPasswordHandler', (): void => {
|
||||
let request: HttpRequest;
|
||||
let operation: Operation;
|
||||
const recordId = '123456';
|
||||
const url = `/resetURL/${recordId}`;
|
||||
const email = 'alice@test.email';
|
||||
@ -25,28 +25,28 @@ describe('A ResetPasswordHandler', (): void => {
|
||||
|
||||
it('errors for non-string recordIds.', async(): Promise<void> => {
|
||||
const errorMessage = 'Invalid request. Open the link from your email again';
|
||||
request = createPostFormRequest({});
|
||||
await expect(handler.handle({ request })).rejects.toThrow(errorMessage);
|
||||
request = createPostFormRequest({}, '');
|
||||
await expect(handler.handle({ request })).rejects.toThrow(errorMessage);
|
||||
operation = createPostFormOperation({});
|
||||
await expect(handler.handle({ operation })).rejects.toThrow(errorMessage);
|
||||
operation = createPostFormOperation({}, '');
|
||||
await expect(handler.handle({ operation })).rejects.toThrow(errorMessage);
|
||||
});
|
||||
|
||||
it('errors for invalid passwords.', async(): Promise<void> => {
|
||||
const errorMessage = 'Your password and confirmation did not match.';
|
||||
request = createPostFormRequest({ password: 'password!', confirmPassword: 'otherPassword!' }, url);
|
||||
await expect(handler.handle({ request })).rejects.toThrow(errorMessage);
|
||||
operation = createPostFormOperation({ password: 'password!', confirmPassword: 'otherPassword!' }, url);
|
||||
await expect(handler.handle({ operation })).rejects.toThrow(errorMessage);
|
||||
});
|
||||
|
||||
it('errors for invalid emails.', async(): Promise<void> => {
|
||||
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);
|
||||
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> => {
|
||||
request = createPostFormRequest({ password: 'password!', confirmPassword: 'password!' }, url);
|
||||
await expect(handler.handle({ request })).resolves.toEqual({ type: 'response' });
|
||||
operation = createPostFormOperation({ password: 'password!', confirmPassword: 'password!' }, url);
|
||||
await expect(handler.handle({ operation })).resolves.toEqual({ type: 'response' });
|
||||
expect(accountStore.getForgotPasswordRecord).toHaveBeenCalledTimes(1);
|
||||
expect(accountStore.getForgotPasswordRecord).toHaveBeenLastCalledWith(recordId);
|
||||
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> => {
|
||||
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');
|
||||
await expect(handler.handle({ request })).rejects.toThrow(errorMessage);
|
||||
await expect(handler.handle({ operation })).rejects.toThrow(errorMessage);
|
||||
});
|
||||
});
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { stringify } from 'querystring';
|
||||
import type { HttpRequest } from '../../../../../../src/server/HttpRequest';
|
||||
import { guardedStreamFrom } from '../../../../../../src/util/StreamUtil';
|
||||
import type { Operation } from '../../../../../../src/ldp/operations/Operation';
|
||||
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
|
||||
@ -8,9 +8,11 @@ import { guardedStreamFrom } from '../../../../../../src/util/StreamUtil';
|
||||
* @param data - Object to encode.
|
||||
* @param url - URL value of the request.
|
||||
*/
|
||||
export function createPostFormRequest(data: NodeJS.Dict<any>, url?: string): HttpRequest {
|
||||
const request = guardedStreamFrom(stringify(data)) as HttpRequest;
|
||||
request.headers = { 'content-type': 'application/x-www-form-urlencoded' };
|
||||
request.url = url;
|
||||
return request;
|
||||
export function createPostFormOperation(data: NodeJS.Dict<any>, url?: string): Operation {
|
||||
return {
|
||||
method: 'POST',
|
||||
preferences: {},
|
||||
target: { path: url ?? 'http://test.com/' },
|
||||
body: new BasicRepresentation(stringify(data), 'application/x-www-form-urlencoded'),
|
||||
};
|
||||
}
|
||||
|
@ -2,9 +2,9 @@ import { stringify } from 'querystring';
|
||||
import {
|
||||
getFormDataRequestBody,
|
||||
} 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 { guardedStreamFrom } from '../../../../../src/util/StreamUtil';
|
||||
|
||||
describe('FormDataUtil', (): void => {
|
||||
describe('#getFormDataRequestBody', (): void => {
|
||||
@ -15,9 +15,13 @@ describe('FormDataUtil', (): void => {
|
||||
|
||||
it('converts the body to an object.', async(): Promise<void> => {
|
||||
const data = { test: 'test!', moreTest: '!TEST!' };
|
||||
const stream = guardedStreamFrom(stringify(data)) as HttpRequest;
|
||||
stream.headers = { 'content-type': 'application/x-www-form-urlencoded' };
|
||||
await expect(getFormDataRequestBody(stream)).resolves.toEqual(data);
|
||||
const operation: Operation = {
|
||||
method: 'GET',
|
||||
preferences: {},
|
||||
target: { path: '' },
|
||||
body: new BasicRepresentation(stringify(data), 'application/x-www-form-urlencoded'),
|
||||
};
|
||||
await expect(getFormDataRequestBody(operation)).resolves.toEqual(data);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,31 +1,39 @@
|
||||
import { createResponse } from 'node-mocks-http';
|
||||
import type { HttpResponse } from '../../../../src';
|
||||
import { readableToString } from '../../../../src';
|
||||
import type { ResponseDescription, ResponseWriter, HttpResponse } from '../../../../src';
|
||||
|
||||
import { TemplateHandler } from '../../../../src/server/util/TemplateHandler';
|
||||
import type { TemplateEngine } from '../../../../src/util/templates/TemplateEngine';
|
||||
|
||||
describe('A TemplateHandler', (): void => {
|
||||
const contents = { contents: 'contents' };
|
||||
const templateFile = '/templates/main.html.ejs';
|
||||
let responseWriter: jest.Mocked<ResponseWriter>;
|
||||
let templateEngine: jest.Mocked<TemplateEngine>;
|
||||
let response: HttpResponse;
|
||||
const response: HttpResponse = {} as any;
|
||||
|
||||
beforeEach((): void => {
|
||||
responseWriter = {
|
||||
handleSafe: jest.fn(),
|
||||
} as any;
|
||||
|
||||
templateEngine = {
|
||||
render: jest.fn().mockResolvedValue('rendered'),
|
||||
};
|
||||
response = createResponse() as HttpResponse;
|
||||
});
|
||||
|
||||
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 });
|
||||
|
||||
expect(templateEngine.render).toHaveBeenCalledTimes(1);
|
||||
expect(templateEngine.render).toHaveBeenCalledWith(contents, { templateFile });
|
||||
|
||||
expect(response.getHeaders()).toHaveProperty('content-type', 'text/html');
|
||||
expect((response as any)._isEndCalled()).toBe(true);
|
||||
expect((response as any)._getData()).toBe('rendered');
|
||||
expect((response as any)._getStatusCode()).toBe(200);
|
||||
expect(responseWriter.handleSafe).toHaveBeenCalledTimes(1);
|
||||
const input: { response: HttpResponse; result: ResponseDescription } = responseWriter.handleSafe.mock.calls[0][0];
|
||||
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user