feat: Add logging

* feat: Add logging

* refactor: Configure the logger for the tests once globally

* feat: Add logging

* fix: Fix ESLint errors due to merge conflicts

* Review log and error messages.

* refactor: Cleanup a bit

* refactor: Change to logger info calls

Co-authored-by: Ruben Verborgh <ruben@verborgh.org>
This commit is contained in:
smessie 2020-10-26 10:31:01 +01:00 committed by GitHub
parent 1ef75126ee
commit 99464d9a95
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 232 additions and 107 deletions

View File

@ -13,7 +13,7 @@ module.exports = {
"js"
],
"testEnvironment": "node",
"setupFilesAfterEnv": ["jest-rdf"],
"setupFilesAfterEnv": ["jest-rdf", "<rootDir>/test/util/SetupTests.ts"],
"collectCoverage": true,
"coveragePathIgnorePatterns": [
"/node_modules/",

View File

@ -4,6 +4,7 @@ import type { Credentials } from '../authentication/Credentials';
import type { PermissionSet } from '../ldp/permissions/PermissionSet';
import type { Representation } from '../ldp/representation/Representation';
import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier';
import { getLoggerFor } from '../logging/LogUtil';
import type { ContainerManager } from '../storage/ContainerManager';
import type { ResourceStore } from '../storage/ResourceStore';
import { INTERNAL_QUADS } from '../util/ContentTypes';
@ -21,6 +22,8 @@ import { Authorizer } from './Authorizer';
* Does not support `acl:agentGroup`, `acl:origin` and `acl:trustedApp` yet.
*/
export class WebAclAuthorizer extends Authorizer {
protected readonly logger = getLoggerFor(this);
private readonly aclManager: AclManager;
private readonly containerManager: ContainerManager;
private readonly resourceStore: ResourceStore;
@ -66,7 +69,14 @@ export class WebAclAuthorizer extends Authorizer {
const modeString = ACL[this.capitalize(mode) as 'Write' | 'Read' | 'Append' | 'Control'];
const auths = store.getQuads(null, ACL.mode, modeString, null).map((quad: Quad): Term => quad.subject);
if (!auths.some((term): boolean => this.hasAccess(agent, term, store))) {
throw typeof agent.webID === 'string' ? new ForbiddenHttpError() : new UnauthorizedHttpError();
const isLoggedIn = typeof agent.webID === 'string';
if (isLoggedIn) {
this.logger.warn(`Agent ${agent.webID} has no ${mode} permissions`);
throw new ForbiddenHttpError();
} else {
this.logger.warn(`Unauthenticated agent has no ${mode} permissions`);
throw new UnauthorizedHttpError();
}
}
}
@ -111,18 +121,25 @@ export class WebAclAuthorizer extends Authorizer {
* @returns A store containing the relevant acl triples.
*/
private async getAclRecursive(id: ResourceIdentifier, recurse?: boolean): Promise<Store> {
this.logger.debug(`Trying to read the direct ACL document of ${id.path}`);
try {
const acl = await this.aclManager.getAcl(id);
this.logger.debug(`Trying to read the ACL document ${acl.path}`);
const data = await this.resourceStore.getRepresentation(acl, { type: [{ value: INTERNAL_QUADS, weight: 1 }]});
this.logger.info(`Reading ACL statements from ${acl.path}`);
return this.filterData(data, recurse ? ACL.default : ACL.accessTo, id.path);
} catch (error: unknown) {
if (!(error instanceof NotFoundHttpError)) {
if (error instanceof NotFoundHttpError) {
this.logger.debug(`No direct ACL document found for ${id.path}`);
} else {
this.logger.error(`Error reading ACL for ${id.path}`, { error });
throw error;
}
const parent = await this.containerManager.getContainer(id);
return this.getAclRecursive(parent, true);
}
this.logger.debug(`Traversing to the parent of ${id.path}`);
const parent = await this.containerManager.getContainer(id);
return this.getAclRecursive(parent, true);
}
/**

View File

@ -35,15 +35,16 @@ export class BasicRequestParser extends RequestParser {
// Can handle all requests
}
public async handle(input: HttpRequest): Promise<Operation> {
if (!input.method) {
throw new Error('Missing method.');
public async handle(request: HttpRequest): Promise<Operation> {
const { method } = request;
if (!method) {
throw new Error('No method specified on the HTTP request');
}
const target = await this.targetExtractor.handleSafe(input);
const preferences = await this.preferenceParser.handleSafe(input);
const metadata = await this.metadataExtractor.handleSafe(input);
const body = await this.bodyParser.handleSafe({ request: input, metadata });
const target = await this.targetExtractor.handleSafe(request);
const preferences = await this.preferenceParser.handleSafe(request);
const metadata = await this.metadataExtractor.handleSafe(request);
const body = await this.bodyParser.handleSafe({ request, metadata });
return { method: input.method, target, preferences, body };
return { method, target, preferences, body };
}
}

View File

@ -1,3 +1,4 @@
import { getLoggerFor } from '../../logging/LogUtil';
import type { HttpResponse } from '../../server/HttpResponse';
import { HttpError } from '../../util/errors/HttpError';
import { UnsupportedHttpError } from '../../util/errors/UnsupportedHttpError';
@ -9,9 +10,12 @@ import { ResponseWriter } from './ResponseWriter';
* Still needs a way to write correct status codes for successful operations.
*/
export class BasicResponseWriter extends ResponseWriter {
protected readonly logger = getLoggerFor(this);
public async canHandle(input: { response: HttpResponse; result: ResponseDescription | Error }): Promise<void> {
if (!(input.result instanceof Error) && input.result.body && !input.result.body.binary) {
throw new UnsupportedHttpError('Only binary results and errors are supported.');
this.logger.warn('This writer can only write binary bodies and errors');
throw new UnsupportedHttpError('Only binary results and errors are supported');
}
}

View File

@ -1,4 +1,5 @@
import type { TLSSocket } from 'tls';
import { getLoggerFor } from '../../logging/LogUtil';
import type { HttpRequest } from '../../server/HttpRequest';
import { toCanonicalUriPath } from '../../util/Util';
import type { ResourceIdentifier } from '../representation/ResourceIdentifier';
@ -10,22 +11,28 @@ import { TargetExtractor } from './TargetExtractor';
* TODO: input requires more extensive cleaning/parsing based on headers (see #22).
*/
export class BasicTargetExtractor extends TargetExtractor {
protected readonly logger = getLoggerFor(this);
public async canHandle(): Promise<void> {
// Can handle all URLs
}
public async handle(input: HttpRequest): Promise<ResourceIdentifier> {
if (!input.url) {
throw new Error('Missing URL.');
public async handle(request: HttpRequest): Promise<ResourceIdentifier> {
if (!request.url) {
this.logger.error('The request has no URL');
throw new Error('Missing URL');
}
if (!input.headers.host) {
throw new Error('Missing host.');
if (!request.headers.host) {
this.logger.error('The request has no Host header');
throw new Error('Missing Host header');
}
const isHttps = input.connection && (input.connection as TLSSocket).encrypted;
const isHttps = request.connection && (request.connection as TLSSocket).encrypted;
this.logger.debug(`Request is using HTTPS: ${isHttps}`);
// URL object applies punycode encoding to domain
const base = `http${isHttps ? 's' : ''}://${input.headers.host}`;
const url = toCanonicalUriPath(input.url);
const base = `http${isHttps ? 's' : ''}://${request.headers.host}`;
const url = toCanonicalUriPath(request.url);
const path = new URL(url, base).href;
return { path };

View File

@ -1,3 +1,4 @@
import { getLoggerFor } from '../../logging/LogUtil';
import { UnsupportedHttpError } from '../../util/errors/UnsupportedHttpError';
import type { Representation } from '../representation/Representation';
import type { BodyParserArgs } from './BodyParser';
@ -7,6 +8,8 @@ import { BodyParser } from './BodyParser';
* Converts incoming {@link HttpRequest} to a Representation without any further parsing.
*/
export class RawBodyParser extends BodyParser {
protected readonly logger = getLoggerFor(this);
public async canHandle(): Promise<void> {
// All content-types are supported
}
@ -17,13 +20,15 @@ export class RawBodyParser extends BodyParser {
// RFC7230, §3.3: The presence of a message body in a request
// is signaled by a Content-Length or Transfer-Encoding header field.
if (!request.headers['content-length'] && !request.headers['transfer-encoding']) {
this.logger.debug('HTTP request appears to not have a body, so nothing to parse');
return;
}
// While RFC7231 allows treating a body without content type as an octet stream,
// such an omission likely signals a mistake, so force clients to make this explicit.
if (!request.headers['content-type']) {
throw new UnsupportedHttpError('An HTTP request body was passed without Content-Type header');
this.logger.warn('A body was passed, but the content length was not specified');
throw new UnsupportedHttpError('HTTP request body was passed without Content-Type header');
}
return {

View File

@ -1,6 +1,7 @@
import { PassThrough } from 'stream';
import type { Algebra } from 'sparqlalgebrajs';
import { translate } from 'sparqlalgebrajs';
import { getLoggerFor } from '../../logging/LogUtil';
import { APPLICATION_SPARQL_UPDATE } from '../../util/ContentTypes';
import { UnsupportedHttpError } from '../../util/errors/UnsupportedHttpError';
import { UnsupportedMediaTypeHttpError } from '../../util/errors/UnsupportedMediaTypeHttpError';
@ -14,8 +15,12 @@ import type { SparqlUpdatePatch } from './SparqlUpdatePatch';
* Will convert the incoming update string to algebra in a {@link SparqlUpdatePatch}.
*/
export class SparqlUpdateBodyParser extends BodyParser {
protected readonly logger = getLoggerFor(this);
public async canHandle({ request }: BodyParserArgs): Promise<void> {
if (request.headers['content-type'] !== APPLICATION_SPARQL_UPDATE) {
const contentType = request.headers['content-type'];
if (contentType !== APPLICATION_SPARQL_UPDATE) {
this.logger.debug(`Unsupported content type: ${contentType}`);
throw new UnsupportedMediaTypeHttpError('This parser only supports SPARQL UPDATE data.');
}
}
@ -33,6 +38,7 @@ export class SparqlUpdateBodyParser extends BodyParser {
const sparql = await readableToString(toAlgebraStream);
algebra = translate(sparql, { quads: true });
} catch (error: unknown) {
this.logger.warn('Could not translate SPARQL query to SPARQL algebra', { error });
if (error instanceof Error) {
throw new UnsupportedHttpError(error.message);
}

View File

@ -1,3 +1,4 @@
import { getLoggerFor } from '../../../logging/LogUtil';
import type { HttpRequest } from '../../../server/HttpRequest';
import { UnsupportedHttpError } from '../../../util/errors/UnsupportedHttpError';
import { HTTP } from '../../../util/UriConstants';
@ -8,12 +9,16 @@ import type { MetadataParser } from './MetadataParser';
* Converts the contents of the slug header to metadata.
*/
export class SlugParser implements MetadataParser {
protected readonly logger = getLoggerFor(this);
public async parse(request: HttpRequest, metadata: RepresentationMetadata): Promise<void> {
const { slug } = request.headers;
if (slug) {
if (Array.isArray(slug)) {
throw new UnsupportedHttpError('At most 1 slug header is allowed.');
this.logger.warn(`Expected 0 or 1 Slug headers but received ${slug.length}`);
throw new UnsupportedHttpError('Request has multiple Slug headers');
}
this.logger.debug(`Request Slug is '${slug}'.`);
metadata.set(HTTP.slug, slug);
}
}

View File

@ -18,7 +18,7 @@ export class DeleteOperationHandler extends OperationHandler {
public async canHandle(input: Operation): Promise<void> {
if (input.method !== 'DELETE') {
throw new UnsupportedHttpError('This handler only supports DELETE operations.');
throw new UnsupportedHttpError('This handler only supports DELETE operations');
}
}

View File

@ -18,7 +18,7 @@ export class GetOperationHandler extends OperationHandler {
public async canHandle(input: Operation): Promise<void> {
if (input.method !== 'GET') {
throw new UnsupportedHttpError('This handler only supports GET operations.');
throw new UnsupportedHttpError('This handler only supports GET operations');
}
}

View File

@ -19,7 +19,7 @@ export class HeadOperationHandler extends OperationHandler {
public async canHandle(input: Operation): Promise<void> {
if (input.method !== 'HEAD') {
throw new UnsupportedHttpError('This handler only supports HEAD operations.');
throw new UnsupportedHttpError('This handler only supports HEAD operations');
}
}

View File

@ -1,3 +1,4 @@
import { getLoggerFor } from '../../logging/LogUtil';
import type { ResourceStore } from '../../storage/ResourceStore';
import { UnsupportedHttpError } from '../../util/errors/UnsupportedHttpError';
import type { Operation } from './Operation';
@ -9,6 +10,8 @@ import type { ResponseDescription } from './ResponseDescription';
* Calls the addResource function from a {@link ResourceStore}.
*/
export class PostOperationHandler extends OperationHandler {
protected readonly logger = getLoggerFor(this);
private readonly store: ResourceStore;
public constructor(store: ResourceStore) {
@ -18,13 +21,14 @@ export class PostOperationHandler extends OperationHandler {
public async canHandle(input: Operation): Promise<void> {
if (input.method !== 'POST') {
throw new UnsupportedHttpError('This handler only supports POST operations.');
throw new UnsupportedHttpError('This handler only supports POST operations');
}
}
public async handle(input: Operation): Promise<ResponseDescription> {
if (!input.body) {
throw new UnsupportedHttpError('POST operations require a body.');
this.logger.warn('POST operations require a body');
throw new UnsupportedHttpError('POST operations require a body');
}
const identifier = await this.store.addResource(input.target, input.body);
return { identifier };

View File

@ -1,3 +1,4 @@
import { getLoggerFor } from '../../logging/LogUtil';
import type { ResourceStore } from '../../storage/ResourceStore';
import { UnsupportedHttpError } from '../../util/errors/UnsupportedHttpError';
import type { Operation } from './Operation';
@ -9,6 +10,8 @@ import type { ResponseDescription } from './ResponseDescription';
* Calls the setRepresentation function from a {@link ResourceStore}.
*/
export class PutOperationHandler extends OperationHandler {
protected readonly logger = getLoggerFor(this);
private readonly store: ResourceStore;
public constructor(store: ResourceStore) {
@ -18,13 +21,14 @@ export class PutOperationHandler extends OperationHandler {
public async canHandle(input: Operation): Promise<void> {
if (input.method !== 'PUT') {
throw new UnsupportedHttpError('This handler only supports PUT operations.');
throw new UnsupportedHttpError('This handler only supports PUT operations');
}
}
public async handle(input: Operation): Promise<ResponseDescription> {
if (typeof input.body !== 'object') {
throw new UnsupportedHttpError('PUT operations require a body.');
this.logger.warn('No body specified on PUT request');
throw new UnsupportedHttpError('PUT operations require a body');
}
await this.store.setRepresentation(input.target, input.body);
return { identifier: input.target };

View File

@ -1,3 +1,4 @@
import { getLoggerFor } from '../../logging/LogUtil';
import { UnsupportedHttpError } from '../../util/errors/UnsupportedHttpError';
import type { Operation } from '../operations/Operation';
import type { PermissionSet } from './PermissionSet';
@ -12,8 +13,11 @@ const SUPPORTED_METHODS = new Set([ ...READ_METHODS, ...WRITE_METHODS ]);
* Specifically: GET, HEAD, POST, PUT and DELETE.
*/
export class MethodPermissionsExtractor extends PermissionsExtractor {
protected readonly logger = getLoggerFor(this);
public async canHandle({ method }: Operation): Promise<void> {
if (!SUPPORTED_METHODS.has(method)) {
this.logger.warn(`Unrecognized method ${method}`);
throw new UnsupportedHttpError(`Cannot determine permissions of ${method}`);
}
}

View File

@ -1,5 +1,6 @@
import { DataFactory, Store } from 'n3';
import type { BlankNode, Literal, NamedNode, Quad, Term } from 'rdf-js';
import { getLoggerFor } from '../../logging/LogUtil';
import { toObjectTerm, toNamedNode, isTerm } from '../../util/UriUtil';
export type MetadataOverrideValue = NamedNode | Literal | string | (NamedNode | Literal | string)[];
@ -9,6 +10,8 @@ export type MetadataOverrideValue = NamedNode | Literal | string | (NamedNode |
* Most functions return the metadata object to allow for chaining.
*/
export class RepresentationMetadata {
protected readonly logger = getLoggerFor(this);
private store: Store;
private id: NamedNode | BlankNode;
@ -184,6 +187,7 @@ export class RepresentationMetadata {
return;
}
if (terms.length > 1) {
this.logger.error(`Multiple results for ${typeof predicate === 'string' ? predicate : predicate.value}`);
throw new Error(`Multiple results for ${typeof predicate === 'string' ? predicate : predicate.value}`);
}
return terms[0];

View File

@ -2,6 +2,7 @@ import { promises as fsPromises } from 'fs';
import { posix } from 'path';
import * as mime from 'mime-types';
import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier';
import { getLoggerFor } from '../logging/LogUtil';
import { APPLICATION_OCTET_STREAM, TEXT_TURTLE } from '../util/ContentTypes';
import { NotFoundHttpError } from '../util/errors/NotFoundHttpError';
import { UnsupportedHttpError } from '../util/errors/UnsupportedHttpError';
@ -36,6 +37,8 @@ export interface ResourcePath {
* This new extension is stripped again when generating an identifier.
*/
export class ExtensionBasedMapper implements FileIdentifierMapper {
protected readonly logger = getLoggerFor(this);
private readonly baseRequestURI: string;
private readonly rootFilepath: string;
private readonly types: Record<string, any>;
@ -56,34 +59,38 @@ export class ExtensionBasedMapper implements FileIdentifierMapper {
* @returns A ResourceLink with all the necessary metadata.
*/
public async mapUrlToFilePath(identifier: ResourceIdentifier, contentType?: string): Promise<ResourceLink> {
let path = this.getRelativePath(identifier);
const path = this.getRelativePath(identifier);
if (!path.startsWith('/')) {
throw new UnsupportedHttpError('URL needs a / after the base.');
this.logger.warn(`URL ${identifier.path} needs a / after the base`);
throw new UnsupportedHttpError('URL needs a / after the base');
}
if (path.includes('/..')) {
throw new UnsupportedHttpError('Disallowed /.. segment in URL.');
this.logger.warn(`Disallowed /.. segment in URL ${identifier.path}.`);
throw new UnsupportedHttpError('Disallowed /.. segment in URL');
}
path = this.getAbsolutePath(path);
let filePath = this.getAbsolutePath(path);
// Container
if (identifier.path.endsWith('/')) {
this.logger.debug(`URL ${identifier.path} points to the container ${filePath}`);
return {
identifier,
filePath: path,
filePath,
};
}
// Would conflict with how new extensions get stored
if (/\$\.\w+$/u.test(path)) {
throw new UnsupportedHttpError('Identifiers cannot contain a dollar sign before their extension.');
// Would conflict with how new extensions are stored
if (/\$\.\w+$/u.test(filePath)) {
this.logger.warn(`Identifier ${identifier.path} contains a dollar sign before its extension`);
throw new UnsupportedHttpError('Identifiers cannot contain a dollar sign before their extension');
}
// Existing file
if (!contentType) {
const [ , folder, documentName ] = /^(.*\/)(.*)$/u.exec(path)!;
const [ , folder, documentName ] = /^(.*\/)(.*)$/u.exec(filePath)!;
let fileName: string | undefined;
try {
@ -94,34 +101,40 @@ export class ExtensionBasedMapper implements FileIdentifierMapper {
);
} catch {
// Parent folder does not exist (or is not a folder)
this.logger.warn(`No parent folder for ${identifier.path} found at ${folder}`);
throw new NotFoundHttpError();
}
// File doesn't exist
if (!fileName) {
this.logger.warn(`File for URL ${identifier.path} does not exist in ${folder}`);
throw new NotFoundHttpError();
}
filePath = joinPath(folder, fileName);
this.logger.info(`The path for ${identifier.path} is ${filePath}`);
return {
identifier,
filePath: joinPath(folder, fileName),
filePath,
contentType: this.getContentTypeFromExtension(fileName),
};
}
// If the extension of the identifier matches a different content-type than the one that is given,
// we need to add a new extension to match the correct type.
if (contentType !== this.getContentTypeFromExtension(path)) {
if (contentType !== this.getContentTypeFromExtension(filePath)) {
const extension = mime.extension(contentType);
if (!extension) {
throw new UnsupportedHttpError(`Unsupported content-type ${contentType}.`);
this.logger.warn(`No extension found for ${contentType}`);
throw new UnsupportedHttpError(`Unsupported content type ${contentType}`);
}
path = `${path}$.${extension}`;
filePath += `$.${extension}`;
}
this.logger.info(`The path for ${identifier.path} is ${filePath}`);
return {
identifier,
filePath: path,
filePath,
contentType,
};
}
@ -135,13 +148,16 @@ export class ExtensionBasedMapper implements FileIdentifierMapper {
*/
public async mapFilePathToUrl(filePath: string, isContainer: boolean): Promise<ResourceLink> {
if (!filePath.startsWith(this.rootFilepath)) {
throw new Error(`File ${filePath} is not part of the file storage at ${this.rootFilepath}.`);
this.logger.error(`Trying to access file ${filePath} outside of ${this.rootFilepath}`);
throw new Error(`File ${filePath} is not part of the file storage at ${this.rootFilepath}`);
}
let relative = filePath.slice(this.rootFilepath.length);
if (isContainer) {
const path = ensureTrailingSlash(this.baseRequestURI + encodeUriPathComponents(relative));
this.logger.info(`Container filepath ${filePath} maps to URL ${path}`);
return {
identifier: { path: ensureTrailingSlash(this.baseRequestURI + encodeUriPathComponents(relative)) },
identifier: { path },
filePath,
};
}
@ -153,8 +169,11 @@ export class ExtensionBasedMapper implements FileIdentifierMapper {
relative = relative.slice(0, -(extension.length + 2));
}
const path = trimTrailingSlashes(this.baseRequestURI + encodeUriPathComponents(relative));
this.logger.info(`File ${filePath} (${contentType}) maps to URL ${path}`);
return {
identifier: { path: trimTrailingSlashes(this.baseRequestURI + encodeUriPathComponents(relative)) },
identifier: { path },
filePath,
contentType,
};
@ -203,6 +222,7 @@ export class ExtensionBasedMapper implements FileIdentifierMapper {
*/
private getRelativePath(identifier: ResourceIdentifier): string {
if (!identifier.path.startsWith(this.baseRequestURI)) {
this.logger.warn(`The URL ${identifier.path} is outside of the scope ${this.baseRequestURI}`);
throw new NotFoundHttpError();
}
return decodeUriPathComponents(identifier.path.slice(this.baseRequestURI.length));

View File

@ -3,6 +3,7 @@ import type { Patch } from '../ldp/http/Patch';
import type { Representation } from '../ldp/representation/Representation';
import type { RepresentationPreferences } from '../ldp/representation/RepresentationPreferences';
import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier';
import { getLoggerFor } from '../logging/LogUtil';
import type { AtomicResourceStore } from './AtomicResourceStore';
import type { Conditions } from './Conditions';
import type { ResourceLocker } from './ResourceLocker';
@ -16,6 +17,8 @@ const READ_TIMEOUT = 1000;
* and releases it afterwards.
*/
export class LockingResourceStore implements AtomicResourceStore {
protected readonly logger = getLoggerFor(this);
private readonly source: ResourceStore;
private readonly locks: ResourceLocker;
@ -117,8 +120,10 @@ export class LockingResourceStore implements AtomicResourceStore {
*/
protected createExpiringReadable(source: Readable): Readable {
// Destroy the source when a timeout occurs.
const destroySource = (): void =>
const destroySource = (): void => {
this.logger.info(`Stream reading timout of ${READ_TIMEOUT}ms exceeded; destroying source`);
source.destroy(new Error(`Stream reading timout of ${READ_TIMEOUT}ms exceeded`));
};
let timeout = setTimeout(destroySource, READ_TIMEOUT);
// Cancel the timeout when the source terminates by itself.

View File

@ -1,6 +1,7 @@
import type { Representation } from '../ldp/representation/Representation';
import type { RepresentationPreferences } from '../ldp/representation/RepresentationPreferences';
import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier';
import { getLoggerFor } from '../logging/LogUtil';
import { matchingMediaType } from '../util/Util';
import type { Conditions } from './Conditions';
import type { RepresentationConverter } from './conversion/RepresentationConverter';
@ -17,6 +18,8 @@ import type { ResourceStore } from './ResourceStore';
* if there is a low weight for that type conversions might still be preferred.
*/
export class RepresentationConvertingStore<T extends ResourceStore = ResourceStore> extends PassthroughStore<T> {
protected readonly logger = getLoggerFor(this);
private readonly converter: RepresentationConverter;
public constructor(source: T, converter: RepresentationConverter) {
@ -30,6 +33,7 @@ export class RepresentationConvertingStore<T extends ResourceStore = ResourceSto
if (this.matchesPreferences(representation, preferences)) {
return representation;
}
this.logger.info(`Convert ${identifier.path} from ${representation.metadata.contentType} to ${preferences.type}`);
return this.converter.handleSafe({ identifier, representation, preferences });
}

View File

@ -1,5 +1,6 @@
import AsyncLock from 'async-lock';
import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier';
import { getLoggerFor } from '../logging/LogUtil';
import type { Lock } from './Lock';
import type { ResourceLocker } from './ResourceLocker';
@ -7,6 +8,8 @@ import type { ResourceLocker } from './ResourceLocker';
* A resource locker making use of the `async-lock` library.
*/
export class SingleThreadedResourceLocker implements ResourceLocker {
protected readonly logger = getLoggerFor(this);
private readonly locks: AsyncLock;
public constructor() {
@ -21,9 +24,14 @@ export class SingleThreadedResourceLocker implements ResourceLocker {
* @returns The {@link Lock} when it's available. Its release function needs to be called when finished.
*/
public async acquire(identifier: ResourceIdentifier): Promise<Lock> {
this.logger.verbose(`Acquiring lock for ${identifier.path}`);
return new Promise(async(resolve): Promise<Lock> =>
this.locks.acquire(identifier.path, (done): void => {
resolve({ release: async(): Promise<void> => done() });
this.logger.verbose(`Acquired lock for ${identifier.path}`);
resolve({ release: async(): Promise<void> => {
this.logger.verbose(`Released lock for ${identifier.path}`);
done();
} });
}));
}
}

View File

@ -15,14 +15,14 @@ export class UrlContainerManager implements ContainerManager {
public async getContainer(id: ResourceIdentifier): Promise<ResourceIdentifier> {
const path = this.canonicalUrl(id.path);
if (this.base === path) {
throw new Error('Root does not have a container.');
throw new Error('Root does not have a container');
}
const parentPath = new URL('..', path).toString();
// This probably means there is an issue with the root
if (parentPath === path) {
throw new Error('URL root reached.');
throw new Error('URL root reached');
}
return { path: parentPath };

View File

@ -1,4 +1,5 @@
import type { Representation } from '../../ldp/representation/Representation';
import { getLoggerFor } from '../../logging/LogUtil';
import { matchingMediaType } from '../../util/Util';
import { checkRequest } from './ConversionUtil';
import type { RepresentationConverterArgs } from './RepresentationConverter';
@ -9,6 +10,8 @@ import { TypedRepresentationConverter } from './TypedRepresentationConverter';
* It chains these converters by finding intermediate types that are supported by converters on either side.
*/
export class ChainedConverter extends TypedRepresentationConverter {
protected readonly logger = getLoggerFor(this);
private readonly converters: TypedRepresentationConverter[];
/**
@ -86,6 +89,7 @@ export class ChainedConverter extends TypedRepresentationConverter {
if (weight > bestMatch.weight && matchingMediaType(leftType, rightType)) {
bestMatch = { type: leftType, weight };
if (weight === 1) {
this.logger.info(`${bestMatch.type} is an exact match between ${leftKeys} and ${rightKeys}`);
return bestMatch.type;
}
}
@ -93,9 +97,11 @@ export class ChainedConverter extends TypedRepresentationConverter {
}
if (bestMatch.weight === 0) {
this.logger.error(`No match found between ${leftKeys} and ${rightKeys}`);
throw new Error(`No match found between ${leftKeys} and ${rightKeys}`);
}
this.logger.info(`${bestMatch.type} is the best match between ${leftKeys} and ${rightKeys}`);
return bestMatch.type;
}
}

View File

@ -8,6 +8,7 @@ import type { SparqlUpdatePatch } from '../../ldp/http/SparqlUpdatePatch';
import type { Representation } from '../../ldp/representation/Representation';
import { RepresentationMetadata } from '../../ldp/representation/RepresentationMetadata';
import type { ResourceIdentifier } from '../../ldp/representation/ResourceIdentifier';
import { getLoggerFor } from '../../logging/LogUtil';
import { INTERNAL_QUADS } from '../../util/ContentTypes';
import { UnsupportedHttpError } from '../../util/errors/UnsupportedHttpError';
import { CONTENT_TYPE } from '../../util/UriConstants';
@ -20,6 +21,8 @@ import { PatchHandler } from './PatchHandler';
* Currently all DELETE/INSERT types are supported that have empty where bodies and no variables.
*/
export class SparqlUpdatePatchHandler extends PatchHandler {
protected readonly logger = getLoggerFor(this);
private readonly source: ResourceStore;
private readonly locker: ResourceLocker;
@ -31,14 +34,15 @@ export class SparqlUpdatePatchHandler extends PatchHandler {
public async canHandle(input: {identifier: ResourceIdentifier; patch: SparqlUpdatePatch}): Promise<void> {
if (typeof input.patch.algebra !== 'object') {
throw new UnsupportedHttpError('Only SPARQL update patch requests are supported.');
throw new UnsupportedHttpError('Only SPARQL update patch requests are supported');
}
}
public async handle(input: {identifier: ResourceIdentifier; patch: SparqlUpdatePatch}): Promise<void> {
const op = input.patch.algebra;
if (!this.isDeleteInsert(op)) {
throw new UnsupportedHttpError('Only DELETE/INSERT SPARQL update operations are supported.');
this.logger.warn(`Unsupported operation: ${op.type}`);
throw new UnsupportedHttpError('Only DELETE/INSERT SPARQL update operations are supported');
}
const def = defaultGraph();
@ -46,14 +50,17 @@ export class SparqlUpdatePatchHandler extends PatchHandler {
const inserts = op.insert ?? [];
if (!deletes.every((pattern): boolean => pattern.graph.equals(def))) {
throw new UnsupportedHttpError('GRAPH statements are not supported.');
this.logger.warn('GRAPH statement in DELETE clause');
throw new UnsupportedHttpError('GRAPH statements are not supported');
}
if (!inserts.every((pattern): boolean => pattern.graph.equals(def))) {
throw new UnsupportedHttpError('GRAPH statements are not supported.');
this.logger.warn('GRAPH statement in INSERT clause');
throw new UnsupportedHttpError('GRAPH statements are not supported');
}
if (op.where ?? deletes.some((pattern): boolean =>
someTerms(pattern, (term): boolean => term.termType === 'Variable'))) {
throw new UnsupportedHttpError('WHERE statements are not supported.');
this.logger.warn('WHERE statements are not supported');
throw new UnsupportedHttpError('WHERE statements are not supported');
}
const lock = await this.locker.acquire(input.identifier);

View File

@ -1,3 +1,4 @@
import { getLoggerFor } from '../logging/LogUtil';
import type { AsyncHandler } from './AsyncHandler';
import { HttpError } from './errors/HttpError';
import { InternalServerError } from './errors/InternalServerError';
@ -10,6 +11,8 @@ import { UnsupportedHttpError } from './errors/UnsupportedHttpError';
* allowing for more fine-grained handlers to check before catch-all handlers.
*/
export class CompositeAsyncHandler<TIn, TOut> implements AsyncHandler<TIn, TOut> {
protected readonly logger = getLoggerFor(this);
private readonly handlers: AsyncHandler<TIn, TOut>[];
/**
@ -43,7 +46,8 @@ export class CompositeAsyncHandler<TIn, TOut> implements AsyncHandler<TIn, TOut>
try {
handler = await this.findHandler(input);
} catch {
throw new Error('All handlers failed. This might be the consequence of calling handle before canHandle.');
this.logger.warn('All handlers failed. This might be the consequence of calling handle before canHandle.');
throw new Error('All handlers failed');
}
return handler.handle(input);
@ -85,13 +89,13 @@ export class CompositeAsyncHandler<TIn, TOut> implements AsyncHandler<TIn, TOut>
} else if (error instanceof Error) {
errors.push(new InternalServerError(error.message));
} else {
errors.push(new InternalServerError('Unknown error.'));
errors.push(new InternalServerError('Unknown error'));
}
}
}
const joined = errors.map((error: Error): string => error.message).join(', ');
const message = `No handler supports the given input: [${joined}].`;
const message = `No handler supports the given input: [${joined}]`;
// Check if all errors have the same status code
if (errors.every((error): boolean => error.statusCode === errors[0].statusCode)) {

View File

@ -1,5 +1,8 @@
import { getLoggerFor } from '../logging/LogUtil';
import { UnsupportedHttpError } from './errors/UnsupportedHttpError';
const logger = getLoggerFor('HeaderUtil');
// BNF based on https://tools.ietf.org/html/rfc7231
//
// Accept = #( media-range [ accept-params ] )
@ -105,9 +108,8 @@ export const transformQuotedStrings = (input: string): { result: string; replace
const result = input.replace(/"(?:[^"\\]|\\.)*"/gu, (match): string => {
// Not all characters allowed in quoted strings, see BNF above
if (!/^"(?:[\t !\u0023-\u005B\u005D-\u007E\u0080-\u00FF]|(?:\\[\t\u0020-\u007E\u0080-\u00FF]))*"$/u.test(match)) {
throw new UnsupportedHttpError(
`Invalid quoted string in header: ${match}. Check which characters are allowed`,
);
logger.warn(`Invalid quoted string in header: ${match}`);
throw new UnsupportedHttpError(`Invalid quoted string in header: ${match}`);
}
const replacement = `"${idx}"`;
replacements[replacement] = match.slice(1, -1);
@ -137,6 +139,7 @@ export const splitAndClean = (input: string): string[] =>
*/
const testQValue = (qvalue: string): void => {
if (!/^(?:(?:0(?:\.\d{0,3})?)|(?:1(?:\.0{0,3})?))$/u.test(qvalue)) {
logger.warn(`Invalid q value: ${qvalue}`);
throw new UnsupportedHttpError(
`Invalid q value: ${qvalue} does not match ( "0" [ "." 0*3DIGIT ] ) / ( "1" [ "." 0*3("0") ] ).`,
);
@ -163,6 +166,7 @@ export const parseParameters = (parameters: string[], replacements: { [id: strin
// parameter = token "=" ( token / quoted-string )
// second part is optional for certain parameters
if (!(token.test(name) && (!rawValue || /^"\d+"$/u.test(rawValue) || token.test(rawValue)))) {
logger.warn(`Invalid parameter value: ${name}=${replacements[rawValue] || rawValue}`);
throw new UnsupportedHttpError(
`Invalid parameter value: ${name}=${replacements[rawValue] || rawValue} ` +
`does not match (token ( "=" ( token / quoted-string ))?). `,
@ -197,6 +201,7 @@ const parseAcceptPart = (part: string, replacements: { [id: string]: string }):
// No reason to test differently for * since we don't check if the type exists
const [ type, subtype ] = range.split('/');
if (!type || !subtype || !token.test(type) || !token.test(subtype)) {
logger.warn(`Invalid Accept range: ${range}`);
throw new UnsupportedHttpError(
`Invalid Accept range: ${range} does not match ( "*/*" / ( token "/" "*" ) / ( token "/" token ) )`,
);
@ -215,8 +220,9 @@ const parseAcceptPart = (part: string, replacements: { [id: string]: string }):
weight = Number.parseFloat(value);
} else {
if (!value && map !== extensionParams) {
logger.warn(`Invalid Accept parameter ${name}`);
throw new UnsupportedHttpError(`Invalid Accept parameter ${name}: ` +
`Accept parameter values are not optional when preceding the q value.`);
`Accept parameter values are not optional when preceding the q value`);
}
map[name] = value || '';
}
@ -249,7 +255,8 @@ const parseNoParameters = (input: string): { range: string; weight: number }[] =
const result = { range, weight: 1 };
if (qvalue) {
if (!qvalue.startsWith('q=')) {
throw new UnsupportedHttpError(`Only q parameters are allowed in ${input}.`);
logger.warn(`Only q parameters are allowed in ${input}`);
throw new UnsupportedHttpError(`Only q parameters are allowed in ${input}`);
}
const val = qvalue.slice(2);
testQValue(val);
@ -293,6 +300,7 @@ export const parseAcceptCharset = (input: string): AcceptCharset[] => {
const results = parseNoParameters(input);
results.forEach((result): void => {
if (!token.test(result.range)) {
logger.warn(`Invalid Accept-Charset range: ${result.range}`);
throw new UnsupportedHttpError(
`Invalid Accept-Charset range: ${result.range} does not match (content-coding / "identity" / "*")`,
);
@ -315,6 +323,7 @@ export const parseAcceptEncoding = (input: string): AcceptEncoding[] => {
const results = parseNoParameters(input);
results.forEach((result): void => {
if (!token.test(result.range)) {
logger.warn(`Invalid Accept-Encoding range: ${result.range}`);
throw new UnsupportedHttpError(`Invalid Accept-Encoding range: ${result.range} does not match (charset / "*")`);
}
});
@ -336,6 +345,9 @@ export const parseAcceptLanguage = (input: string): AcceptLanguage[] => {
results.forEach((result): void => {
// (1*8ALPHA *("-" 1*8alphanum)) / "*"
if (result.range !== '*' && !/^[a-zA-Z]{1,8}(?:-[a-zA-Z0-9]{1,8})*$/u.test(result.range)) {
logger.warn(
`Invalid Accept-Language range: ${result.range}`,
);
throw new UnsupportedHttpError(
`Invalid Accept-Language range: ${result.range} does not match ((1*8ALPHA *("-" 1*8alphanum)) / "*")`,
);

View File

@ -2,6 +2,9 @@ import type { Readable, Writable } from 'stream';
import arrayifyStream from 'arrayify-stream';
import { DataFactory } from 'n3';
import type { Literal, NamedNode, Quad } from 'rdf-js';
import { getLoggerFor } from '../logging/LogUtil';
const logger = getLoggerFor('Util');
/**
* Makes sure the input path has exactly 1 slash at the end.
@ -66,7 +69,10 @@ export const matchingMediaType = (mediaA: string, mediaB: string): boolean => {
export const pipeStreamsAndErrors = <T extends Writable>(readable: Readable, destination: T,
mapError?: (error: Error) => Error): T => {
readable.pipe(destination);
readable.on('error', (error): boolean => destination.emit('error', mapError ? mapError(error) : error));
readable.on('error', (error): boolean => {
logger.warn(`Piped stream errored with ${error.message}`);
return destination.emit('error', mapError ? mapError(error) : error);
});
return destination;
};

View File

@ -2,8 +2,6 @@ import * as Path from 'path';
import type { Loader } from 'componentsjs';
import { runCli } from '../../../src/init/CliRunner';
import type { Setup } from '../../../src/init/Setup';
import { setGlobalLoggerFactory } from '../../../src/logging/LogUtil';
import { VoidLoggerFactory } from '../../../src/logging/VoidLoggerFactory';
let calledInstantiateFromUrl: boolean;
let calledRegisterAvailableModuleResources: boolean;
@ -56,8 +54,6 @@ jest.mock('yargs', (): any => ({
describe('CliRunner', (): void => {
beforeAll(async(): Promise<void> => {
setGlobalLoggerFactory(new VoidLoggerFactory());
mockSetup.setup.mockImplementation(async(): Promise<any> => {
// The info method will be called when all other code has been executed, so end the waiting function.
outsideResolve();

View File

@ -25,7 +25,8 @@ describe('A BasicRequestParser', (): void => {
});
it('errors if there is no input.', async(): Promise<void> => {
await expect(requestParser.handle({ url: 'url' } as any)).rejects.toThrow('Missing method.');
await expect(requestParser.handle({ url: 'url' } as any))
.rejects.toThrow('No method specified on the HTTP request');
});
it('returns the output of all input parsers after calling handle.', async(): Promise<void> => {

View File

@ -8,11 +8,11 @@ describe('A BasicTargetExtractor', (): void => {
});
it('errors if there is no URL.', async(): Promise<void> => {
await expect(extractor.handle({ headers: { host: 'test.com' }} as any)).rejects.toThrow('Missing URL.');
await expect(extractor.handle({ headers: { host: 'test.com' }} as any)).rejects.toThrow('Missing URL');
});
it('errors if there is no host.', async(): Promise<void> => {
await expect(extractor.handle({ url: 'url', headers: {}} as any)).rejects.toThrow('Missing host.');
await expect(extractor.handle({ url: 'url', headers: {}} as any)).rejects.toThrow('Missing Host header');
});
it('returns the input URL.', async(): Promise<void> => {

View File

@ -3,8 +3,8 @@ import streamifyArray from 'streamify-array';
import type { BodyParserArgs } from '../../../../src/ldp/http/BodyParser';
import { RawBodyParser } from '../../../../src/ldp/http/RawBodyParser';
import { RepresentationMetadata } from '../../../../src/ldp/representation/RepresentationMetadata';
import type { HttpRequest } from '../../../../src/server/HttpRequest';
import 'jest-rdf';
import type { HttpRequest } from '../../../../src/server/HttpRequest';
describe('A RawBodyparser', (): void => {
const bodyParser = new RawBodyParser();
@ -28,14 +28,14 @@ describe('A RawBodyparser', (): void => {
input.request = streamifyArray([ 'abc' ]) as HttpRequest;
input.request.headers = { 'content-length': '0' };
await expect(bodyParser.handle(input)).rejects
.toThrow('An HTTP request body was passed without Content-Type header');
.toThrow('HTTP request body was passed without Content-Type header');
});
it('errors when a transfer encoding was specified without content type.', async(): Promise<void> => {
input.request = streamifyArray([ 'abc' ]) as HttpRequest;
input.request.headers = { 'transfer-encoding': 'chunked' };
await expect(bodyParser.handle(input)).rejects
.toThrow('An HTTP request body was passed without Content-Type header');
.toThrow('HTTP request body was passed without Content-Type header');
});
it('returns a Representation if there was data.', async(): Promise<void> => {

View File

@ -1,7 +1,5 @@
import { LinkTypeParser } from '../../../../../src/ldp/http/metadata/LinkTypeParser';
import { RepresentationMetadata } from '../../../../../src/ldp/representation/RepresentationMetadata';
import { setGlobalLoggerFactory } from '../../../../../src/logging/LogUtil';
import { VoidLoggerFactory } from '../../../../../src/logging/VoidLoggerFactory';
import type { HttpRequest } from '../../../../../src/server/HttpRequest';
import { RDF } from '../../../../../src/util/UriConstants';
@ -10,10 +8,6 @@ describe('A LinkTypeParser', (): void => {
let request: HttpRequest;
let metadata: RepresentationMetadata;
beforeAll(async(): Promise<void> => {
setGlobalLoggerFactory(new VoidLoggerFactory());
});
beforeEach(async(): Promise<void> => {
request = { headers: {}} as HttpRequest;
metadata = new RepresentationMetadata();

View File

@ -22,7 +22,7 @@ describe('A SlugParser', (): void => {
it('errors if there are multiple slug headers.', async(): Promise<void> => {
request.headers.slug = [ 'slugA', 'slugB' ];
await expect(parser.parse(request, metadata))
.rejects.toThrow(new UnsupportedHttpError('At most 1 slug header is allowed.'));
.rejects.toThrow(new UnsupportedHttpError('Request has multiple Slug headers'));
});
it('stores the slug metadata.', async(): Promise<void> => {

View File

@ -1,7 +1,5 @@
import type { Server } from 'http';
import request from 'supertest';
import { setGlobalLoggerFactory } from '../../../src/logging/LogUtil';
import { VoidLoggerFactory } from '../../../src/logging/VoidLoggerFactory';
import { ExpressHttpServer } from '../../../src/server/ExpressHttpServer';
import { HttpHandler } from '../../../src/server/HttpHandler';
import type { HttpRequest } from '../../../src/server/HttpRequest';
@ -33,7 +31,6 @@ describe('ExpressHttpServer', (): void => {
beforeAll(async(): Promise<void> => {
// Prevent test from writing to stderr
mock = jest.spyOn(process.stderr, 'write').mockImplementation((): boolean => true);
setGlobalLoggerFactory(new VoidLoggerFactory());
});
beforeEach(async(): Promise<void> => {

View File

@ -27,12 +27,12 @@ describe('An ExtensionBasedMapper', (): void => {
it('throws 404 if the relative path does not start with a slash.', async(): Promise<void> => {
await expect(mapper.mapUrlToFilePath({ path: `${trimTrailingSlashes(base)}test` }))
.rejects.toThrow(new UnsupportedHttpError('URL needs a / after the base.'));
.rejects.toThrow(new UnsupportedHttpError('URL needs a / after the base'));
});
it('throws 400 if the input path contains relative parts.', async(): Promise<void> => {
await expect(mapper.mapUrlToFilePath({ path: `${base}test/../test2` }))
.rejects.toThrow(new UnsupportedHttpError('Disallowed /.. segment in URL.'));
.rejects.toThrow(new UnsupportedHttpError('Disallowed /.. segment in URL'));
});
it('returns the corresponding file path for container identifiers.', async(): Promise<void> => {
@ -44,7 +44,7 @@ describe('An ExtensionBasedMapper', (): void => {
it('rejects URLs that end with "$.{extension}".', async(): Promise<void> => {
await expect(mapper.mapUrlToFilePath({ path: `${base}test$.txt` }))
.rejects.toThrow(new UnsupportedHttpError('Identifiers cannot contain a dollar sign before their extension.'));
.rejects.toThrow(new UnsupportedHttpError('Identifiers cannot contain a dollar sign before their extension'));
});
it('throws 404 when looking in a folder that does not exist.', async(): Promise<void> => {
@ -95,7 +95,7 @@ describe('An ExtensionBasedMapper', (): void => {
it('throws 400 if the given content-type is not recognized.', async(): Promise<void> => {
await expect(mapper.mapUrlToFilePath({ path: `${base}test.txt` }, 'fake/data'))
.rejects.toThrow(new UnsupportedHttpError(`Unsupported content-type fake/data.`));
.rejects.toThrow(new UnsupportedHttpError(`Unsupported content type fake/data`));
});
});

View File

@ -12,20 +12,20 @@ describe('An UrlContainerManager', (): void => {
it('errors when getting the container of root.', async(): Promise<void> => {
let manager = new UrlContainerManager('http://test.com/foo/');
await expect(manager.getContainer({ path: 'http://test.com/foo/' }))
.rejects.toThrow('Root does not have a container.');
.rejects.toThrow('Root does not have a container');
await expect(manager.getContainer({ path: 'http://test.com/foo' }))
.rejects.toThrow('Root does not have a container.');
.rejects.toThrow('Root does not have a container');
manager = new UrlContainerManager('http://test.com/foo/');
await expect(manager.getContainer({ path: 'http://test.com/foo/' }))
.rejects.toThrow('Root does not have a container.');
.rejects.toThrow('Root does not have a container');
await expect(manager.getContainer({ path: 'http://test.com/foo' }))
.rejects.toThrow('Root does not have a container.');
.rejects.toThrow('Root does not have a container');
});
it('errors when the root of an URl is reached that does not match the input root.', async(): Promise<void> => {
const manager = new UrlContainerManager('http://test.com/foo/');
await expect(manager.getContainer({ path: 'http://test.com/' }))
.rejects.toThrow('URL root reached.');
.rejects.toThrow('URL root reached');
});
});

View File

@ -152,7 +152,7 @@ describe('A SparqlUpdatePatchHandler', (): void => {
'<http://test.com/startS1> <http://test.com/startP1> <http://test.com/startO1> } }',
{ quads: true },
) } as SparqlUpdatePatch });
await expect(handle).rejects.toThrow('GRAPH statements are not supported.');
await expect(handle).rejects.toThrow('GRAPH statements are not supported');
expect(order).toEqual([]);
});
@ -163,7 +163,7 @@ describe('A SparqlUpdatePatchHandler', (): void => {
'<http://test.com/startS1> <http://test.com/startP1> <http://test.com/startO1> } }',
{ quads: true },
) } as SparqlUpdatePatch });
await expect(handle).rejects.toThrow('GRAPH statements are not supported.');
await expect(handle).rejects.toThrow('GRAPH statements are not supported');
expect(order).toEqual([]);
});
@ -175,7 +175,7 @@ describe('A SparqlUpdatePatchHandler', (): void => {
'WHERE { ?s ?p ?o }',
{ quads: true },
) } as SparqlUpdatePatch });
await expect(handle).rejects.toThrow('WHERE statements are not supported.');
await expect(handle).rejects.toThrow('WHERE statements are not supported');
expect(order).toEqual([]);
});
@ -185,7 +185,7 @@ describe('A SparqlUpdatePatchHandler', (): void => {
'DELETE WHERE { ?v <http://test.com/startP1> <http://test.com/startO1> }',
{ quads: true },
) } as SparqlUpdatePatch });
await expect(handle).rejects.toThrow('WHERE statements are not supported.');
await expect(handle).rejects.toThrow('WHERE statements are not supported');
expect(order).toEqual([]);
});
@ -195,7 +195,7 @@ describe('A SparqlUpdatePatchHandler', (): void => {
'MOVE DEFAULT TO GRAPH <http://test.com/newGraph>',
{ quads: true },
) } as SparqlUpdatePatch });
await expect(handle).rejects.toThrow('Only DELETE/INSERT SPARQL update operations are supported.');
await expect(handle).rejects.toThrow('Only DELETE/INSERT SPARQL update operations are supported');
expect(order).toEqual([]);
});
});

View File

@ -44,7 +44,7 @@ describe('A CompositeAsyncHandler', (): void => {
it('can not handle data if no handler supports it.', async(): Promise<void> => {
const handler = new CompositeAsyncHandler([ handlerFalse, handlerFalse ]);
await expect(handler.canHandle(null)).rejects.toThrow('[Not supported., Not supported.]');
await expect(handler.canHandle(null)).rejects.toThrow('[Not supported, Not supported]');
});
it('throws unknown errors if no Error objects are thrown.', async(): Promise<void> => {
@ -53,7 +53,7 @@ describe('A CompositeAsyncHandler', (): void => {
};
const handler = new CompositeAsyncHandler([ handlerFalse, handlerFalse ]);
await expect(handler.canHandle(null)).rejects.toThrow('[Unknown error., Unknown error.]');
await expect(handler.canHandle(null)).rejects.toThrow('[Unknown error, Unknown error]');
});
it('handles data if a handler supports it.', async(): Promise<void> => {
@ -67,7 +67,7 @@ describe('A CompositeAsyncHandler', (): void => {
it('errors if the handle function is called but no handler supports the data.', async(): Promise<void> => {
const handler = new CompositeAsyncHandler([ handlerFalse, handlerFalse ]);
await expect(handler.handle('test')).rejects.toThrow('All handlers failed.');
await expect(handler.handle('test')).rejects.toThrow('All handlers failed');
});
it('only calls the canHandle function once of its handlers when handleSafe is called.', async(): Promise<void> => {
@ -81,7 +81,7 @@ describe('A CompositeAsyncHandler', (): void => {
it('throws the canHandle error when calling handleSafe if the data is not supported.', async(): Promise<void> => {
const handler = new CompositeAsyncHandler([ handlerFalse, handlerFalse ]);
await expect(handler.handleSafe(null)).rejects.toThrow('[Not supported., Not supported.]');
await expect(handler.handleSafe(null)).rejects.toThrow('[Not supported, Not supported]');
});
it('throws an error with matching status code if all handlers threw the same.', async(): Promise<void> => {

4
test/util/SetupTests.ts Normal file
View File

@ -0,0 +1,4 @@
import { setGlobalLoggerFactory } from '../../src/logging/LogUtil';
import { VoidLoggerFactory } from '../../src/logging/VoidLoggerFactory';
setGlobalLoggerFactory(new VoidLoggerFactory());

View File

@ -14,7 +14,7 @@ export class StaticAsyncHandler<TOut> extends AsyncHandler<any, TOut> {
if (this.canHandleStatic) {
return;
}
throw new Error('Not supported.');
throw new Error('Not supported');
}
public async handle(): Promise<TOut> {