From e7529271710674c064b3962c808771fb022a1f33 Mon Sep 17 00:00:00 2001 From: Joachim Van Herwegen Date: Fri, 22 Jan 2021 16:34:10 +0100 Subject: [PATCH] fix: Remove all instanceof checks This prevents problems with different environments. Also introduces unit tests to double check HttpError values. --- src/authorization/WebAclAuthorizer.ts | 2 +- src/ldp/AuthenticatedLdpHandler.ts | 3 +- src/ldp/http/BasicResponseWriter.ts | 3 +- src/ldp/http/ErrorResponseWriter.ts | 5 ++- src/ldp/http/SparqlUpdateBodyParser.ts | 4 +- src/pods/PodManagerHttpHandler.ts | 3 +- src/server/ExpressHttpServerFactory.ts | 3 +- src/storage/DataAccessorBasedStore.ts | 13 +++--- src/storage/RoutingResourceStore.ts | 2 +- src/storage/StoreUtil.ts | 4 +- src/storage/accessors/InMemoryDataAccessor.ts | 2 +- src/storage/accessors/SparqlDataAccessor.ts | 5 ++- src/storage/patch/SparqlUpdatePatchHandler.ts | 2 +- src/util/WaterfallHandler.ts | 5 ++- src/util/errors/BadRequestHttpError.ts | 4 ++ src/util/errors/ConflictHttpError.ts | 4 ++ src/util/errors/ErrorUtil.ts | 8 ++++ src/util/errors/ForbiddenHttpError.ts | 4 ++ src/util/errors/HttpError.ts | 9 ++++- src/util/errors/InternalServerError.ts | 4 ++ src/util/errors/MethodNotAllowedHttpError.ts | 4 ++ src/util/errors/NotFoundHttpError.ts | 4 ++ src/util/errors/NotImplementedHttpError.ts | 4 ++ src/util/errors/UnauthorizedHttpError.ts | 4 ++ .../errors/UnsupportedMediaTypeHttpError.ts | 4 ++ test/unit/util/errors/HttpError.test.ts | 40 +++++++++++++++++++ 26 files changed, 124 insertions(+), 25 deletions(-) create mode 100644 src/util/errors/ErrorUtil.ts create mode 100644 test/unit/util/errors/HttpError.test.ts diff --git a/src/authorization/WebAclAuthorizer.ts b/src/authorization/WebAclAuthorizer.ts index e7c6679d3..fca773647 100644 --- a/src/authorization/WebAclAuthorizer.ts +++ b/src/authorization/WebAclAuthorizer.ts @@ -149,7 +149,7 @@ export class WebAclAuthorizer extends Authorizer { const resourceId = await this.aclManager.getAclConstrainedResource(id); return this.filterData(data, recurse ? ACL.default : ACL.accessTo, resourceId.path); } catch (error: unknown) { - if (error instanceof NotFoundHttpError) { + if (NotFoundHttpError.isInstance(error)) { this.logger.debug(`No direct ACL document found for ${id.path}`); } else { this.logger.error(`Error reading ACL for ${id.path}: ${(error as Error).message}`, { error }); diff --git a/src/ldp/AuthenticatedLdpHandler.ts b/src/ldp/AuthenticatedLdpHandler.ts index f6289ca6a..a3bb3e8a6 100644 --- a/src/ldp/AuthenticatedLdpHandler.ts +++ b/src/ldp/AuthenticatedLdpHandler.ts @@ -6,6 +6,7 @@ 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 type { RequestParser } from './http/RequestParser'; import type { ResponseDescription } from './http/response/ResponseDescription'; import type { ResponseWriter } from './http/ResponseWriter'; @@ -95,7 +96,7 @@ export class AuthenticatedLdpHandler extends HttpHandler { try { writeData = { response: input.response, result: await this.runHandlers(input.request) }; } catch (error: unknown) { - if (error instanceof Error) { + if (isNativeError(error)) { writeData = { response: input.response, result: error }; } else { throw error; diff --git a/src/ldp/http/BasicResponseWriter.ts b/src/ldp/http/BasicResponseWriter.ts index 8bcddad13..f71536ba1 100644 --- a/src/ldp/http/BasicResponseWriter.ts +++ b/src/ldp/http/BasicResponseWriter.ts @@ -1,6 +1,7 @@ import { getLoggerFor } from '../../logging/LogUtil'; import type { HttpResponse } from '../../server/HttpResponse'; import { INTERNAL_QUADS } from '../../util/ContentTypes'; +import { isNativeError } from '../../util/errors/ErrorUtil'; import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError'; import { pipeSafely } from '../../util/StreamUtil'; import type { MetadataWriter } from './metadata/MetadataWriter'; @@ -20,7 +21,7 @@ export class BasicResponseWriter extends ResponseWriter { } public async canHandle(input: { response: HttpResponse; result: ResponseDescription | Error }): Promise { - if (input.result instanceof Error || input.result.metadata?.contentType === INTERNAL_QUADS) { + if (isNativeError(input.result) || input.result.metadata?.contentType === INTERNAL_QUADS) { throw new NotImplementedHttpError('Only successful binary responses are supported'); } } diff --git a/src/ldp/http/ErrorResponseWriter.ts b/src/ldp/http/ErrorResponseWriter.ts index 34e5c8aae..5fe7970c5 100644 --- a/src/ldp/http/ErrorResponseWriter.ts +++ b/src/ldp/http/ErrorResponseWriter.ts @@ -1,5 +1,6 @@ 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'; @@ -12,14 +13,14 @@ export class ErrorResponseWriter extends ResponseWriter { protected readonly logger = getLoggerFor(this); public async canHandle(input: { response: HttpResponse; result: ResponseDescription | Error }): Promise { - if (!(input.result instanceof Error)) { + if (!isNativeError(input.result)) { throw new NotImplementedHttpError('Only errors are supported'); } } public async handle(input: { response: HttpResponse; result: Error }): Promise { let code = 500; - if (input.result instanceof HttpError) { + if (HttpError.isInstance(input.result)) { code = input.result.statusCode; } input.response.setHeader('content-type', 'text/plain'); diff --git a/src/ldp/http/SparqlUpdateBodyParser.ts b/src/ldp/http/SparqlUpdateBodyParser.ts index 31941ac0b..95b87d612 100644 --- a/src/ldp/http/SparqlUpdateBodyParser.ts +++ b/src/ldp/http/SparqlUpdateBodyParser.ts @@ -4,12 +4,12 @@ import { translate } from 'sparqlalgebrajs'; import { getLoggerFor } from '../../logging/LogUtil'; import { APPLICATION_SPARQL_UPDATE } from '../../util/ContentTypes'; import { BadRequestHttpError } from '../../util/errors/BadRequestHttpError'; +import { isNativeError } from '../../util/errors/ErrorUtil'; import { UnsupportedMediaTypeHttpError } from '../../util/errors/UnsupportedMediaTypeHttpError'; import { pipeSafely, readableToString } from '../../util/StreamUtil'; import type { BodyParserArgs } from './BodyParser'; import { BodyParser } from './BodyParser'; import type { SparqlUpdatePatch } from './SparqlUpdatePatch'; - /** * {@link BodyParser} that supports `application/sparql-update` content. * Will convert the incoming update string to algebra in a {@link SparqlUpdatePatch}. @@ -34,7 +34,7 @@ export class SparqlUpdateBodyParser extends BodyParser { algebra = translate(sparql, { quads: true, baseIRI: metadata.identifier.value }); } catch (error: unknown) { this.logger.warn('Could not translate SPARQL query to SPARQL algebra', { error }); - if (error instanceof Error) { + if (isNativeError(error)) { throw new BadRequestHttpError(error.message); } throw new BadRequestHttpError(); diff --git a/src/pods/PodManagerHttpHandler.ts b/src/pods/PodManagerHttpHandler.ts index 213316d2f..f652f76df 100644 --- a/src/pods/PodManagerHttpHandler.ts +++ b/src/pods/PodManagerHttpHandler.ts @@ -5,6 +5,7 @@ import { HttpHandler } from '../server/HttpHandler'; import type { HttpRequest } from '../server/HttpRequest'; import type { HttpResponse } from '../server/HttpResponse'; import { BadRequestHttpError } from '../util/errors/BadRequestHttpError'; +import { isNativeError } from '../util/errors/ErrorUtil'; import { InternalServerError } from '../util/errors/InternalServerError'; import { NotImplementedHttpError } from '../util/errors/NotImplementedHttpError'; import type { AgentParser } from './agent/AgentParser'; @@ -59,7 +60,7 @@ export class PodManagerHttpHandler extends HttpHandler { await this.responseWriter.handleSafe({ response, result: new CreatedResponseDescription(id) }); } catch (error: unknown) { - if (error instanceof Error) { + if (isNativeError(error)) { await this.responseWriter.handleSafe({ response, result: error }); } else { await this.responseWriter.handleSafe({ response, result: new InternalServerError('Unexpected error') }); diff --git a/src/server/ExpressHttpServerFactory.ts b/src/server/ExpressHttpServerFactory.ts index 22f72c8b9..94e1f9c86 100644 --- a/src/server/ExpressHttpServerFactory.ts +++ b/src/server/ExpressHttpServerFactory.ts @@ -2,6 +2,7 @@ import type { Server } from 'http'; import type { Express } from 'express'; import express from 'express'; import { getLoggerFor } from '../logging/LogUtil'; +import { isNativeError } from '../util/errors/ErrorUtil'; import { guardStream } from '../util/GuardedStream'; import type { HttpHandler } from './HttpHandler'; import type { HttpServerFactory } from './HttpServerFactory'; @@ -26,7 +27,7 @@ export class ExpressHttpServerFactory implements HttpServerFactory { this.logger.info(`Received request for ${request.url}`); await this.handler.handleSafe({ request: guardStream(request), response }); } catch (error: unknown) { - const errMsg = error instanceof Error ? `${error.name}: ${error.message}\n${error.stack}` : 'Unknown error.'; + const errMsg = isNativeError(error) ? `${error.name}: ${error.message}\n${error.stack}` : 'Unknown error.'; this.logger.error(errMsg); if (response.headersSent) { response.end(); diff --git a/src/storage/DataAccessorBasedStore.ts b/src/storage/DataAccessorBasedStore.ts index c2705a946..5cfcf7bc3 100644 --- a/src/storage/DataAccessorBasedStore.ts +++ b/src/storage/DataAccessorBasedStore.ts @@ -9,6 +9,7 @@ import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifie import { INTERNAL_QUADS } from '../util/ContentTypes'; import { BadRequestHttpError } from '../util/errors/BadRequestHttpError'; import { ConflictHttpError } from '../util/errors/ConflictHttpError'; +import { isNativeError } from '../util/errors/ErrorUtil'; import { ForbiddenHttpError } from '../util/errors/ForbiddenHttpError'; import { MethodNotAllowedHttpError } from '../util/errors/MethodNotAllowedHttpError'; import { NotFoundHttpError } from '../util/errors/NotFoundHttpError'; @@ -194,7 +195,7 @@ export class DataAccessorBasedStore implements ResourceStore { try { return await this.accessor.getMetadata(identifier); } catch (error: unknown) { - if (error instanceof NotFoundHttpError) { + if (NotFoundHttpError.isInstance(error)) { const otherIdentifier = { path: hasSlash ? trimTrailingSlashes(identifier.path) : ensureTrailingSlash(identifier.path) }; @@ -214,7 +215,7 @@ export class DataAccessorBasedStore implements ResourceStore { try { return await this.getNormalizedMetadata(identifier); } catch (error: unknown) { - if (!(error instanceof NotFoundHttpError)) { + if (!NotFoundHttpError.isInstance(error)) { throw error; } } @@ -270,7 +271,7 @@ export class DataAccessorBasedStore implements ResourceStore { quads = await parseQuads(representation.data, { format: contentType, baseIRI: identifier.value }); } } catch (error: unknown) { - if (error instanceof Error) { + if (isNativeError(error)) { throw new BadRequestHttpError(`Can only create containers with RDF data. ${error.message}`); } throw error; @@ -354,8 +355,8 @@ export class DataAccessorBasedStore implements ResourceStore { /** * Checks in a list of types if any of them match a Container type. */ - protected hasContainerType(types: Term[]): boolean { - return types.some((type): boolean => type.value === LDP.Container || type.value === LDP.BasicContainer); + protected hasContainerType(rdfTypes: Term[]): boolean { + return rdfTypes.some((type): boolean => type.value === LDP.Container || type.value === LDP.BasicContainer); } /** @@ -382,7 +383,7 @@ export class DataAccessorBasedStore implements ResourceStore { throw new ForbiddenHttpError(`Creating container ${container.path} conflicts with an existing resource.`); } } catch (error: unknown) { - if (error instanceof NotFoundHttpError) { + if (NotFoundHttpError.isInstance(error)) { // Make sure the parent exists first await this.createRecursiveContainers(this.identifierStrategy.getParentContainer(container)); await this.writeData(container, new BasicRepresentation([], container), true); diff --git a/src/storage/RoutingResourceStore.ts b/src/storage/RoutingResourceStore.ts index 56c37d4ff..03490894f 100644 --- a/src/storage/RoutingResourceStore.ts +++ b/src/storage/RoutingResourceStore.ts @@ -53,7 +53,7 @@ export class RoutingResourceStore implements ResourceStore { try { return await this.rule.handleSafe({ identifier }); } catch (error: unknown) { - if (error instanceof NotImplementedHttpError) { + if (NotImplementedHttpError.isInstance(error)) { throw new NotFoundHttpError(); } throw error; diff --git a/src/storage/StoreUtil.ts b/src/storage/StoreUtil.ts index 93440c24f..f84c18cd6 100644 --- a/src/storage/StoreUtil.ts +++ b/src/storage/StoreUtil.ts @@ -1,6 +1,6 @@ import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier'; -import type { ResourceStore } from '../storage/ResourceStore'; import { NotFoundHttpError } from '../util/errors/NotFoundHttpError'; +import type { ResourceStore } from './ResourceStore'; export async function containsResource(store: ResourceStore, identifier: ResourceIdentifier): Promise { try { @@ -8,7 +8,7 @@ export async function containsResource(store: ResourceStore, identifier: Resourc result.data.destroy(); return true; } catch (error: unknown) { - if (error instanceof NotFoundHttpError) { + if (NotFoundHttpError.isInstance(error)) { return false; } throw error; diff --git a/src/storage/accessors/InMemoryDataAccessor.ts b/src/storage/accessors/InMemoryDataAccessor.ts index 99ee86a35..cb5c479f4 100644 --- a/src/storage/accessors/InMemoryDataAccessor.ts +++ b/src/storage/accessors/InMemoryDataAccessor.ts @@ -72,7 +72,7 @@ export class InMemoryDataAccessor implements DataAccessor { entry.metadata = metadata; } catch (error: unknown) { // Create new entry if it didn't exist yet - if (error instanceof NotFoundHttpError) { + if (NotFoundHttpError.isInstance(error)) { const { parent, name } = this.getParentEntry(identifier); parent.entries[name] = { entries: {}, diff --git a/src/storage/accessors/SparqlDataAccessor.ts b/src/storage/accessors/SparqlDataAccessor.ts index db1039cc1..d31125941 100644 --- a/src/storage/accessors/SparqlDataAccessor.ts +++ b/src/storage/accessors/SparqlDataAccessor.ts @@ -20,6 +20,7 @@ import type { ResourceIdentifier } from '../../ldp/representation/ResourceIdenti import { getLoggerFor } from '../../logging/LogUtil'; import { INTERNAL_QUADS } from '../../util/ContentTypes'; import { ConflictHttpError } from '../../util/errors/ConflictHttpError'; +import { isNativeError } from '../../util/errors/ErrorUtil'; import { NotFoundHttpError } from '../../util/errors/NotFoundHttpError'; import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError'; import { UnsupportedMediaTypeHttpError } from '../../util/errors/UnsupportedMediaTypeHttpError'; @@ -310,7 +311,7 @@ export class SparqlDataAccessor implements DataAccessor { try { return guardStream(await this.fetcher.fetchTriples(this.endpoint, query)); } catch (error: unknown) { - if (error instanceof Error) { + if (isNativeError(error)) { this.logger.error(`SPARQL endpoint ${this.endpoint} error: ${error.message}`); } throw error; @@ -327,7 +328,7 @@ export class SparqlDataAccessor implements DataAccessor { try { return await this.fetcher.fetchUpdate(this.endpoint, query); } catch (error: unknown) { - if (error instanceof Error) { + if (isNativeError(error)) { this.logger.error(`SPARQL endpoint ${this.endpoint} error: ${error.message}`); } throw error; diff --git a/src/storage/patch/SparqlUpdatePatchHandler.ts b/src/storage/patch/SparqlUpdatePatchHandler.ts index 485f82eaa..c0671278e 100644 --- a/src/storage/patch/SparqlUpdatePatchHandler.ts +++ b/src/storage/patch/SparqlUpdatePatchHandler.ts @@ -115,7 +115,7 @@ export class SparqlUpdatePatchHandler extends PatchHandler { } catch (error: unknown) { // Solid, ยง5.1: "Clients who want to assign a URI to a resource, MUST use PUT and PATCH requests." // https://solid.github.io/specification/protocol#resource-type-heuristics - if (!(error instanceof NotFoundHttpError)) { + if (!NotFoundHttpError.isInstance(error)) { throw error; } this.logger.debug(`Patching new resource ${identifier.path}.`); diff --git a/src/util/WaterfallHandler.ts b/src/util/WaterfallHandler.ts index cae5c5918..4ab45b10e 100644 --- a/src/util/WaterfallHandler.ts +++ b/src/util/WaterfallHandler.ts @@ -1,6 +1,7 @@ import { getLoggerFor } from '../logging/LogUtil'; import type { AsyncHandler } from './AsyncHandler'; import { BadRequestHttpError } from './errors/BadRequestHttpError'; +import { isNativeError } from './errors/ErrorUtil'; import { HttpError } from './errors/HttpError'; import { InternalServerError } from './errors/InternalServerError'; @@ -84,9 +85,9 @@ export class WaterfallHandler implements AsyncHandler { return handler; } catch (error: unknown) { - if (error instanceof HttpError) { + if (HttpError.isInstance(error)) { errors.push(error); - } else if (error instanceof Error) { + } else if (isNativeError(error)) { errors.push(new InternalServerError(error.message)); } else { errors.push(new InternalServerError('Unknown error')); diff --git a/src/util/errors/BadRequestHttpError.ts b/src/util/errors/BadRequestHttpError.ts index 8b979861b..7bc5acd15 100644 --- a/src/util/errors/BadRequestHttpError.ts +++ b/src/util/errors/BadRequestHttpError.ts @@ -12,4 +12,8 @@ export class BadRequestHttpError extends HttpError { public constructor(message?: string) { super(400, 'BadRequestHttpError', message ?? 'The given input is not supported by the server configuration.'); } + + public static isInstance(error: any): error is BadRequestHttpError { + return HttpError.isInstance(error) && error.statusCode === 400; + } } diff --git a/src/util/errors/ConflictHttpError.ts b/src/util/errors/ConflictHttpError.ts index c73f73c36..efb887b06 100644 --- a/src/util/errors/ConflictHttpError.ts +++ b/src/util/errors/ConflictHttpError.ts @@ -6,4 +6,8 @@ export class ConflictHttpError extends HttpError { public constructor(message?: string) { super(409, 'ConflictHttpError', message); } + + public static isInstance(error: any): error is ConflictHttpError { + return HttpError.isInstance(error) && error.statusCode === 409; + } } diff --git a/src/util/errors/ErrorUtil.ts b/src/util/errors/ErrorUtil.ts new file mode 100644 index 000000000..0c4d5c444 --- /dev/null +++ b/src/util/errors/ErrorUtil.ts @@ -0,0 +1,8 @@ +import { types } from 'util'; + +/** + * Checks if the input is an {@link Error}. + */ +export function isNativeError(error: any): error is Error { + return types.isNativeError(error); +} diff --git a/src/util/errors/ForbiddenHttpError.ts b/src/util/errors/ForbiddenHttpError.ts index f12466793..8539490dc 100644 --- a/src/util/errors/ForbiddenHttpError.ts +++ b/src/util/errors/ForbiddenHttpError.ts @@ -7,4 +7,8 @@ export class ForbiddenHttpError extends HttpError { public constructor(message?: string) { super(403, 'ForbiddenHttpError', message); } + + public static isInstance(error: any): error is ForbiddenHttpError { + return HttpError.isInstance(error) && error.statusCode === 403; + } } diff --git a/src/util/errors/HttpError.ts b/src/util/errors/HttpError.ts index a443cca24..9d213dc64 100644 --- a/src/util/errors/HttpError.ts +++ b/src/util/errors/HttpError.ts @@ -1,9 +1,12 @@ +import { isNativeError } from './ErrorUtil'; + /** * A class for all errors that could be thrown by Solid. * All errors inheriting from this should fix the status code thereby hiding the HTTP internals from other components. */ export class HttpError extends Error { - public statusCode: number; + protected static readonly statusCode: number; + public readonly statusCode: number; /** * Creates a new HTTP error. Subclasses should call this with their fixed status code. @@ -16,4 +19,8 @@ export class HttpError extends Error { this.statusCode = statusCode; this.name = name; } + + public static isInstance(error: any): error is HttpError { + return isNativeError(error) && typeof (error as any).statusCode === 'number'; + } } diff --git a/src/util/errors/InternalServerError.ts b/src/util/errors/InternalServerError.ts index 25c7c0843..1619d5861 100644 --- a/src/util/errors/InternalServerError.ts +++ b/src/util/errors/InternalServerError.ts @@ -6,4 +6,8 @@ export class InternalServerError extends HttpError { public constructor(message?: string) { super(500, 'InternalServerError', message); } + + public static isInstance(error: any): error is InternalServerError { + return HttpError.isInstance(error) && error.statusCode === 500; + } } diff --git a/src/util/errors/MethodNotAllowedHttpError.ts b/src/util/errors/MethodNotAllowedHttpError.ts index b00a69e2e..f2163374c 100644 --- a/src/util/errors/MethodNotAllowedHttpError.ts +++ b/src/util/errors/MethodNotAllowedHttpError.ts @@ -6,4 +6,8 @@ export class MethodNotAllowedHttpError extends HttpError { public constructor(message?: string) { super(405, 'MethodNotAllowedHttpError', message); } + + public static isInstance(error: any): error is MethodNotAllowedHttpError { + return HttpError.isInstance(error) && error.statusCode === 405; + } } diff --git a/src/util/errors/NotFoundHttpError.ts b/src/util/errors/NotFoundHttpError.ts index ea5e054c1..7ce6d0624 100644 --- a/src/util/errors/NotFoundHttpError.ts +++ b/src/util/errors/NotFoundHttpError.ts @@ -6,4 +6,8 @@ export class NotFoundHttpError extends HttpError { public constructor(message?: string) { super(404, 'NotFoundHttpError', message); } + + public static isInstance(error: any): error is NotFoundHttpError { + return HttpError.isInstance(error) && error.statusCode === 404; + } } diff --git a/src/util/errors/NotImplementedHttpError.ts b/src/util/errors/NotImplementedHttpError.ts index 1418c3d2c..16df618a5 100644 --- a/src/util/errors/NotImplementedHttpError.ts +++ b/src/util/errors/NotImplementedHttpError.ts @@ -7,4 +7,8 @@ export class NotImplementedHttpError extends HttpError { public constructor(message?: string) { super(501, 'NotImplementedHttpError', message); } + + public static isInstance(error: any): error is NotImplementedHttpError { + return HttpError.isInstance(error) && error.statusCode === 501; + } } diff --git a/src/util/errors/UnauthorizedHttpError.ts b/src/util/errors/UnauthorizedHttpError.ts index fcbfc1c79..f68c69f44 100644 --- a/src/util/errors/UnauthorizedHttpError.ts +++ b/src/util/errors/UnauthorizedHttpError.ts @@ -7,4 +7,8 @@ export class UnauthorizedHttpError extends HttpError { public constructor(message?: string) { super(401, 'UnauthorizedHttpError', message); } + + public static isInstance(error: any): error is UnauthorizedHttpError { + return HttpError.isInstance(error) && error.statusCode === 401; + } } diff --git a/src/util/errors/UnsupportedMediaTypeHttpError.ts b/src/util/errors/UnsupportedMediaTypeHttpError.ts index 8fa562995..0ca1542db 100644 --- a/src/util/errors/UnsupportedMediaTypeHttpError.ts +++ b/src/util/errors/UnsupportedMediaTypeHttpError.ts @@ -7,4 +7,8 @@ export class UnsupportedMediaTypeHttpError extends HttpError { public constructor(message?: string) { super(415, 'UnsupportedMediaTypeHttpError', message); } + + public static isInstance(error: any): error is UnsupportedMediaTypeHttpError { + return HttpError.isInstance(error) && error.statusCode === 415; + } } diff --git a/test/unit/util/errors/HttpError.test.ts b/test/unit/util/errors/HttpError.test.ts new file mode 100644 index 000000000..7feaa96b8 --- /dev/null +++ b/test/unit/util/errors/HttpError.test.ts @@ -0,0 +1,40 @@ +import { BadRequestHttpError } from '../../../../src/util/errors/BadRequestHttpError'; +import { ConflictHttpError } from '../../../../src/util/errors/ConflictHttpError'; +import { ForbiddenHttpError } from '../../../../src/util/errors/ForbiddenHttpError'; +import { HttpError } from '../../../../src/util/errors/HttpError'; +import { InternalServerError } from '../../../../src/util/errors/InternalServerError'; +import { MethodNotAllowedHttpError } from '../../../../src/util/errors/MethodNotAllowedHttpError'; +import { NotFoundHttpError } from '../../../../src/util/errors/NotFoundHttpError'; +import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError'; +import { UnauthorizedHttpError } from '../../../../src/util/errors/UnauthorizedHttpError'; +import { UnsupportedMediaTypeHttpError } from '../../../../src/util/errors/UnsupportedMediaTypeHttpError'; + +// Only used to make typings easier in the tests +class FixedHttpError extends HttpError { + public constructor(message?: string) { + super(0, '', message); + } +} + +describe('An HttpError', (): void => { + const errors: [string, number, typeof FixedHttpError][] = [ + [ 'BadRequestHttpError', 400, BadRequestHttpError ], + [ 'UnauthorizedHttpError', 401, UnauthorizedHttpError ], + [ 'ForbiddenHttpError', 403, ForbiddenHttpError ], + [ 'NotFoundHttpError', 404, NotFoundHttpError ], + [ 'MethodNotAllowedHttpError', 405, MethodNotAllowedHttpError ], + [ 'ConflictHttpError', 409, ConflictHttpError ], + [ 'MethodNotAllowedHttpError', 405, MethodNotAllowedHttpError ], + [ 'UnsupportedMediaTypeHttpError', 415, UnsupportedMediaTypeHttpError ], + [ 'InternalServerError', 500, InternalServerError ], + [ 'NotImplementedHttpError', 501, NotImplementedHttpError ], + ]; + + it.each(errors)('%s is valid', (name, statusCode, constructor): void => { + const instance = new constructor('message'); + expect(constructor.isInstance(instance)).toBeTruthy(); + expect(instance.statusCode).toBe(statusCode); + expect(instance.name).toBe(name); + expect(instance.message).toBe('message'); + }); +});