refactor: Restructure source code folder

This way the location of certain classes should make more sense
This commit is contained in:
Joachim Van Herwegen
2021-10-08 10:58:35 +02:00
parent 012d9e0864
commit b3da9c9fcf
280 changed files with 684 additions and 673 deletions

View File

@@ -0,0 +1,92 @@
import { EventEmitter } from 'events';
import { PassThrough } from 'stream';
import type { MockResponse } from 'node-mocks-http';
import { createResponse } from 'node-mocks-http';
import { BasicResponseWriter } from '../../../../src/http/output/BasicResponseWriter';
import type { MetadataWriter } from '../../../../src/http/output/metadata/MetadataWriter';
import type { ResponseDescription } from '../../../../src/http/output/response/ResponseDescription';
import { RepresentationMetadata } from '../../../../src/http/representation/RepresentationMetadata';
import { INTERNAL_QUADS } from '../../../../src/util/ContentTypes';
import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError';
import { guardedStreamFrom } from '../../../../src/util/StreamUtil';
import { StaticAsyncHandler } from '../../../util/StaticAsyncHandler';
describe('A BasicResponseWriter', (): void => {
let metadataWriter: MetadataWriter;
let writer: BasicResponseWriter;
let response: MockResponse<any>;
let result: ResponseDescription;
beforeEach(async(): Promise<void> => {
metadataWriter = new StaticAsyncHandler(true, undefined);
writer = new BasicResponseWriter(metadataWriter);
response = createResponse({ eventEmitter: EventEmitter });
result = { statusCode: 201 };
});
it('requires the input to be a binary ResponseDescription.', async(): Promise<void> => {
const metadata = new RepresentationMetadata(INTERNAL_QUADS);
await expect(writer.canHandle({ response, result: { statusCode: 201, metadata }}))
.rejects.toThrow(NotImplementedHttpError);
metadata.contentType = 'text/turtle';
await expect(writer.canHandle({ response, result: { statusCode: 201, metadata }}))
.resolves.toBeUndefined();
await expect(writer.canHandle({ response, result: { statusCode: 201 }}))
.resolves.toBeUndefined();
});
it('responds with the status code of the ResponseDescription.', async(): Promise<void> => {
await writer.handle({ response, result });
expect(response._isEndCalled()).toBeTruthy();
expect(response._getStatusCode()).toBe(201);
});
it('responds with a body if the description has a body.', async(): Promise<void> => {
const data = guardedStreamFrom([ '<http://test.com/s> <http://test.com/p> <http://test.com/o>.' ]);
result = { statusCode: 201, data };
const end = new Promise<void>((resolve): void => {
response.on('end', (): void => {
expect(response._isEndCalled()).toBeTruthy();
expect(response._getStatusCode()).toBe(201);
expect(response._getData()).toEqual('<http://test.com/s> <http://test.com/p> <http://test.com/o>.');
resolve();
});
});
await writer.handle({ response, result });
await end;
});
it('serializes metadata if there is metadata.', async(): Promise<void> => {
result = { statusCode: 201, metadata: new RepresentationMetadata() };
metadataWriter.handle = jest.fn();
await writer.handle({ response, result });
expect(metadataWriter.handle).toHaveBeenCalledTimes(1);
expect(metadataWriter.handle).toHaveBeenLastCalledWith({ response, metadata: result.metadata });
expect(response._isEndCalled()).toBeTruthy();
expect(response._getStatusCode()).toBe(201);
});
it('can handle the data stream erroring.', async(): Promise<void> => {
const data = guardedStreamFrom([]);
data.read = (): any => {
data.emit('error', new Error('bad data!'));
return null;
};
result = { statusCode: 201, data };
response = new PassThrough();
response.writeHead = jest.fn();
const end = new Promise<void>((resolve): void => {
response.on('error', (error: Error): void => {
expect(error).toEqual(new Error('bad data!'));
resolve();
});
});
await expect(writer.handle({ response, result })).resolves.toBeUndefined();
await end;
});
});

View File

@@ -0,0 +1,95 @@
import 'jest-rdf';
import arrayifyStream from 'arrayify-stream';
import { DataFactory } from 'n3';
import { ConvertingErrorHandler } from '../../../../../src/http/output/error/ConvertingErrorHandler';
import { BasicRepresentation } from '../../../../../src/http/representation/BasicRepresentation';
import type { Representation } from '../../../../../src/http/representation/Representation';
import type { RepresentationPreferences } from '../../../../../src/http/representation/RepresentationPreferences';
import type {
RepresentationConverter,
RepresentationConverterArgs,
} from '../../../../../src/storage/conversion/RepresentationConverter';
import { NotFoundHttpError } from '../../../../../src/util/errors/NotFoundHttpError';
import { HTTP, XSD } from '../../../../../src/util/Vocabularies';
import literal = DataFactory.literal;
const preferences: RepresentationPreferences = { type: { 'text/turtle': 1 }};
async function expectValidArgs(args: RepresentationConverterArgs, stack?: string): Promise<void> {
expect(args.preferences).toBe(preferences);
expect(args.representation.metadata.get(HTTP.terms.statusCodeNumber))
.toEqualRdfTerm(literal(404, XSD.terms.integer));
expect(args.representation.metadata.contentType).toBe('internal/error');
// Error contents
const errorArray = await arrayifyStream(args.representation.data);
expect(errorArray).toHaveLength(1);
const resultError = errorArray[0];
expect(resultError).toMatchObject({ name: 'NotFoundHttpError', message: 'not here' });
expect(resultError.stack).toBe(stack);
}
describe('A ConvertingErrorHandler', (): void => {
// The error object can get modified by the handler
let error: Error;
let stack: string | undefined;
let converter: RepresentationConverter;
let handler: ConvertingErrorHandler;
beforeEach(async(): Promise<void> => {
error = new NotFoundHttpError('not here');
({ stack } = error);
converter = {
canHandle: jest.fn(),
handle: jest.fn((): Representation => new BasicRepresentation('serialization', 'text/turtle', true)),
handleSafe: jest.fn((): Representation => new BasicRepresentation('serialization', 'text/turtle', true)),
} as any;
handler = new ConvertingErrorHandler(converter, true);
});
it('rejects input not supported by the converter.', async(): Promise<void> => {
(converter.canHandle as jest.Mock).mockRejectedValueOnce(new Error('rejected'));
await expect(handler.canHandle({ error, preferences })).rejects.toThrow('rejected');
expect(converter.canHandle).toHaveBeenCalledTimes(1);
const args = (converter.canHandle as jest.Mock).mock.calls[0][0] as RepresentationConverterArgs;
expect(args.preferences).toBe(preferences);
expect(args.representation.metadata.contentType).toBe('internal/error');
});
it('accepts input supported by the converter.', async(): Promise<void> => {
await expect(handler.canHandle({ error, preferences })).resolves.toBeUndefined();
expect(converter.canHandle).toHaveBeenCalledTimes(1);
const args = (converter.canHandle as jest.Mock).mock.calls[0][0] as RepresentationConverterArgs;
expect(args.preferences).toBe(preferences);
expect(args.representation.metadata.contentType).toBe('internal/error');
});
it('returns the converted error response.', async(): Promise<void> => {
const prom = handler.handle({ error, preferences });
await expect(prom).resolves.toMatchObject({ statusCode: 404 });
expect((await prom).metadata?.contentType).toBe('text/turtle');
expect(converter.handle).toHaveBeenCalledTimes(1);
const args = (converter.handle as jest.Mock).mock.calls[0][0] as RepresentationConverterArgs;
await expectValidArgs(args, stack);
});
it('uses the handleSafe function of the converter during its own handleSafe call.', async(): Promise<void> => {
const prom = handler.handleSafe({ error, preferences });
await expect(prom).resolves.toMatchObject({ statusCode: 404 });
expect((await prom).metadata?.contentType).toBe('text/turtle');
expect(converter.handleSafe).toHaveBeenCalledTimes(1);
const args = (converter.handleSafe as jest.Mock).mock.calls[0][0] as RepresentationConverterArgs;
await expectValidArgs(args, stack);
});
it('hides the stack trace if the option is disabled.', async(): Promise<void> => {
handler = new ConvertingErrorHandler(converter);
const prom = handler.handle({ error, preferences });
await expect(prom).resolves.toMatchObject({ statusCode: 404 });
expect((await prom).metadata?.contentType).toBe('text/turtle');
expect(converter.handle).toHaveBeenCalledTimes(1);
const args = (converter.handle as jest.Mock).mock.calls[0][0] as RepresentationConverterArgs;
await expectValidArgs(args);
});
});

View File

@@ -0,0 +1,77 @@
import 'jest-rdf';
import { DataFactory } from 'n3';
import type { ErrorHandler } from '../../../../../src/http/output/error/ErrorHandler';
import { SafeErrorHandler } from '../../../../../src/http/output/error/SafeErrorHandler';
import { BasicRepresentation } from '../../../../../src/http/representation/BasicRepresentation';
import { NotFoundHttpError } from '../../../../../src/util/errors/NotFoundHttpError';
import { readableToString } from '../../../../../src/util/StreamUtil';
import { HTTP, XSD } from '../../../../../src/util/Vocabularies';
import literal = DataFactory.literal;
describe('A SafeErrorHandler', (): void => {
let error: Error;
let stack: string | undefined;
let errorHandler: jest.Mocked<ErrorHandler>;
let handler: SafeErrorHandler;
beforeEach(async(): Promise<void> => {
error = new NotFoundHttpError('not here');
({ stack } = error);
errorHandler = {
handleSafe: jest.fn().mockResolvedValue(new BasicRepresentation('<html>fancy error</html>', 'text/html')),
} as any;
handler = new SafeErrorHandler(errorHandler, true);
});
it('can handle everything.', async(): Promise<void> => {
await expect(handler.canHandle({} as any)).resolves.toBeUndefined();
});
it('sends the request to the stored error handler.', async(): Promise<void> => {
const prom = handler.handle({ error } as any);
await expect(prom).resolves.toBeDefined();
const result = await prom;
expect(result.metadata?.contentType).toBe('text/html');
await expect(readableToString(result.data!)).resolves.toBe('<html>fancy error</html>');
});
describe('where the wrapped error handler fails', (): void => {
beforeEach(async(): Promise<void> => {
errorHandler.handleSafe.mockRejectedValue(new Error('handler failed'));
});
it('creates a text representation of the error.', async(): Promise<void> => {
const prom = handler.handle({ error } as any);
await expect(prom).resolves.toBeDefined();
const result = await prom;
expect(result.statusCode).toBe(404);
expect(result.metadata?.get(HTTP.terms.statusCodeNumber)).toEqualRdfTerm(literal(404, XSD.terms.integer));
expect(result.metadata?.contentType).toBe('text/plain');
await expect(readableToString(result.data!)).resolves.toBe(`${stack}\n`);
});
it('concatenates name and message if there is no stack.', async(): Promise<void> => {
delete error.stack;
const prom = handler.handle({ error } as any);
await expect(prom).resolves.toBeDefined();
const result = await prom;
expect(result.statusCode).toBe(404);
expect(result.metadata?.get(HTTP.terms.statusCodeNumber)).toEqualRdfTerm(literal(404, XSD.terms.integer));
expect(result.metadata?.contentType).toBe('text/plain');
await expect(readableToString(result.data!)).resolves.toBe(`NotFoundHttpError: not here\n`);
});
it('hides the stack trace if the option is disabled.', async(): Promise<void> => {
handler = new SafeErrorHandler(errorHandler);
const prom = handler.handle({ error } as any);
await expect(prom).resolves.toBeDefined();
const result = await prom;
expect(result.statusCode).toBe(404);
expect(result.metadata?.get(HTTP.terms.statusCodeNumber)).toEqualRdfTerm(literal(404, XSD.terms.integer));
expect(result.metadata?.contentType).toBe('text/plain');
await expect(readableToString(result.data!)).resolves.toBe(`NotFoundHttpError: not here\n`);
});
});
});

View File

@@ -0,0 +1,24 @@
import { createResponse } from 'node-mocks-http';
import { ConstantMetadataWriter } from '../../../../../src/http/output/metadata/ConstantMetadataWriter';
import type { HttpResponse } from '../../../../../src/server/HttpResponse';
describe('A ConstantMetadataWriter', (): void => {
const writer = new ConstantMetadataWriter({ 'custom-Header': 'X', other: 'Y' });
it('adds new headers.', async(): Promise<void> => {
const response = createResponse() as HttpResponse;
await expect(writer.handle({ response })).resolves.toBeUndefined();
expect(response.getHeaders()).toEqual({ 'custom-header': 'X', other: 'Y' });
});
it('extends existing headers.', async(): Promise<void> => {
const response = createResponse() as HttpResponse;
response.setHeader('Other', 'A');
await expect(writer.handle({ response })).resolves.toBeUndefined();
expect(response.getHeaders()).toEqual({ 'custom-header': 'X', other: [ 'A', 'Y' ]});
});
});

View File

@@ -0,0 +1,16 @@
import { createResponse } from 'node-mocks-http';
import { LinkRelMetadataWriter } from '../../../../../src/http/output/metadata/LinkRelMetadataWriter';
import { RepresentationMetadata } from '../../../../../src/http/representation/RepresentationMetadata';
import type { HttpResponse } from '../../../../../src/server/HttpResponse';
import { LDP, RDF } from '../../../../../src/util/Vocabularies';
describe('A LinkRelMetadataWriter', (): void => {
const writer = new LinkRelMetadataWriter({ [RDF.type]: 'type', dummy: 'dummy' });
it('adds the correct link headers.', async(): Promise<void> => {
const response = createResponse() as HttpResponse;
const metadata = new RepresentationMetadata({ [RDF.type]: LDP.terms.Resource, unused: 'text' });
await expect(writer.handle({ response, metadata })).resolves.toBeUndefined();
expect(response.getHeaders()).toEqual({ link: `<${LDP.Resource}>; rel="type"` });
});
});

View File

@@ -0,0 +1,16 @@
import { createResponse } from 'node-mocks-http';
import { MappedMetadataWriter } from '../../../../../src/http/output/metadata/MappedMetadataWriter';
import { RepresentationMetadata } from '../../../../../src/http/representation/RepresentationMetadata';
import type { HttpResponse } from '../../../../../src/server/HttpResponse';
import { CONTENT_TYPE } from '../../../../../src/util/Vocabularies';
describe('A MappedMetadataWriter', (): void => {
const writer = new MappedMetadataWriter({ [CONTENT_TYPE]: 'content-type', dummy: 'dummy' });
it('adds metadata to the corresponding header.', async(): Promise<void> => {
const response = createResponse() as HttpResponse;
const metadata = new RepresentationMetadata({ [CONTENT_TYPE]: 'text/turtle', unused: 'text' });
await expect(writer.handle({ response, metadata })).resolves.toBeUndefined();
expect(response.getHeaders()).toEqual({ 'content-type': 'text/turtle' });
});
});

View File

@@ -0,0 +1,29 @@
import { createResponse } from 'node-mocks-http';
import { ModifiedMetadataWriter } from '../../../../../src/http/output/metadata/ModifiedMetadataWriter';
import { RepresentationMetadata } from '../../../../../src/http/representation/RepresentationMetadata';
import type { HttpResponse } from '../../../../../src/server/HttpResponse';
import { updateModifiedDate } from '../../../../../src/util/ResourceUtil';
import { DC } from '../../../../../src/util/Vocabularies';
describe('A ModifiedMetadataWriter', (): void => {
const writer = new ModifiedMetadataWriter();
it('adds the Last-Modified and ETag header if there is dc:modified metadata.', async(): Promise<void> => {
const response = createResponse() as HttpResponse;
const metadata = new RepresentationMetadata();
updateModifiedDate(metadata);
const dateTime = metadata.get(DC.terms.modified)!.value;
await expect(writer.handle({ response, metadata })).resolves.toBeUndefined();
expect(response.getHeaders()).toEqual({
'last-modified': new Date(dateTime).toUTCString(),
etag: `"${new Date(dateTime).getTime()}"`,
});
});
it('does nothing if there is no matching metadata.', async(): Promise<void> => {
const response = createResponse() as HttpResponse;
const metadata = new RepresentationMetadata();
await expect(writer.handle({ response, metadata })).resolves.toBeUndefined();
expect(response.getHeaders()).toEqual({});
});
});

View File

@@ -0,0 +1,54 @@
import { createResponse } from 'node-mocks-http';
import { WacAllowMetadataWriter } from '../../../../../src/http/output/metadata/WacAllowMetadataWriter';
import { RepresentationMetadata } from '../../../../../src/http/representation/RepresentationMetadata';
import type { HttpResponse } from '../../../../../src/server/HttpResponse';
import { ACL, AUTH } from '../../../../../src/util/Vocabularies';
describe('A WacAllowMetadataWriter', (): void => {
const writer = new WacAllowMetadataWriter();
let response: HttpResponse;
beforeEach(async(): Promise<void> => {
response = createResponse() as HttpResponse;
});
it('adds no header if there is no relevant metadata.', async(): Promise<void> => {
const metadata = new RepresentationMetadata();
await expect(writer.handle({ response, metadata })).resolves.toBeUndefined();
expect(response.getHeaders()).toEqual({ });
});
it('adds a WAC-Allow header if there is relevant metadata.', async(): Promise<void> => {
const metadata = new RepresentationMetadata({
[AUTH.userMode]: [ ACL.terms.Read, ACL.terms.Write ],
[AUTH.publicMode]: [ ACL.terms.Read ],
});
await expect(writer.handle({ response, metadata })).resolves.toBeUndefined();
expect(response.getHeaders()).toEqual({
'wac-allow': 'user="read write",public="read"',
});
});
it('only adds a header value for entries with at least 1 permission.', async(): Promise<void> => {
const metadata = new RepresentationMetadata({
[AUTH.userMode]: [ ACL.terms.Read, ACL.terms.Write ],
});
await expect(writer.handle({ response, metadata })).resolves.toBeUndefined();
expect(response.getHeaders()).toEqual({
'wac-allow': 'user="read write"',
});
});
it('applies public modes to user modes.', async(): Promise<void> => {
const metadata = new RepresentationMetadata({
[AUTH.publicMode]: [ ACL.terms.Read, ACL.terms.Write ],
});
await expect(writer.handle({ response, metadata })).resolves.toBeUndefined();
expect(response.getHeaders()).toEqual({
'wac-allow': 'user="read write",public="read write"',
});
});
});

View File

@@ -0,0 +1,36 @@
import { createResponse } from 'node-mocks-http';
import { WwwAuthMetadataWriter } from '../../../../../src/http/output/metadata/WwwAuthMetadataWriter';
import { RepresentationMetadata } from '../../../../../src/http/representation/RepresentationMetadata';
import type { HttpResponse } from '../../../../../src/server/HttpResponse';
import { HTTP } from '../../../../../src/util/Vocabularies';
describe('A WwwAuthMetadataWriter', (): void => {
const auth = 'Bearer scope="openid webid"';
const writer = new WwwAuthMetadataWriter(auth);
let response: HttpResponse;
beforeEach(async(): Promise<void> => {
response = createResponse() as HttpResponse;
});
it('adds no header if there is no relevant metadata.', async(): Promise<void> => {
const metadata = new RepresentationMetadata();
await expect(writer.handle({ response, metadata })).resolves.toBeUndefined();
expect(response.getHeaders()).toEqual({ });
});
it('adds no header if the status code is not 401.', async(): Promise<void> => {
const metadata = new RepresentationMetadata({ [HTTP.statusCodeNumber]: '403' });
await expect(writer.handle({ response, metadata })).resolves.toBeUndefined();
expect(response.getHeaders()).toEqual({ });
});
it('adds a WWW-Authenticate header if the status code is 401.', async(): Promise<void> => {
const metadata = new RepresentationMetadata({ [HTTP.statusCodeNumber]: '401' });
await expect(writer.handle({ response, metadata })).resolves.toBeUndefined();
expect(response.getHeaders()).toEqual({
'www-authenticate': auth,
});
});
});

View File

@@ -0,0 +1,18 @@
import { RedirectResponseDescription } from '../../../../../src/http/output/response/RedirectResponseDescription';
import { SOLID_HTTP } from '../../../../../src/util/Vocabularies';
describe('A RedirectResponseDescription', (): void => {
const location = 'http://test.com/foo';
it('has status code 302 and a location.', async(): Promise<void> => {
const description = new RedirectResponseDescription(location);
expect(description.metadata?.get(SOLID_HTTP.terms.location)?.value).toBe(location);
expect(description.statusCode).toBe(302);
});
it('has status code 301 if the change is permanent.', async(): Promise<void> => {
const description = new RedirectResponseDescription(location, true);
expect(description.metadata?.get(SOLID_HTTP.terms.location)?.value).toBe(location);
expect(description.statusCode).toBe(301);
});
});