mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
feat: Integrate ErrorHandler and remove ResponseWriter error support
This commit is contained in:
parent
e1f95877da
commit
57d77e941d
@ -41,6 +41,7 @@
|
||||
"@id": "urn:solid-server:default:HttpServerFactory",
|
||||
"@type": "BaseHttpServerFactory",
|
||||
"handler": { "@id": "urn:solid-server:default:HttpHandler" },
|
||||
"options_showStackTrace": { "@id": "urn:solid-server:default:variable:showStackTrace" },
|
||||
"options_https": true,
|
||||
"options_key": "/path/to/server.key",
|
||||
"options_cert": "/path/to/server.cert"
|
||||
|
@ -9,6 +9,7 @@
|
||||
"@id": "urn:solid-server:default:HttpServerFactory",
|
||||
"@type": "BaseHttpServerFactory",
|
||||
"handler": { "@id": "urn:solid-server:default:HttpHandler" },
|
||||
"options_showStackTrace": { "@id": "urn:solid-server:default:variable:showStackTrace" },
|
||||
"options_https": true,
|
||||
"options_key": "/path/to/server.key",
|
||||
"options_cert": "/path/to/server.cert"
|
||||
|
@ -5,7 +5,8 @@
|
||||
"comment": "Creates a server that supports HTTP requests.",
|
||||
"@id": "urn:solid-server:default:ServerFactory",
|
||||
"@type": "BaseHttpServerFactory",
|
||||
"handler": { "@id": "urn:solid-server:default:HttpHandler" }
|
||||
"handler": { "@id": "urn:solid-server:default:HttpHandler" },
|
||||
"options_showStackTrace": { "@id": "urn:solid-server:default:variable:showStackTrace" }
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -8,7 +8,8 @@
|
||||
"baseServerFactory": {
|
||||
"@id": "urn:solid-server:default:HttpServerFactory",
|
||||
"@type": "BaseHttpServerFactory",
|
||||
"handler": { "@id": "urn:solid-server:default:HttpHandler" }
|
||||
"handler": { "@id": "urn:solid-server:default:HttpHandler" },
|
||||
"options_showStackTrace": { "@id": "urn:solid-server:default:variable:showStackTrace" }
|
||||
},
|
||||
"webSocketHandler": {
|
||||
"@type": "UnsecureWebSocketsProtocol",
|
||||
|
@ -21,7 +21,8 @@
|
||||
"providerFactory": { "@id": "urn:solid-server:default:IdentityProviderFactory" },
|
||||
"interactionPolicy": { "@id": "urn:solid-server:auth:password:AccountInteractionPolicy" },
|
||||
"interactionHttpHandler": { "@id": "urn:solid-server:auth:password:InteractionHttpHandler" },
|
||||
"errorResponseWriter": { "@type": "ErrorResponseWriter" }
|
||||
"errorHandler": { "@id": "urn:solid-server:default:ErrorHandler" },
|
||||
"responseWriter": { "@id": "urn:solid-server:default:ResponseWriter" }
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -7,7 +7,8 @@
|
||||
"@type": "IdentityProviderFactory",
|
||||
"issuer": { "@id": "urn:solid-server:default:variable:baseUrl" },
|
||||
"configurationFactory": { "@id": "urn:solid-server:default:IdpConfigurationFactory" },
|
||||
"errorResponseWriter": { "@type": "ErrorResponseWriter" }
|
||||
"errorHandler": { "@id": "urn:solid-server:default:ErrorHandler" },
|
||||
"responseWriter": { "@id": "urn:solid-server:default:ResponseWriter" }
|
||||
},
|
||||
{
|
||||
"comment": "Sets up the JWKS and cookie keys.",
|
||||
|
21
config/ldp/handler/components/error-handler.json
Normal file
21
config/ldp/handler/components/error-handler.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^0.0.0/components/context.jsonld",
|
||||
"@graph": [
|
||||
{
|
||||
"comment": "Changes an error into a valid representation to send as a response.",
|
||||
"@id": "urn:solid-server:default:ErrorHandler",
|
||||
"@type": "WaterfallHandler",
|
||||
"handlers": [
|
||||
{
|
||||
"@type": "ConvertingErrorHandler",
|
||||
"converter": { "@id": "urn:solid-server:default:RepresentationConverter" },
|
||||
"showStackTrace": { "@id": "urn:solid-server:default:variable:showStackTrace" }
|
||||
},
|
||||
{
|
||||
"@type": "TextErrorHandler",
|
||||
"showStackTrace": { "@id": "urn:solid-server:default:variable:showStackTrace" }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
@ -4,14 +4,8 @@
|
||||
{
|
||||
"comment": "Writes the result to the response stream.",
|
||||
"@id": "urn:solid-server:default:ResponseWriter",
|
||||
"@type": "WaterfallHandler",
|
||||
"handlers": [
|
||||
{ "@type": "ErrorResponseWriter" },
|
||||
{
|
||||
"@type": "BasicResponseWriter",
|
||||
"metadataWriter": { "@id": "urn:solid-server:default:MetadataWriter" }
|
||||
}
|
||||
]
|
||||
"@type": "BasicResponseWriter",
|
||||
"metadataWriter": { "@id": "urn:solid-server:default:MetadataWriter" }
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
{
|
||||
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^0.0.0/components/context.jsonld",
|
||||
"import": [
|
||||
"files-scs:config/ldp/handler/components/error-handler.json",
|
||||
"files-scs:config/ldp/handler/components/operation-handler.json",
|
||||
"files-scs:config/ldp/handler/components/request-parser.json",
|
||||
"files-scs:config/ldp/handler/components/response-writer.json"
|
||||
@ -15,6 +16,7 @@
|
||||
"args_permissionsExtractor": { "@id": "urn:solid-server:default:PermissionsExtractor" },
|
||||
"args_authorizer": { "@id": "urn:solid-server:default:Authorizer" },
|
||||
"args_operationHandler": { "@id": "urn:solid-server:default:OperationHandler" },
|
||||
"args_errorHandler": { "@id": "urn:solid-server:default:ErrorHandler" },
|
||||
"args_responseWriter": { "@id": "urn:solid-server:default:ResponseWriter" }
|
||||
}
|
||||
]
|
||||
|
@ -23,7 +23,8 @@
|
||||
"@type": "ChainedConverter",
|
||||
"converters": [
|
||||
{ "@id": "urn:solid-server:default:RdfToQuadConverter" },
|
||||
{ "@id": "urn:solid-server:default:QuadToRdfConverter" }
|
||||
{ "@id": "urn:solid-server:default:QuadToRdfConverter" },
|
||||
{ "@type": "ErrorToQuadConverter" }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
@ -23,9 +23,15 @@
|
||||
"@type": "Variable"
|
||||
},
|
||||
{
|
||||
"comment": "The max level of log messages that should be output.",
|
||||
"@id": "urn:solid-server:default:variable:loggingLevel",
|
||||
"@type": "Variable"
|
||||
},
|
||||
{
|
||||
"comment": "Whether error stack traces should be shown in the server responses.",
|
||||
"@id": "urn:solid-server:default:variable:showStackTrace",
|
||||
"@type": "Variable"
|
||||
},
|
||||
{
|
||||
"comment": "Path to the JSON file used to store configuration for dynamic pods.",
|
||||
"@id": "urn:solid-server:default:variable:podConfigJson",
|
||||
|
@ -6,8 +6,9 @@ import type { AnyObject,
|
||||
Account,
|
||||
ErrorOut } from 'oidc-provider';
|
||||
import { Provider } from 'oidc-provider';
|
||||
import type { ErrorHandler } from '../ldp/http/ErrorHandler';
|
||||
import type { ResponseWriter } from '../ldp/http/ResponseWriter';
|
||||
|
||||
import type { RepresentationPreferences } from '../ldp/representation/RepresentationPreferences';
|
||||
import type { ConfigurationFactory } from './configuration/ConfigurationFactory';
|
||||
|
||||
/**
|
||||
@ -18,13 +19,15 @@ import type { ConfigurationFactory } from './configuration/ConfigurationFactory'
|
||||
export class IdentityProviderFactory {
|
||||
private readonly issuer: string;
|
||||
private readonly configurationFactory: ConfigurationFactory;
|
||||
private readonly errorResponseWriter: ResponseWriter;
|
||||
private readonly errorHandler: ErrorHandler;
|
||||
private readonly responseWriter: ResponseWriter;
|
||||
|
||||
public constructor(issuer: string, configurationFactory: ConfigurationFactory,
|
||||
errorResponseWriter: ResponseWriter) {
|
||||
errorHandler: ErrorHandler, responseWriter: ResponseWriter) {
|
||||
this.issuer = issuer;
|
||||
this.configurationFactory = configurationFactory;
|
||||
this.errorResponseWriter = errorResponseWriter;
|
||||
this.errorHandler = errorHandler;
|
||||
this.responseWriter = responseWriter;
|
||||
}
|
||||
|
||||
public async createProvider(interactionPolicyOptions: {
|
||||
@ -81,7 +84,9 @@ export class IdentityProviderFactory {
|
||||
},
|
||||
renderError:
|
||||
async(ctx: KoaContextWithOIDC, out: ErrorOut, error: Error): Promise<void> => {
|
||||
await this.errorResponseWriter.handleSafe({ response: ctx.res, result: error });
|
||||
const preferences: RepresentationPreferences = { type: { 'text/plain': 1 }};
|
||||
const result = await this.errorHandler.handleSafe({ error, preferences });
|
||||
await this.responseWriter.handleSafe({ response: ctx.res, result });
|
||||
},
|
||||
};
|
||||
return new Provider(this.issuer, augmentedConfig);
|
||||
|
@ -1,9 +1,11 @@
|
||||
import type { Provider } from 'oidc-provider';
|
||||
import type { ErrorHandler } from '../ldp/http/ErrorHandler';
|
||||
import type { ResponseWriter } from '../ldp/http/ResponseWriter';
|
||||
import type { RepresentationPreferences } from '../ldp/representation/RepresentationPreferences';
|
||||
import { getLoggerFor } from '../logging/LogUtil';
|
||||
import type { HttpHandlerInput } from '../server/HttpHandler';
|
||||
import { HttpHandler } from '../server/HttpHandler';
|
||||
import { isNativeError } from '../util/errors/ErrorUtil';
|
||||
import { assertNativeError, isNativeError } from '../util/errors/ErrorUtil';
|
||||
import type { IdentityProviderFactory } from './IdentityProviderFactory';
|
||||
import type { InteractionHttpHandler } from './interaction/InteractionHttpHandler';
|
||||
import type { InteractionPolicy } from './interaction/InteractionPolicy';
|
||||
@ -26,20 +28,23 @@ export class IdentityProviderHttpHandler extends HttpHandler {
|
||||
private readonly providerFactory: IdentityProviderFactory;
|
||||
private readonly interactionPolicy: InteractionPolicy;
|
||||
private readonly interactionHttpHandler: InteractionHttpHandler;
|
||||
private readonly errorResponseWriter: ResponseWriter;
|
||||
private readonly errorHandler: ErrorHandler;
|
||||
private readonly responseWriter: ResponseWriter;
|
||||
private provider?: Provider;
|
||||
|
||||
public constructor(
|
||||
providerFactory: IdentityProviderFactory,
|
||||
interactionPolicy: InteractionPolicy,
|
||||
interactionHttpHandler: InteractionHttpHandler,
|
||||
errorResponseWriter: ResponseWriter,
|
||||
errorHandler: ErrorHandler,
|
||||
responseWriter: ResponseWriter,
|
||||
) {
|
||||
super();
|
||||
this.providerFactory = providerFactory;
|
||||
this.interactionPolicy = interactionPolicy;
|
||||
this.interactionHttpHandler = interactionHttpHandler;
|
||||
this.errorResponseWriter = errorResponseWriter;
|
||||
this.errorHandler = errorHandler;
|
||||
this.responseWriter = responseWriter;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -70,11 +75,10 @@ export class IdentityProviderHttpHandler extends HttpHandler {
|
||||
try {
|
||||
await this.interactionHttpHandler.handle({ ...input, provider });
|
||||
} catch (error: unknown) {
|
||||
// ResponseWriter can only handle native errors
|
||||
if (!isNativeError(error)) {
|
||||
throw error;
|
||||
}
|
||||
await this.errorResponseWriter.handleSafe({ response: input.response, result: error });
|
||||
assertNativeError(error);
|
||||
const preferences: RepresentationPreferences = { type: { 'text/plain': 1 }};
|
||||
const result = await this.errorHandler.handleSafe({ error, preferences });
|
||||
await this.responseWriter.handleSafe({ response: input.response, result });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -109,7 +109,8 @@ export * from './ldp/http/AcceptPreferenceParser';
|
||||
export * from './ldp/http/BasicRequestParser';
|
||||
export * from './ldp/http/BasicResponseWriter';
|
||||
export * from './ldp/http/BodyParser';
|
||||
export * from './ldp/http/ErrorResponseWriter';
|
||||
export * from './ldp/http/ConvertingErrorHandler';
|
||||
export * from './ldp/http/ErrorHandler';
|
||||
export * from './ldp/http/OriginalUrlExtractor';
|
||||
export * from './ldp/http/Patch';
|
||||
export * from './ldp/http/PreferenceParser';
|
||||
@ -119,6 +120,7 @@ export * from './ldp/http/ResponseWriter';
|
||||
export * from './ldp/http/SparqlUpdateBodyParser';
|
||||
export * from './ldp/http/SparqlUpdatePatch';
|
||||
export * from './ldp/http/TargetExtractor';
|
||||
export * from './ldp/http/TextErrorHandler';
|
||||
|
||||
// LDP/Operations
|
||||
export * from './ldp/operations/DeleteOperationHandler';
|
||||
@ -216,9 +218,10 @@ export * from './storage/accessors/SparqlDataAccessor';
|
||||
export * from './storage/conversion/ChainedConverter';
|
||||
export * from './storage/conversion/ConstantConverter';
|
||||
export * from './storage/conversion/ContentTypeReplacer';
|
||||
export * from './storage/conversion/ConversionUtil';
|
||||
export * from './storage/conversion/ErrorToQuadConverter';
|
||||
export * from './storage/conversion/IfNeededConverter';
|
||||
export * from './storage/conversion/PassthroughConverter';
|
||||
export * from './storage/conversion/ConversionUtil';
|
||||
export * from './storage/conversion/QuadToRdfConverter';
|
||||
export * from './storage/conversion/RdfToQuadConverter';
|
||||
export * from './storage/conversion/RepresentationConverter';
|
||||
|
@ -6,7 +6,8 @@ import type { HttpHandlerInput } from '../server/HttpHandler';
|
||||
import { HttpHandler } from '../server/HttpHandler';
|
||||
import type { HttpRequest } from '../server/HttpRequest';
|
||||
import type { HttpResponse } from '../server/HttpResponse';
|
||||
import { isNativeError } from '../util/errors/ErrorUtil';
|
||||
import { assertNativeError } from '../util/errors/ErrorUtil';
|
||||
import type { ErrorHandler } from './http/ErrorHandler';
|
||||
import type { RequestParser } from './http/RequestParser';
|
||||
import type { ResponseDescription } from './http/response/ResponseDescription';
|
||||
import type { ResponseWriter } from './http/ResponseWriter';
|
||||
@ -14,6 +15,7 @@ import type { Operation } from './operations/Operation';
|
||||
import type { OperationHandler } from './operations/OperationHandler';
|
||||
import type { PermissionSet } from './permissions/PermissionSet';
|
||||
import type { PermissionsExtractor } from './permissions/PermissionsExtractor';
|
||||
import type { RepresentationPreferences } from './representation/RepresentationPreferences';
|
||||
|
||||
/**
|
||||
* Collection of handlers needed for {@link AuthenticatedLdpHandler} to function.
|
||||
@ -39,6 +41,10 @@ export interface AuthenticatedLdpHandlerArgs {
|
||||
* Executed the operation.
|
||||
*/
|
||||
operationHandler: OperationHandler;
|
||||
/**
|
||||
* Converts errors to a serializable format.
|
||||
*/
|
||||
errorHandler: ErrorHandler;
|
||||
/**
|
||||
* Writes out the response of the operation.
|
||||
*/
|
||||
@ -54,6 +60,7 @@ export class AuthenticatedLdpHandler extends HttpHandler {
|
||||
private readonly permissionsExtractor!: PermissionsExtractor;
|
||||
private readonly authorizer!: Authorizer;
|
||||
private readonly operationHandler!: OperationHandler;
|
||||
private readonly errorHandler!: ErrorHandler;
|
||||
private readonly responseWriter!: ResponseWriter;
|
||||
private readonly logger = getLoggerFor(this);
|
||||
|
||||
@ -91,16 +98,16 @@ export class AuthenticatedLdpHandler extends HttpHandler {
|
||||
* @returns A promise resolving when the handling is finished.
|
||||
*/
|
||||
public async handle(input: HttpHandlerInput): Promise<void> {
|
||||
let writeData: { response: HttpResponse; result: ResponseDescription | Error };
|
||||
let writeData: { response: HttpResponse; result: ResponseDescription };
|
||||
|
||||
try {
|
||||
writeData = { response: input.response, result: await this.runHandlers(input.request) };
|
||||
} catch (error: unknown) {
|
||||
if (isNativeError(error)) {
|
||||
writeData = { response: input.response, result: error };
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
assertNativeError(error);
|
||||
// We don't know the preferences yet at this point
|
||||
const preferences: RepresentationPreferences = { type: { 'text/plain': 1 }};
|
||||
const result = await this.errorHandler.handleSafe({ error, preferences });
|
||||
writeData = { response: input.response, result };
|
||||
}
|
||||
|
||||
await this.responseWriter.handleSafe(writeData);
|
||||
@ -119,6 +126,20 @@ export class AuthenticatedLdpHandler extends HttpHandler {
|
||||
const operation: Operation = await this.requestParser.handleSafe(request);
|
||||
this.logger.verbose(`Parsed ${operation.method} operation on ${operation.target.path}`);
|
||||
|
||||
try {
|
||||
return await this.handleOperation(request, operation);
|
||||
} catch (error: unknown) {
|
||||
assertNativeError(error);
|
||||
return await this.errorHandler.handleSafe({ error, preferences: operation.preferences });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the operation object.
|
||||
* Runs all non-RequestParser handlers.
|
||||
* This way the preferences can be used in case an error needs to be written.
|
||||
*/
|
||||
private async handleOperation(request: HttpRequest, operation: Operation): Promise<ResponseDescription> {
|
||||
const credentials: Credentials = await this.credentialsExtractor.handleSafe(request);
|
||||
this.logger.verbose(`Extracted credentials: ${credentials.webId}`);
|
||||
|
||||
|
@ -1,32 +0,0 @@
|
||||
import { getLoggerFor } from '../../logging/LogUtil';
|
||||
import type { HttpResponse } from '../../server/HttpResponse';
|
||||
import { isNativeError } from '../../util/errors/ErrorUtil';
|
||||
import { HttpError } from '../../util/errors/HttpError';
|
||||
import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError';
|
||||
import type { ResponseDescription } from './response/ResponseDescription';
|
||||
import { ResponseWriter } from './ResponseWriter';
|
||||
|
||||
/**
|
||||
* Writes to an {@link HttpResponse} based on the incoming Error.
|
||||
*/
|
||||
export class ErrorResponseWriter extends ResponseWriter {
|
||||
protected readonly logger = getLoggerFor(this);
|
||||
|
||||
public async canHandle(input: { response: HttpResponse; result: ResponseDescription | Error }): Promise<void> {
|
||||
if (!isNativeError(input.result)) {
|
||||
throw new NotImplementedHttpError('Only errors are supported');
|
||||
}
|
||||
}
|
||||
|
||||
public async handle(input: { response: HttpResponse; result: Error }): Promise<void> {
|
||||
let code = 500;
|
||||
if (HttpError.isInstance(input.result)) {
|
||||
code = input.result.statusCode;
|
||||
}
|
||||
input.response.setHeader('content-type', 'text/plain');
|
||||
input.response.writeHead(code);
|
||||
input.response.end(typeof input.result.stack === 'string' ?
|
||||
`${input.result.stack}\n` :
|
||||
`${input.result.name}: ${input.result.message}\n`);
|
||||
}
|
||||
}
|
@ -3,8 +3,7 @@ import { AsyncHandler } from '../../util/handlers/AsyncHandler';
|
||||
import type { ResponseDescription } from './response/ResponseDescription';
|
||||
|
||||
/**
|
||||
* Writes to the HttpResponse.
|
||||
* Response depends on the operation result and potentially which errors was thrown.
|
||||
* Writes the ResponseDescription to the HttpResponse.
|
||||
*/
|
||||
export abstract class ResponseWriter
|
||||
extends AsyncHandler<{ response: HttpResponse; result: ResponseDescription | Error }> {}
|
||||
extends AsyncHandler<{ response: HttpResponse; result: ResponseDescription }> {}
|
||||
|
@ -21,6 +21,11 @@ export interface BaseHttpServerOptions {
|
||||
*/
|
||||
https?: boolean;
|
||||
|
||||
/**
|
||||
* If the error stack traces should be shown in case the HttpHandler throws one.
|
||||
*/
|
||||
showStackTrace?: boolean;
|
||||
|
||||
key?: string;
|
||||
cert?: string;
|
||||
|
||||
@ -61,7 +66,14 @@ export class BaseHttpServerFactory implements HttpServerFactory {
|
||||
this.logger.info(`Received ${request.method} request for ${request.url}`);
|
||||
await this.handler.handleSafe({ request: guardStream(request), response });
|
||||
} catch (error: unknown) {
|
||||
const errMsg = isNativeError(error) ? `${error.name}: ${error.message}\n${error.stack}` : 'Unknown error.';
|
||||
let errMsg: string;
|
||||
if (!isNativeError(error)) {
|
||||
errMsg = 'Unknown error.\n';
|
||||
} else if (this.options.showStackTrace && error.stack) {
|
||||
errMsg = `${error.stack}\n`;
|
||||
} else {
|
||||
errMsg = `${error.name}: ${error.message}\n`;
|
||||
}
|
||||
this.logger.error(errMsg);
|
||||
if (response.headersSent) {
|
||||
response.end();
|
||||
|
@ -2,7 +2,9 @@ import type { interactionPolicy, Configuration, KoaContextWithOIDC } from 'oidc-
|
||||
import type { ConfigurationFactory } from '../../../src/identity/configuration/ConfigurationFactory';
|
||||
import { IdentityProviderFactory } from '../../../src/identity/IdentityProviderFactory';
|
||||
import type { InteractionPolicy } from '../../../src/identity/interaction/InteractionPolicy';
|
||||
import type { ErrorHandler } from '../../../src/ldp/http/ErrorHandler';
|
||||
import type { ResponseWriter } from '../../../src/ldp/http/ResponseWriter';
|
||||
import type { HttpResponse } from '../../../src/server/HttpResponse';
|
||||
|
||||
jest.mock('oidc-provider', (): any => ({
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
@ -17,7 +19,8 @@ describe('An IdentityProviderFactory', (): void => {
|
||||
};
|
||||
const webId = 'http://alice.test.com/card#me';
|
||||
let configuration: any;
|
||||
let errorResponseWriter: ResponseWriter;
|
||||
let errorHandler: ErrorHandler;
|
||||
let responseWriter: ResponseWriter;
|
||||
let factory: IdentityProviderFactory;
|
||||
|
||||
beforeEach(async(): Promise<void> => {
|
||||
@ -26,11 +29,13 @@ describe('An IdentityProviderFactory', (): void => {
|
||||
createConfiguration: async(): Promise<any> => configuration,
|
||||
};
|
||||
|
||||
errorResponseWriter = {
|
||||
handleSafe: jest.fn(),
|
||||
errorHandler = {
|
||||
handleSafe: jest.fn().mockResolvedValue({ statusCode: 500 }),
|
||||
} as any;
|
||||
|
||||
factory = new IdentityProviderFactory(issuer, configurationFactory, errorResponseWriter);
|
||||
responseWriter = { handleSafe: jest.fn() } as any;
|
||||
|
||||
factory = new IdentityProviderFactory(issuer, configurationFactory, errorHandler, responseWriter);
|
||||
});
|
||||
|
||||
it('has fixed default values.', async(): Promise<void> => {
|
||||
@ -65,9 +70,14 @@ describe('An IdentityProviderFactory', (): void => {
|
||||
aud: 'solid',
|
||||
});
|
||||
|
||||
await expect(result.config.renderError({ res: 'response!' }, null, 'error!')).resolves.toBeUndefined();
|
||||
expect(errorResponseWriter.handleSafe).toHaveBeenCalledTimes(1);
|
||||
expect(errorResponseWriter.handleSafe).toHaveBeenLastCalledWith({ response: 'response!', result: 'error!' });
|
||||
// Test the renderError function
|
||||
const response: HttpResponse = { } as any;
|
||||
await expect(result.config.renderError({ res: response }, null, 'error!')).resolves.toBeUndefined();
|
||||
expect(errorHandler.handleSafe).toHaveBeenCalledTimes(1);
|
||||
expect(errorHandler.handleSafe)
|
||||
.toHaveBeenLastCalledWith({ error: 'error!', preferences: { type: { 'text/plain': 1 }}});
|
||||
expect(responseWriter.handleSafe).toHaveBeenCalledTimes(1);
|
||||
expect(responseWriter.handleSafe).toHaveBeenLastCalledWith({ response, result: { statusCode: 500 }});
|
||||
});
|
||||
|
||||
it('overwrites fields from the factory config.', async(): Promise<void> => {
|
||||
|
@ -3,6 +3,8 @@ import type { IdentityProviderFactory } from '../../../src/identity/IdentityProv
|
||||
import { IdentityProviderHttpHandler } from '../../../src/identity/IdentityProviderHttpHandler';
|
||||
import type { InteractionHttpHandler } from '../../../src/identity/interaction/InteractionHttpHandler';
|
||||
import type { InteractionPolicy } from '../../../src/identity/interaction/InteractionPolicy';
|
||||
import type { ErrorHandler } from '../../../src/ldp/http/ErrorHandler';
|
||||
import type { ResponseDescription } from '../../../src/ldp/http/response/ResponseDescription';
|
||||
import type { ResponseWriter } from '../../../src/ldp/http/ResponseWriter';
|
||||
import type { HttpRequest } from '../../../src/server/HttpRequest';
|
||||
import type { HttpResponse } from '../../../src/server/HttpResponse';
|
||||
@ -16,7 +18,8 @@ describe('An IdentityProviderHttpHandler', (): void => {
|
||||
url: (ctx: KoaContextWithOIDC): string => `/idp/interaction/${ctx.oidc.uid}`,
|
||||
};
|
||||
let interactionHttpHandler: InteractionHttpHandler;
|
||||
let errorResponseWriter: ResponseWriter;
|
||||
let errorHandler: ErrorHandler;
|
||||
let responseWriter: ResponseWriter;
|
||||
let provider: Provider;
|
||||
let handler: IdentityProviderHttpHandler;
|
||||
|
||||
@ -34,15 +37,16 @@ describe('An IdentityProviderHttpHandler', (): void => {
|
||||
handle: jest.fn(),
|
||||
} as any;
|
||||
|
||||
errorResponseWriter = {
|
||||
handleSafe: jest.fn(),
|
||||
} as any;
|
||||
errorHandler = { handleSafe: jest.fn() } as any;
|
||||
|
||||
responseWriter = { handleSafe: jest.fn() } as any;
|
||||
|
||||
handler = new IdentityProviderHttpHandler(
|
||||
providerFactory,
|
||||
idpPolicy,
|
||||
interactionHttpHandler,
|
||||
errorResponseWriter,
|
||||
errorHandler,
|
||||
responseWriter,
|
||||
);
|
||||
});
|
||||
|
||||
@ -52,7 +56,7 @@ describe('An IdentityProviderHttpHandler', (): void => {
|
||||
expect(provider.callback).toHaveBeenCalledTimes(1);
|
||||
expect(provider.callback).toHaveBeenLastCalledWith(request, response);
|
||||
expect(interactionHttpHandler.handle).toHaveBeenCalledTimes(0);
|
||||
expect(errorResponseWriter.handleSafe).toHaveBeenCalledTimes(0);
|
||||
expect(responseWriter.handleSafe).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('calls the interaction handler if it can handle the input.', async(): Promise<void> => {
|
||||
@ -60,18 +64,22 @@ describe('An IdentityProviderHttpHandler', (): void => {
|
||||
expect(provider.callback).toHaveBeenCalledTimes(0);
|
||||
expect(interactionHttpHandler.handle).toHaveBeenCalledTimes(1);
|
||||
expect(interactionHttpHandler.handle).toHaveBeenLastCalledWith({ request, response, provider });
|
||||
expect(errorResponseWriter.handleSafe).toHaveBeenCalledTimes(0);
|
||||
expect(responseWriter.handleSafe).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('calls the errorResponseWriter if there was an issue with the interaction handler.', async(): Promise<void> => {
|
||||
it('returns an error response if there was an issue with the interaction handler.', async(): Promise<void> => {
|
||||
const error = new Error('error!');
|
||||
const errorResponse: ResponseDescription = { statusCode: 500 };
|
||||
(interactionHttpHandler.handle as jest.Mock).mockRejectedValueOnce(error);
|
||||
(errorHandler.handleSafe as jest.Mock).mockResolvedValueOnce(errorResponse);
|
||||
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
|
||||
expect(provider.callback).toHaveBeenCalledTimes(0);
|
||||
expect(interactionHttpHandler.handle).toHaveBeenCalledTimes(1);
|
||||
expect(interactionHttpHandler.handle).toHaveBeenLastCalledWith({ request, response, provider });
|
||||
expect(errorResponseWriter.handleSafe).toHaveBeenCalledTimes(1);
|
||||
expect(errorResponseWriter.handleSafe).toHaveBeenLastCalledWith({ response, result: error });
|
||||
expect(errorHandler.handleSafe).toHaveBeenCalledTimes(1);
|
||||
expect(errorHandler.handleSafe).toHaveBeenLastCalledWith({ error, preferences: { type: { 'text/plain': 1 }}});
|
||||
expect(responseWriter.handleSafe).toHaveBeenCalledTimes(1);
|
||||
expect(responseWriter.handleSafe).toHaveBeenLastCalledWith({ response, result: errorResponse });
|
||||
});
|
||||
|
||||
it('re-throws the error if it is not a native Error.', async(): Promise<void> => {
|
||||
|
@ -1,80 +1,117 @@
|
||||
import type { CredentialsExtractor } from '../../../src/authentication/CredentialsExtractor';
|
||||
import type { Authorizer } from '../../../src/authorization/Authorizer';
|
||||
import type { Credentials } from '../../../src/authentication/Credentials';
|
||||
import type { Authorization } from '../../../src/authorization/Authorization';
|
||||
import type { AuthenticatedLdpHandlerArgs } from '../../../src/ldp/AuthenticatedLdpHandler';
|
||||
import { AuthenticatedLdpHandler } from '../../../src/ldp/AuthenticatedLdpHandler';
|
||||
import type { RequestParser } from '../../../src/ldp/http/RequestParser';
|
||||
import type { ResponseWriter } from '../../../src/ldp/http/ResponseWriter';
|
||||
import { ResetResponseDescription } from '../../../src/ldp/http/response/ResetResponseDescription';
|
||||
import type { ResponseDescription } from '../../../src/ldp/http/response/ResponseDescription';
|
||||
import type { Operation } from '../../../src/ldp/operations/Operation';
|
||||
import type { OperationHandler } from '../../../src/ldp/operations/OperationHandler';
|
||||
import type { PermissionsExtractor } from '../../../src/ldp/permissions/PermissionsExtractor';
|
||||
import type { PermissionSet } from '../../../src/ldp/permissions/PermissionSet';
|
||||
import type { RepresentationPreferences } from '../../../src/ldp/representation/RepresentationPreferences';
|
||||
import * as LogUtil from '../../../src/logging/LogUtil';
|
||||
import type { HttpRequest } from '../../../src/server/HttpRequest';
|
||||
import type { HttpResponse } from '../../../src/server/HttpResponse';
|
||||
import { StaticAsyncHandler } from '../../util/StaticAsyncHandler';
|
||||
|
||||
describe('An AuthenticatedLdpHandler', (): void => {
|
||||
const request: HttpRequest = {} as any;
|
||||
const response: HttpResponse = {} as any;
|
||||
const preferences: RepresentationPreferences = { type: { 'text/turtle': 0.9 }};
|
||||
let operation: Operation;
|
||||
const credentials: Credentials = {};
|
||||
const permissions: PermissionSet = { read: true, write: false, append: false, control: false };
|
||||
const authorization: Authorization = { addMetadata: jest.fn() };
|
||||
const result: ResponseDescription = new ResetResponseDescription();
|
||||
const errorResult: ResponseDescription = { statusCode: 500 };
|
||||
let args: AuthenticatedLdpHandlerArgs;
|
||||
let responseFn: jest.Mock<Promise<void>, [any]>;
|
||||
let handler: AuthenticatedLdpHandler;
|
||||
|
||||
beforeEach(async(): Promise<void> => {
|
||||
const requestParser: RequestParser = new StaticAsyncHandler(true, ({ target: 'target' }) as any);
|
||||
const credentialsExtractor: CredentialsExtractor = new StaticAsyncHandler(true, 'credentials' as any);
|
||||
const permissionsExtractor: PermissionsExtractor = new StaticAsyncHandler(true, 'permissions' as any);
|
||||
const authorizer: Authorizer = new StaticAsyncHandler(true, 'authorizer' as any);
|
||||
const operationHandler: OperationHandler = new StaticAsyncHandler(true, 'operation' as any);
|
||||
const responseWriter: ResponseWriter = new StaticAsyncHandler(true, 'response' as any);
|
||||
|
||||
responseFn = jest.fn(async(input: any): Promise<void> => {
|
||||
if (!input) {
|
||||
throw new Error('error');
|
||||
}
|
||||
});
|
||||
responseWriter.canHandle = responseFn;
|
||||
|
||||
args = { requestParser, credentialsExtractor, permissionsExtractor, authorizer, operationHandler, responseWriter };
|
||||
operation = { target: { path: 'identifier' }, method: 'GET', preferences };
|
||||
args = {
|
||||
requestParser: {
|
||||
canHandle: jest.fn(),
|
||||
handleSafe: jest.fn().mockResolvedValue(operation),
|
||||
} as any,
|
||||
credentialsExtractor: { handleSafe: jest.fn().mockResolvedValue(credentials) } as any,
|
||||
permissionsExtractor: { handleSafe: jest.fn().mockResolvedValue(permissions) } as any,
|
||||
authorizer: { handleSafe: jest.fn().mockResolvedValue(authorization) } as any,
|
||||
operationHandler: { handleSafe: jest.fn().mockResolvedValue(result) } as any,
|
||||
errorHandler: { handleSafe: jest.fn().mockResolvedValue(errorResult) } as any,
|
||||
responseWriter: { handleSafe: jest.fn() } as any,
|
||||
};
|
||||
handler = new AuthenticatedLdpHandler(args);
|
||||
});
|
||||
|
||||
it('can be created.', async(): Promise<void> => {
|
||||
expect(new AuthenticatedLdpHandler(args)).toBeInstanceOf(AuthenticatedLdpHandler);
|
||||
});
|
||||
|
||||
it('can check if it handles input.', async(): Promise<void> => {
|
||||
const handler = new AuthenticatedLdpHandler(args);
|
||||
it('can not handle the input if the RequestParser rejects it.', async(): Promise<void> => {
|
||||
(args.requestParser.canHandle as jest.Mock).mockRejectedValueOnce(new Error('bad data!'));
|
||||
await expect(handler.canHandle({ request, response })).rejects.toThrow('bad data!');
|
||||
expect(args.requestParser.canHandle).toHaveBeenLastCalledWith(request);
|
||||
});
|
||||
|
||||
await expect(handler.canHandle(
|
||||
{ request: {} as HttpRequest, response: {} as HttpResponse },
|
||||
)).resolves.toBeUndefined();
|
||||
it('can handle the input if the RequestParser can handle it.', async(): Promise<void> => {
|
||||
await expect(handler.canHandle({ request, response })).resolves.toBeUndefined();
|
||||
expect(args.requestParser.canHandle).toHaveBeenCalledTimes(1);
|
||||
expect(args.requestParser.canHandle).toHaveBeenLastCalledWith(request);
|
||||
});
|
||||
|
||||
it('can handle input.', async(): Promise<void> => {
|
||||
const handler = new AuthenticatedLdpHandler(args);
|
||||
|
||||
await expect(handler.handle({ request: 'request' as any, response: 'response' as any })).resolves.toBeUndefined();
|
||||
expect(responseFn).toHaveBeenCalledTimes(1);
|
||||
expect(responseFn).toHaveBeenLastCalledWith({ response: 'response', result: 'operation' as any });
|
||||
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
|
||||
expect(args.requestParser.handleSafe).toHaveBeenCalledTimes(1);
|
||||
expect(args.requestParser.handleSafe).toHaveBeenLastCalledWith(request);
|
||||
expect(args.credentialsExtractor.handleSafe).toHaveBeenCalledTimes(1);
|
||||
expect(args.credentialsExtractor.handleSafe).toHaveBeenLastCalledWith(request);
|
||||
expect(args.permissionsExtractor.handleSafe).toHaveBeenCalledTimes(1);
|
||||
expect(args.permissionsExtractor.handleSafe).toHaveBeenLastCalledWith(operation);
|
||||
expect(args.authorizer.handleSafe).toHaveBeenCalledTimes(1);
|
||||
expect(args.authorizer.handleSafe)
|
||||
.toHaveBeenLastCalledWith({ credentials, identifier: { path: 'identifier' }, permissions });
|
||||
expect(operation.authorization).toBe(authorization);
|
||||
expect(args.operationHandler.handleSafe).toHaveBeenCalledTimes(1);
|
||||
expect(args.operationHandler.handleSafe).toHaveBeenLastCalledWith(operation);
|
||||
expect(args.errorHandler.handleSafe).toHaveBeenCalledTimes(0);
|
||||
expect(args.responseWriter.handleSafe).toHaveBeenCalledTimes(1);
|
||||
expect(args.responseWriter.handleSafe).toHaveBeenLastCalledWith({ response, result });
|
||||
});
|
||||
|
||||
it('sends an error to the output if a handler does not support the input.', async(): Promise<void> => {
|
||||
args.requestParser = new StaticAsyncHandler(false, {} as Operation);
|
||||
const handler = new AuthenticatedLdpHandler(args);
|
||||
it('sets preferences to text/plain in case of an error during request parsing.', async(): Promise<void> => {
|
||||
const error = new Error('bad request!');
|
||||
(args.requestParser.handleSafe as jest.Mock).mockRejectedValueOnce(new Error('bad request!'));
|
||||
|
||||
await expect(handler.handle({ request: 'request' as any, response: {} as HttpResponse })).resolves.toBeUndefined();
|
||||
expect(responseFn).toHaveBeenCalledTimes(1);
|
||||
expect(responseFn.mock.calls[0][0].result).toBeInstanceOf(Error);
|
||||
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
|
||||
expect(args.requestParser.handleSafe).toHaveBeenCalledTimes(1);
|
||||
expect(args.credentialsExtractor.handleSafe).toHaveBeenCalledTimes(0);
|
||||
expect(args.errorHandler.handleSafe).toHaveBeenCalledTimes(1);
|
||||
expect(args.errorHandler.handleSafe).toHaveBeenLastCalledWith({ error, preferences: { type: { 'text/plain': 1 }}});
|
||||
expect(args.responseWriter.handleSafe).toHaveBeenCalledTimes(1);
|
||||
expect(args.responseWriter.handleSafe).toHaveBeenLastCalledWith({ response, result: errorResult });
|
||||
});
|
||||
|
||||
it('errors if the response writer does not support the result.', async(): Promise< void> => {
|
||||
args.responseWriter = new StaticAsyncHandler(false, undefined);
|
||||
const handler = new AuthenticatedLdpHandler(args);
|
||||
it('sets preferences to the request preferences if they were parsed before the error.', async(): Promise<void> => {
|
||||
const error = new Error('bad request!');
|
||||
(args.credentialsExtractor.handleSafe as jest.Mock).mockRejectedValueOnce(new Error('bad request!'));
|
||||
|
||||
await expect(handler.handle({ request: 'request' as any, response: {} as HttpResponse })).rejects.toThrow(Error);
|
||||
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
|
||||
expect(args.requestParser.handleSafe).toHaveBeenCalledTimes(1);
|
||||
expect(args.credentialsExtractor.handleSafe).toHaveBeenCalledTimes(1);
|
||||
expect(args.authorizer.handleSafe).toHaveBeenCalledTimes(0);
|
||||
expect(args.errorHandler.handleSafe).toHaveBeenCalledTimes(1);
|
||||
expect(args.errorHandler.handleSafe).toHaveBeenLastCalledWith({ error, preferences });
|
||||
expect(args.responseWriter.handleSafe).toHaveBeenCalledTimes(1);
|
||||
expect(args.responseWriter.handleSafe).toHaveBeenLastCalledWith({ response, result: errorResult });
|
||||
});
|
||||
|
||||
it('errors an invalid object was thrown by a handler.', async(): Promise< void> => {
|
||||
args.authorizer.handle = async(): Promise<any> => {
|
||||
throw 'apple';
|
||||
};
|
||||
const handler = new AuthenticatedLdpHandler(args);
|
||||
it('logs an error if authorization failed.', async(): Promise<void> => {
|
||||
const logger = { verbose: jest.fn() };
|
||||
const mock = jest.spyOn(LogUtil, 'getLoggerFor');
|
||||
mock.mockReturnValueOnce(logger as any);
|
||||
handler = new AuthenticatedLdpHandler(args);
|
||||
(args.authorizer.handleSafe as jest.Mock).mockRejectedValueOnce(new Error('bad auth!'));
|
||||
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
|
||||
expect(logger.verbose).toHaveBeenLastCalledWith('Authorization failed: bad auth!');
|
||||
|
||||
await expect(handler.handle({ request: 'request' as any, response: {} as HttpResponse })).rejects.toEqual('apple');
|
||||
mock.mockRestore();
|
||||
});
|
||||
});
|
||||
|
@ -1,51 +0,0 @@
|
||||
import { EventEmitter } from 'events';
|
||||
import type { MockResponse } from 'node-mocks-http';
|
||||
import { createResponse } from 'node-mocks-http';
|
||||
import { ErrorResponseWriter } from '../../../../src/ldp/http/ErrorResponseWriter';
|
||||
import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError';
|
||||
|
||||
describe('An ErrorResponseWriter', (): void => {
|
||||
const writer = new ErrorResponseWriter();
|
||||
let response: MockResponse<any>;
|
||||
|
||||
beforeEach(async(): Promise<void> => {
|
||||
response = createResponse({ eventEmitter: EventEmitter });
|
||||
});
|
||||
|
||||
it('requires the input to be an error.', async(): Promise<void> => {
|
||||
await expect(writer.canHandle({ response, result: new Error('error') }))
|
||||
.resolves.toBeUndefined();
|
||||
await expect(writer.canHandle({ response, result: { statusCode: 200 }}))
|
||||
.rejects.toThrow(NotImplementedHttpError);
|
||||
});
|
||||
|
||||
it('responds with 500 if an error if there is an error.', async(): Promise<void> => {
|
||||
await writer.handle({ response, result: new Error('error') });
|
||||
expect(response._isEndCalled()).toBeTruthy();
|
||||
expect(response._getStatusCode()).toBe(500);
|
||||
expect(response._getData()).toMatch('Error: error');
|
||||
});
|
||||
|
||||
it('responds with the given statuscode if there is an HttpError.', async(): Promise<void> => {
|
||||
const error = new NotImplementedHttpError('error');
|
||||
await writer.handle({ response, result: error });
|
||||
expect(response._isEndCalled()).toBeTruthy();
|
||||
expect(response._getStatusCode()).toBe(error.statusCode);
|
||||
expect(response._getData()).toMatch('NotImplementedHttpError: error');
|
||||
});
|
||||
|
||||
it('responds with the error name and message when no stack trace is lazily generated.', async(): Promise<void> => {
|
||||
const error = new Error('error');
|
||||
error.stack = undefined;
|
||||
await writer.handle({ response, result: error });
|
||||
expect(response._isEndCalled()).toBeTruthy();
|
||||
expect(response._getStatusCode()).toBe(500);
|
||||
expect(response._getData()).toMatch('Error: error');
|
||||
});
|
||||
|
||||
it('ends its response with a newline if there is an error.', async(): Promise<void> => {
|
||||
await writer.handle({ response, result: new Error('error') });
|
||||
expect(response._isEndCalled()).toBeTruthy();
|
||||
expect(response._getData().endsWith('\n')).toBeTruthy();
|
||||
});
|
||||
});
|
@ -66,12 +66,12 @@ describe('A BaseHttpServerFactory', (): void => {
|
||||
await expect(request(server).get('/').expect(404)).resolves.toBeDefined();
|
||||
});
|
||||
|
||||
it('writes an error to the HTTP response.', async(): Promise<void> => {
|
||||
it('writes an error to the HTTP response without the stack trace.', async(): Promise<void> => {
|
||||
handler.handleSafe.mockRejectedValueOnce(new Error('dummyError'));
|
||||
|
||||
const res = await request(server).get('/').expect(500);
|
||||
expect(res.headers['content-type']).toBe('text/plain; charset=utf-8');
|
||||
expect(res.text).toContain('dummyError');
|
||||
expect(res.text).toBe('Error: dummyError\n');
|
||||
});
|
||||
|
||||
it('does not write an error if the response had been started.', async(): Promise<void> => {
|
||||
@ -91,4 +91,29 @@ describe('A BaseHttpServerFactory', (): void => {
|
||||
expect(res.text).toContain('Unknown error.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('with showStackTrace enabled', (): void => {
|
||||
const httpOptions = {
|
||||
http: true,
|
||||
showStackTrace: true,
|
||||
};
|
||||
|
||||
beforeAll(async(): Promise<void> => {
|
||||
const factory = new BaseHttpServerFactory(handler, httpOptions);
|
||||
server = factory.startServer(port);
|
||||
});
|
||||
|
||||
afterAll(async(): Promise<void> => {
|
||||
server.close();
|
||||
});
|
||||
|
||||
it('throws unknown errors if its handler throw non-Error objects.', async(): Promise<void> => {
|
||||
const error = new Error('dummyError');
|
||||
handler.handleSafe.mockRejectedValueOnce(error);
|
||||
|
||||
const res = await request(server).get('/').expect(500);
|
||||
expect(res.headers['content-type']).toBe('text/plain; charset=utf-8');
|
||||
expect(res.text).toBe(`${error.stack}\n`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user