diff --git a/package-lock.json b/package-lock.json index a93f4edc6..248c3e085 100644 --- a/package-lock.json +++ b/package-lock.json @@ -749,18 +749,60 @@ "@babel/types": "^7.3.0" } }, + "@types/body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ==", + "dev": true, + "requires": { + "@types/connect": "*", + "@types/node": "*" + } + }, "@types/color-name": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", "dev": true }, + "@types/connect": { + "version": "3.4.33", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.33.tgz", + "integrity": "sha512-2+FrkXY4zllzTNfJth7jOqEHC+enpLeGslEhpnTAkg21GkRrWV4SsAtqchtT4YS9/nODBU2/ZfsBY2X4J/dX7A==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/eslint-visitor-keys": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", "integrity": "sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag==", "dev": true }, + "@types/express": { + "version": "4.17.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.6.tgz", + "integrity": "sha512-n/mr9tZI83kd4azlPG5y997C/M4DNABK9yErhFM6hKdym4kkmd9j0vtsJyjFIwfRBxtrxZtAfGZCNRIBMFLK5w==", + "dev": true, + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "*", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "@types/express-serve-static-core": { + "version": "4.17.7", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.7.tgz", + "integrity": "sha512-EMgTj/DF9qpgLXyc+Btimg+XoH7A2liE8uKul8qSmMTHCeNYzydDKFdsJskDvw42UsesCnhO63dO0Grbj8J4Dw==", + "dev": true, + "requires": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" + } + }, "@types/graceful-fs": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.3.tgz", @@ -871,6 +913,12 @@ "integrity": "sha512-8+KAKzEvSUdeo+kmqnKrqgeE+LcA0tjYWFY7RPProVYwnqDjukzO+3b6dLD56rYX5TdWejnEOLJYOIeh4CXKuA==", "dev": true }, + "@types/mime": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.2.tgz", + "integrity": "sha512-4kPlzbljFcsttWEq6aBW0OZe6BDajAmyvr2xknBG92tejQnvdGtT9+kXSZ580DqpxY9qG2xeQVF9Dq0ymUTo5Q==", + "dev": true + }, "@types/n3": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/@types/n3/-/n3-1.1.6.tgz", @@ -903,6 +951,18 @@ "integrity": "sha512-/rM+sWiuOZ5dvuVzV37sUuklsbg+JPOP8d+nNFlo2ZtfpzPiPvh1/gc8liWOLBqe+sR+ZM7guPaIcTt6UZTo7Q==", "dev": true }, + "@types/qs": { + "version": "6.9.3", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.3.tgz", + "integrity": "sha512-7s9EQWupR1fTc2pSMtXRQ9w9gLOcrJn+h7HOXw4evxyvVqMi4f+q7d2tnFe3ng3SNHjtK+0EzGMGFUQX4/AQRA==", + "dev": true + }, + "@types/range-parser": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz", + "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==", + "dev": true + }, "@types/rdf-js": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@types/rdf-js/-/rdf-js-3.0.0.tgz", @@ -911,6 +971,16 @@ "@types/node": "*" } }, + "@types/serve-static": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.4.tgz", + "integrity": "sha512-jTDt0o/YbpNwZbQmE/+2e+lfjJEJJR0I3OFaKQKPWkASkCoW3i6fsUnqudSMcNAfbtmADGu8f4MV4q+GqULmug==", + "dev": true, + "requires": { + "@types/express-serve-static-core": "*", + "@types/mime": "*" + } + }, "@types/stack-utils": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz", @@ -1006,6 +1076,16 @@ "integrity": "sha512-tsFzPpcttalNjFBCFMqsKYQcWxxen1pgJR56by//QwvJc4/OUS3kPOOttx2tSIfjsylB0pYu7f5D3K1RCxUnUg==", "dev": true }, + "accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", + "dev": true, + "requires": { + "mime-types": "~2.1.24", + "negotiator": "0.6.2" + } + }, "acorn": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.2.0.tgz", @@ -1779,6 +1859,12 @@ "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", "dev": true }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", + "dev": true + }, "detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -2676,6 +2762,12 @@ "map-cache": "^0.2.2" } }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", + "dev": true + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -4095,12 +4187,30 @@ "object-visit": "^1.0.0" } }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", + "dev": true + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=", + "dev": true + }, "merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", "dev": true }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", + "dev": true + }, "micromatch": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", @@ -4111,6 +4221,12 @@ "picomatch": "^2.0.5" } }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true + }, "mime-db": { "version": "1.44.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", @@ -4225,6 +4341,12 @@ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, + "negotiator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==", + "dev": true + }, "nice-try": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", @@ -4237,6 +4359,23 @@ "integrity": "sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=", "dev": true }, + "node-mocks-http": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/node-mocks-http/-/node-mocks-http-1.8.1.tgz", + "integrity": "sha512-qtd9YwXzCTdLfqjP7XSOtFei3TggwnjFIppmYEneQBaDIuknwgJTpItLskC5/pWOpU3lsK5aqdo+5CfIKHkXLg==", + "dev": true, + "requires": { + "accepts": "^1.3.7", + "depd": "^1.1.0", + "fresh": "^0.5.2", + "merge-descriptors": "^1.0.1", + "methods": "^1.1.2", + "mime": "^1.3.4", + "parseurl": "^1.3.3", + "range-parser": "^1.2.0", + "type-is": "^1.6.18" + } + }, "node-modules-regexp": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz", @@ -4536,6 +4675,12 @@ "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==", "dev": true }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true + }, "pascalcase": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", @@ -4696,6 +4841,12 @@ "integrity": "sha512-pVzZdDpWwWqEVVLshWUHjNwuVP7SfcmPraYuqocJp1yo2U1R7P+5QAfDhdItkuoGqIBnBYrtPp7rEPqDn9HlZA==", "dev": true }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true + }, "rdf-isomorphic": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/rdf-isomorphic/-/rdf-isomorphic-1.1.0.tgz", @@ -5969,6 +6120,16 @@ "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", "dev": true }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, "typedarray-to-buffer": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", diff --git a/package.json b/package.json index 1a54f10cc..986a048ab 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ }, "devDependencies": { "@types/arrayify-stream": "^1.0.0", + "@types/express": "^4.17.6", "@types/jest": "^25.2.1", "@types/streamify-array": "^1.0.0", "@typescript-eslint/eslint-plugin": "^2.33.0", @@ -40,6 +41,7 @@ "husky": "^4.2.5", "jest": "^26.0.1", "jest-rdf": "^1.5.0", + "node-mocks-http": "^1.8.1", "streamify-array": "^1.0.1", "ts-jest": "^26.0.0", "typescript": "^3.9.2" diff --git a/src/ldp/http/SimpleResponseWriter.ts b/src/ldp/http/SimpleResponseWriter.ts new file mode 100644 index 000000000..3308c100d --- /dev/null +++ b/src/ldp/http/SimpleResponseWriter.ts @@ -0,0 +1,39 @@ +import { HttpError } from '../../util/errors/HttpError'; +import { HttpResponse } from '../../server/HttpResponse'; +import { ResponseDescription } from '../operations/ResponseDescription'; +import { ResponseWriter } from './ResponseWriter'; +import { UnsupportedHttpError } from '../../util/errors/UnsupportedHttpError'; + +export class SimpleResponseWriter extends ResponseWriter { + public async canHandle(input: { response: HttpResponse; description?: ResponseDescription; error?: Error }): Promise { + if (!input.description && !input.error) { + throw new UnsupportedHttpError('Either a description or an error is required for output.'); + } + } + + public async handle(input: { response: HttpResponse; description?: ResponseDescription; error?: Error }): Promise { + if (input.description) { + input.response.setHeader('location', input.description.identifier.path); + if (input.description.body) { + if (input.description.body.metadata.contentType) { + input.response.setHeader('content-type', input.description.body.metadata.contentType); + } + input.description.body.data.pipe(input.response); + } + + input.response.writeHead(200); + + if (!input.description.body) { + // If there is an input body the response will end once the input stream ends + input.response.end(); + } + } else { + let code = 500; + if (input.error instanceof HttpError) { + code = input.error.statusCode; + } + input.response.writeHead(code); + input.response.end(`${input.error.name}: ${input.error.message}\n${input.error.stack}`); + } + } +} diff --git a/src/server/HttpResponse.ts b/src/server/HttpResponse.ts index 09174e542..ab6829c5f 100644 --- a/src/server/HttpResponse.ts +++ b/src/server/HttpResponse.ts @@ -1,6 +1,6 @@ -import { OutgoingMessage } from 'http'; +import { ServerResponse } from 'http'; /** * An outgoing HTTP response; */ -export type HttpResponse = OutgoingMessage; +export type HttpResponse = ServerResponse; diff --git a/test/integration/AuthenticatedLdpHandler.test.ts b/test/integration/AuthenticatedLdpHandler.test.ts new file mode 100644 index 000000000..c87169b3f --- /dev/null +++ b/test/integration/AuthenticatedLdpHandler.test.ts @@ -0,0 +1,143 @@ +import { AuthenticatedLdpHandler } from '../../src/ldp/AuthenticatedLdpHandler'; +import { CompositeAsyncHandler } from '../../src/util/CompositeAsyncHandler'; +import { EventEmitter } from 'events'; +import { HttpRequest } from '../../src/server/HttpRequest'; +import { Operation } from '../../src/ldp/operations/Operation'; +import { ResponseDescription } from '../../src/ldp/operations/ResponseDescription'; +import { SimpleAuthorizer } from '../../src/authorization/SimpleAuthorizer'; +import { SimpleBodyParser } from '../../src/ldp/http/SimpleBodyParser'; +import { SimpleCredentialsExtractor } from '../../src/authentication/SimpleCredentialsExtractor'; +import { SimpleDeleteOperationHandler } from '../../src/ldp/operations/SimpleDeleteOperationHandler'; +import { SimpleGetOperationHandler } from '../../src/ldp/operations/SimpleGetOperationHandler'; +import { SimplePermissionsExtractor } from '../../src/ldp/permissions/SimplePermissionsExtractor'; +import { SimplePostOperationHandler } from '../../src/ldp/operations/SimplePostOperationHandler'; +import { SimplePreferenceParser } from '../../src/ldp/http/SimplePreferenceParser'; +import { SimpleRequestParser } from '../../src/ldp/http/SimpleRequestParser'; +import { SimpleResourceStore } from '../../src/storage/SimpleResourceStore'; +import { SimpleResponseWriter } from '../../src/ldp/http/SimpleResponseWriter'; +import { SimpleTargetExtractor } from '../../src/ldp/http/SimpleTargetExtractor'; +import streamifyArray from 'streamify-array'; +import { createResponse, MockResponse } from 'node-mocks-http'; + +describe('An AuthenticatedLdpHandler with instantiated handlers', (): void => { + let handler: AuthenticatedLdpHandler; + + beforeEach(async(): Promise => { + const requestParser = new SimpleRequestParser({ + targetExtractor: new SimpleTargetExtractor(), + preferenceParser: new SimplePreferenceParser(), + bodyParser: new SimpleBodyParser(), + }); + + const credentialsExtractor = new SimpleCredentialsExtractor(); + const permissionsExtractor = new SimplePermissionsExtractor(); + const authorizer = new SimpleAuthorizer(); + + const store = new SimpleResourceStore('http://test.com/'); + const operationHandler = new CompositeAsyncHandler([ + new SimpleGetOperationHandler(store), + new SimplePostOperationHandler(store), + new SimpleDeleteOperationHandler(store), + ]); + + const responseWriter = new SimpleResponseWriter(); + + handler = new AuthenticatedLdpHandler({ + requestParser, + credentialsExtractor, + permissionsExtractor, + authorizer, + operationHandler, + responseWriter, + }); + }); + + it('can add, read and delete data based on incoming requests.', async(): Promise => { + // POST + let request = streamifyArray([ ' .' ]) as HttpRequest; + request.url = 'http://test.com/'; + request.method = 'POST'; + request.headers = { + 'content-type': 'text/turtle', + }; + let response: MockResponse = createResponse({ eventEmitter: EventEmitter }); + + let id; + let endPromise = new Promise((resolve): void => { + response.on('end', (): void => { + expect(response._isEndCalled()).toBeTruthy(); + expect(response.statusCode).toBe(200); + expect(response._getData()).toHaveLength(0); + id = response._getHeaders().location; + expect(id).toContain(request.url); + resolve(); + }); + }); + + await handler.handleSafe({ request, response }); + await endPromise; + + // GET + request = {} as HttpRequest; + request.url = id; + request.method = 'GET'; + request.headers = { + accept: 'text/turtle', + }; + response = createResponse({ eventEmitter: EventEmitter }); + + endPromise = new Promise((resolve): void => { + response.on('end', (): void => { + expect(response._isEndCalled()).toBeTruthy(); + expect(response.statusCode).toBe(200); + expect(response._getData()).toContain(' .'); + expect(response._getHeaders().location).toBe(request.url); + resolve(); + }); + }); + + await handler.handleSafe({ request, response }); + await endPromise; + + // DELETE + request = {} as HttpRequest; + request.url = id; + request.method = 'DELETE'; + request.headers = {}; + response = createResponse({ eventEmitter: EventEmitter }); + + endPromise = new Promise((resolve): void => { + response.on('end', (): void => { + expect(response._isEndCalled()).toBeTruthy(); + expect(response.statusCode).toBe(200); + expect(response._getData()).toHaveLength(0); + expect(response._getHeaders().location).toBe(request.url); + resolve(); + }); + }); + + await handler.handleSafe({ request, response }); + await endPromise; + + // GET + request = {} as HttpRequest; + request.url = id; + request.method = 'GET'; + request.headers = { + accept: 'text/turtle', + }; + response = createResponse({ eventEmitter: EventEmitter }); + + endPromise = new Promise((resolve): void => { + response.on('end', (): void => { + expect(response._isEndCalled()).toBeTruthy(); + expect(response.statusCode).toBe(404); + expect(response._getData()).toContain('NotFoundHttpError'); + resolve(); + }); + }); + + await handler.handleSafe({ request, response }); + await endPromise; + }); +}); diff --git a/test/unit/ldp/http/SimpleResponseWriter.test.ts b/test/unit/ldp/http/SimpleResponseWriter.test.ts new file mode 100644 index 000000000..c2679fdeb --- /dev/null +++ b/test/unit/ldp/http/SimpleResponseWriter.test.ts @@ -0,0 +1,87 @@ +import { EventEmitter } from 'events'; +import { Quad } from 'rdf-js'; +import { ResponseDescription } from '../../../../src/ldp/operations/ResponseDescription'; +import { SimpleResponseWriter } from '../../../../src/ldp/http/SimpleResponseWriter'; +import streamifyArray from 'streamify-array'; +import { UnsupportedHttpError } from '../../../../src/util/errors/UnsupportedHttpError'; +import { createResponse, MockResponse } from 'node-mocks-http'; + +describe('A SimpleResponseWriter', (): void => { + const writer = new SimpleResponseWriter(); + let response: MockResponse; + + beforeEach(async(): Promise => { + response = createResponse({ eventEmitter: EventEmitter }); + }); + + it('can handle input that has at least a description or an error.', async(): Promise => { + await expect(writer.canHandle({ response, description: {} as ResponseDescription })).resolves.toBeUndefined(); + await expect(writer.canHandle({ response, error: {} as Error })).resolves.toBeUndefined(); + await expect(writer.canHandle({ response })).rejects.toThrow(UnsupportedHttpError); + }); + + it('responds with status code 200 and a location header if there is a description.', async(): Promise => { + await writer.handle({ response, description: { identifier: { path: 'path' }}}); + expect(response._isEndCalled()).toBeTruthy(); + expect(response._getStatusCode()).toBe(200); + expect(response._getHeaders()).toMatchObject({ location: 'path' }); + }); + + it('responds with a body if the description has a body.', async(done): Promise => { + const body = { + data: streamifyArray([ ' .' ]), + dataType: 'binary', + metadata: { + raw: [] as Quad[], + profiles: [] as string[], + }, + }; + + response.on('end', (): void => { + expect(response._isEndCalled()).toBeTruthy(); + expect(response._getStatusCode()).toBe(200); + expect(response._getHeaders()).toMatchObject({ location: 'path' }); + expect(response._getData()).toEqual(' .'); + done(); + }); + + await writer.handle({ response, description: { identifier: { path: 'path' }, body }}); + }); + + it('responds with a content-type if the metadata has it.', async(done): Promise => { + const body = { + data: streamifyArray([ ' .' ]), + dataType: 'binary', + metadata: { + raw: [] as Quad[], + profiles: [] as string[], + contentType: 'text/turtle', + }, + }; + + response.on('end', (): void => { + expect(response._isEndCalled()).toBeTruthy(); + expect(response._getStatusCode()).toBe(200); + expect(response._getHeaders()).toMatchObject({ location: 'path', 'content-type': 'text/turtle' }); + expect(response._getData()).toEqual(' .'); + done(); + }); + + await writer.handle({ response, description: { identifier: { path: 'path' }, body }}); + }); + + it('responds with 500 if an error if there is an error.', async(): Promise => { + await writer.handle({ response, error: new Error('error') }); + expect(response._isEndCalled()).toBeTruthy(); + expect(response._getStatusCode()).toBe(500); + expect(response._getData()).toMatch('Error: error'); + }); + + it('responds with the given statuscode if there is an HttpError.', async(): Promise => { + const error = new UnsupportedHttpError('error'); + await writer.handle({ response, error }); + expect(response._isEndCalled()).toBeTruthy(); + expect(response._getStatusCode()).toBe(error.statusCode); + expect(response._getData()).toMatch('UnsupportedHttpError: error'); + }); +});