feat: add simple response writer

This commit is contained in:
Joachim Van Herwegen 2020-06-23 10:19:43 +02:00
parent fe8749390c
commit 618005675f
6 changed files with 434 additions and 2 deletions

161
package-lock.json generated
View File

@ -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",

View File

@ -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"

View File

@ -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<void> {
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<void> {
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}`);
}
}
}

View File

@ -1,6 +1,6 @@
import { OutgoingMessage } from 'http';
import { ServerResponse } from 'http';
/**
* An outgoing HTTP response;
*/
export type HttpResponse = OutgoingMessage;
export type HttpResponse = ServerResponse;

View File

@ -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<void> => {
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<Operation, ResponseDescription>([
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<void> => {
// POST
let request = streamifyArray([ '<http://test.com/s> <http://test.com/p> <http://test.com/o>.' ]) as HttpRequest;
request.url = 'http://test.com/';
request.method = 'POST';
request.headers = {
'content-type': 'text/turtle',
};
let response: MockResponse<any> = 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('<http://test.com/s> <http://test.com/p> <http://test.com/o>.');
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;
});
});

View File

@ -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<any>;
beforeEach(async(): Promise<void> => {
response = createResponse({ eventEmitter: EventEmitter });
});
it('can handle input that has at least a description or an error.', async(): Promise<void> => {
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<void> => {
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<void> => {
const body = {
data: streamifyArray([ '<http://test.com/s> <http://test.com/p> <http://test.com/o>.' ]),
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('<http://test.com/s> <http://test.com/p> <http://test.com/o>.');
done();
});
await writer.handle({ response, description: { identifier: { path: 'path' }, body }});
});
it('responds with a content-type if the metadata has it.', async(done): Promise<void> => {
const body = {
data: streamifyArray([ '<http://test.com/s> <http://test.com/p> <http://test.com/o>.' ]),
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('<http://test.com/s> <http://test.com/p> <http://test.com/o>.');
done();
});
await writer.handle({ response, description: { identifier: { path: 'path' }, body }});
});
it('responds with 500 if an error if there is an error.', async(): Promise<void> => {
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<void> => {
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');
});
});