Browse Source

Add more unit tests

Include tests for emplace, modify fallback content negotiation logic.
master
TheoryOfNekomata 7 months ago
parent
commit
0ae3823bca
7 changed files with 149 additions and 65 deletions
  1. +4
    -0
      TODO.md
  2. +27
    -20
      src/backend/servers/http/core.ts
  3. +4
    -4
      src/backend/servers/http/decorators/backend/content-negotiation.ts
  4. +0
    -0
      test/features/decorators.test.ts
  5. +28
    -21
      test/handlers/http/default.test.ts
  6. +36
    -20
      test/handlers/http/error-handling.test.ts
  7. +50
    -0
      test/utils.ts

+ 4
- 0
TODO.md View File

@@ -26,3 +26,7 @@
- [ ] Implement RPC endpoints - [ ] Implement RPC endpoints
- [ ] Implement `Vary` header (requires providing a `getHeader()` method in the request object to listen for obtained headers) - [ ] Implement `Vary` header (requires providing a `getHeader()` method in the request object to listen for obtained headers)
- [ ] Add example on serving data as documents using `application/html` type. - [ ] Add example on serving data as documents using `application/html` type.
- [ ] OpenAPI support
- [ ] Swagger docs plugin
- [ ] Plugin support
- [ ] Add option to reject content negotiation params with `406 Not Acceptable`

+ 27
- 20
src/backend/servers/http/core.ts View File

@@ -384,40 +384,43 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr
const handleMiddlewareError = (processRequestErrRaw: Error) => (resourceReq: ResourceRequestContext, res: http.ServerResponse<RequestContext>) => { const handleMiddlewareError = (processRequestErrRaw: Error) => (resourceReq: ResourceRequestContext, res: http.ServerResponse<RequestContext>) => {
const finalErr = processRequestErrRaw as ErrorPlainResponse; const finalErr = processRequestErrRaw as ErrorPlainResponse;
const headers = finalErr.headers ?? {}; const headers = finalErr.headers ?? {};
const language = resourceReq.cn.language ?? resourceReq.backend.cn.language;
const mediaType = resourceReq.cn.mediaType ?? resourceReq.backend.cn.mediaType;
const charset = resourceReq.cn.charset ?? resourceReq.backend.cn.charset;
let encoded: Buffer | undefined; let encoded: Buffer | undefined;
let serialized; let serialized;
try { try {
serialized = typeof finalErr.body !== 'undefined' ? resourceReq.backend.cn.mediaType.serialize(finalErr.body) : undefined;
serialized = typeof finalErr.body !== 'undefined' ? mediaType.serialize(finalErr.body) : undefined;
} catch (cause) { } catch (cause) {
res.statusMessage = resourceReq.backend.cn.language.statusMessages['unableToSerializeResponse']?.replace(
res.statusMessage = language.statusMessages['unableToSerializeResponse']?.replace(
/\$RESOURCE/g, /\$RESOURCE/g,
resourceReq.resource!.state.itemName) ?? '';
resourceReq.resource.state.itemName) ?? '';
res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR); res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR);
res.end(); res.end();
return; return;
} }


try { try {
encoded = typeof serialized !== 'undefined' ? resourceReq.backend.cn.charset.encode(serialized) : undefined;
encoded = typeof serialized !== 'undefined' ? charset.encode(serialized) : undefined;
} catch (cause) { } catch (cause) {
res.statusMessage = resourceReq.backend.cn.language.statusMessages['unableToEncodeResponse']?.replace(/\$RESOURCE/g,
resourceReq.resource!.state.itemName) ?? '';
res.statusMessage = language.statusMessages['unableToEncodeResponse']?.replace(/\$RESOURCE/g,
resourceReq.resource.state.itemName) ?? '';
res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR); res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR);
res.end(); res.end();
return; return;
} }


headers['Content-Type'] = [ headers['Content-Type'] = [
resourceReq.backend.cn.mediaType.name,
typeof serialized !== 'undefined' ? `charset=${resourceReq.backend.cn.charset.name}` : '',
mediaType.name,
typeof serialized !== 'undefined' ? `charset=${charset.name}` : '',
] ]
.filter((s) => s.length > 0) .filter((s) => s.length > 0)
.join('; '); .join('; ');


res.statusMessage = resourceReq.backend.cn.language.statusMessages[
res.statusMessage = language.statusMessages[
finalErr.statusMessage ?? 'internalServerError' finalErr.statusMessage ?? 'internalServerError'
]?.replace(/\$RESOURCE/g, ]?.replace(/\$RESOURCE/g,
resourceReq.resource!.state.itemName);
resourceReq.resource.state.itemName);
res.writeHead(finalErr.statusCode ?? constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, headers); res.writeHead(finalErr.statusCode ?? constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, headers);
if (typeof encoded !== 'undefined') { if (typeof encoded !== 'undefined') {
res.end(encoded); res.end(encoded);
@@ -430,6 +433,10 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr
const plainReq = await decorateRequest(reqRaw); // TODO add type safety here, put handleGetRoot as its own middleware as it does not concern over any resource const plainReq = await decorateRequest(reqRaw); // TODO add type safety here, put handleGetRoot as its own middleware as it does not concern over any resource
if (typeof plainReq.resource !== 'undefined') { if (typeof plainReq.resource !== 'undefined') {
const resourceReq = plainReq as ResourceRequestContext; const resourceReq = plainReq as ResourceRequestContext;
const language = resourceReq.cn.language ?? resourceReq.backend.cn.language;
const mediaType = resourceReq.cn.mediaType ?? resourceReq.backend.cn.mediaType;
const charset = resourceReq.cn.charset ?? resourceReq.backend.cn.charset;

// TODO custom middlewares // TODO custom middlewares
const effectiveMiddlewares = ( const effectiveMiddlewares = (
typeof resourceReq.resourceId === 'string' typeof resourceReq.resourceId === 'string'
@@ -452,7 +459,7 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr
...( ...(
middlewareState.headers ?? {} middlewareState.headers ?? {}
), ),
'Content-Language': resourceReq.cn.language.name,
'Content-Language': language.name,
}; };
if (middlewareState instanceof http.ServerResponse) { if (middlewareState instanceof http.ServerResponse) {
// TODO streaming responses // TODO streaming responses
@@ -464,10 +471,10 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr
if (typeof middlewareState.body !== 'undefined') { if (typeof middlewareState.body !== 'undefined') {
let serialized; let serialized;
try { try {
serialized = resourceReq.cn.mediaType.serialize(middlewareState.body);
serialized = mediaType.serialize(middlewareState.body);
} catch (cause) { } catch (cause) {
const headers: Record<string, string> = { const headers: Record<string, string> = {
'Content-Language': resourceReq.backend.cn.language.name,
'Content-Language': language.name,
}; };
if (resourceReq.method === 'POST') { if (resourceReq.method === 'POST') {
headers['Accept-Post'] = Array.from(resourceReq.backend.app.mediaTypes.keys()).join(','); headers['Accept-Post'] = Array.from(resourceReq.backend.app.mediaTypes.keys()).join(',');
@@ -482,7 +489,7 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr
return `${mimeType}/merge-patch+${mimeSubtype}`; return `${mimeType}/merge-patch+${mimeSubtype}`;
}).join(','); }).join(',');
} }
res.statusMessage = resourceReq.backend.cn.language.statusMessages['unableToSerializeResponse']?.replace(
res.statusMessage = language.statusMessages['unableToSerializeResponse']?.replace(
/\$RESOURCE/g, /\$RESOURCE/g,
resourceReq.resource!.state.itemName) ?? ''; resourceReq.resource!.state.itemName) ?? '';
res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, headers); res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, headers);
@@ -491,24 +498,24 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr
} }


try { try {
encoded = resourceReq.cn.charset.encode(serialized);
encoded = charset.encode(serialized);
} catch (cause) { } catch (cause) {
res.statusMessage = resourceReq.backend.cn.language.statusMessages['unableToEncodeResponse']?.replace(/\$RESOURCE/g,
res.statusMessage = language.statusMessages['unableToEncodeResponse']?.replace(/\$RESOURCE/g,
resourceReq.resource!.state.itemName) ?? ''; resourceReq.resource!.state.itemName) ?? '';
res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, { res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, {
'Content-Language': resourceReq.backend.cn.language.name,
'Content-Language': language.name,
}); });
res.end(); res.end();
return; return;
} }


headers['Content-Type'] = [ headers['Content-Type'] = [
resourceReq.cn.mediaType.name,
`charset=${resourceReq.cn.charset.name}`,
mediaType.name,
`charset=${charset.name}`,
].join('; '); ].join('; ');
} }


const statusMessageKey = middlewareState.statusMessage ? resourceReq.cn.language.statusMessages[middlewareState.statusMessage] : undefined;
const statusMessageKey = middlewareState.statusMessage ? language.statusMessages[middlewareState.statusMessage] : undefined;
res.statusMessage = statusMessageKey?.replace(/\$RESOURCE/g, resourceReq.resource!.state.itemName) ?? ''; res.statusMessage = statusMessageKey?.replace(/\$RESOURCE/g, resourceReq.resource!.state.itemName) ?? '';
res.writeHead(middlewareState.statusCode, headers); res.writeHead(middlewareState.statusCode, headers);
if (typeof encoded !== 'undefined') { if (typeof encoded !== 'undefined') {


+ 4
- 4
src/backend/servers/http/decorators/backend/content-negotiation.ts View File

@@ -4,7 +4,7 @@ import Negotiator from 'negotiator';


declare module '../../../../common' { declare module '../../../../common' {
interface RequestContext { interface RequestContext {
cn: ContentNegotiation;
cn: Partial<ContentNegotiation>;
} }
} }


@@ -20,9 +20,9 @@ export const decorateRequestWithContentNegotiation: RequestDecorator = (req) =>
const mediaTypeCandidate = negotiator.mediaType(availableMediaTypes.map((l) => l.name)) ?? req.backend.cn.mediaType.name; const mediaTypeCandidate = negotiator.mediaType(availableMediaTypes.map((l) => l.name)) ?? req.backend.cn.mediaType.name;


req.cn = { req.cn = {
language: req.backend.app.languages.get(languageCandidate) ?? req.backend.cn.language,
mediaType: req.backend.app.mediaTypes.get(mediaTypeCandidate) ?? req.backend.cn.mediaType,
charset: req.backend.app.charsets.get(charsetCandidate) ?? req.backend.cn.charset,
language: req.backend.app.languages.get(languageCandidate),
mediaType: req.backend.app.mediaTypes.get(mediaTypeCandidate),
charset: req.backend.app.charsets.get(charsetCandidate),
}; };


return req; return req;


test/e2e/features.test.ts → test/features/decorators.test.ts View File


test/e2e/http/default.test.ts → test/handlers/http/default.test.ts View File

@@ -10,9 +10,15 @@ import {
} from 'vitest'; } from 'vitest';
import {constants} from 'http2'; import {constants} from 'http2';
import {Backend} from '../../../src/backend'; import {Backend} from '../../../src/backend';
import {application, resource, validation as v, Resource, Application} from '../../../src/common';
import {
application,
resource,
validation as v,
Resource,
Application,
} from '../../../src/common';
import { autoIncrement } from '../../fixtures'; import { autoIncrement } from '../../fixtures';
import {createTestClient, DummyDataSource, TestClient} from '../../utils';
import {createTestClient, DummyDataSource, TEST_LANGUAGE, TestClient} from '../../utils';
import {DataSource} from '../../../src/backend/data-source'; import {DataSource} from '../../../src/backend/data-source';


const PORT = 3000; const PORT = 3000;
@@ -24,7 +30,7 @@ const ACCEPT_CHARSET = 'utf-8';
const CONTENT_TYPE_CHARSET = 'utf-8'; const CONTENT_TYPE_CHARSET = 'utf-8';
const CONTENT_TYPE = ACCEPT; const CONTENT_TYPE = ACCEPT;


describe.only('happy path', () => {
describe('happy path', () => {
let Piano: Resource; let Piano: Resource;
let app: Application; let app: Application;
let dataSource: DataSource; let dataSource: DataSource;
@@ -51,6 +57,7 @@ describe.only('happy path', () => {
app = application({ app = application({
name: 'piano-service', name: 'piano-service',
}) })
.language(TEST_LANGUAGE)
.resource(Piano); .resource(Piano);


dataSource = new DummyDataSource(); dataSource = new DummyDataSource();
@@ -120,7 +127,7 @@ describe.only('happy path', () => {
}); });


expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_OK); expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_OK);
expect(res).toHaveProperty('statusMessage', 'Piano Collection Fetched');
expect(res).toHaveProperty('statusMessage', TEST_LANGUAGE.statusMessages.resourceCollectionFetched);
expect(res.headers).toHaveProperty('content-type', expect.stringContaining(ACCEPT)); expect(res.headers).toHaveProperty('content-type', expect.stringContaining(ACCEPT));


if (typeof resData === 'undefined') { if (typeof resData === 'undefined') {
@@ -138,7 +145,7 @@ describe.only('happy path', () => {
}); });


expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_OK); expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_OK);
expect(res).toHaveProperty('statusMessage', 'Piano Collection Fetched');
expect(res).toHaveProperty('statusMessage', TEST_LANGUAGE.statusMessages.resourceCollectionFetched);
}); });


it('returns options', async () => { it('returns options', async () => {
@@ -148,7 +155,7 @@ describe.only('happy path', () => {
}); });


expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_NO_CONTENT); expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_NO_CONTENT);
expect(res).toHaveProperty('statusMessage', 'Provide Options');
expect(res).toHaveProperty('statusMessage', TEST_LANGUAGE.statusMessages.provideOptions);
const allowedMethods = res.headers.allow?.split(',').map((s) => s.trim()) ?? []; const allowedMethods = res.headers.allow?.split(',').map((s) => s.trim()) ?? [];
expect(allowedMethods).toContain('GET'); expect(allowedMethods).toContain('GET');
expect(allowedMethods).toContain('HEAD'); expect(allowedMethods).toContain('HEAD');
@@ -182,7 +189,7 @@ describe.only('happy path', () => {
}); });


expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_OK); expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_OK);
expect(res).toHaveProperty('statusMessage', 'Piano Fetched');
expect(res).toHaveProperty('statusMessage', TEST_LANGUAGE.statusMessages.resourceFetched);
expect(res.headers).toHaveProperty('content-type', expect.stringContaining(ACCEPT)); expect(res.headers).toHaveProperty('content-type', expect.stringContaining(ACCEPT));
if (typeof resData === 'undefined') { if (typeof resData === 'undefined') {
expect.fail('Response body must be defined.'); expect.fail('Response body must be defined.');
@@ -199,7 +206,7 @@ describe.only('happy path', () => {
}); });


expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_OK); expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_OK);
expect(res).toHaveProperty('statusMessage', 'Piano Fetched');
expect(res).toHaveProperty('statusMessage', TEST_LANGUAGE.statusMessages.resourceFetched);
}); });


it('returns options', async () => { it('returns options', async () => {
@@ -209,7 +216,7 @@ describe.only('happy path', () => {
}); });


expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_NO_CONTENT); expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_NO_CONTENT);
expect(res).toHaveProperty('statusMessage', 'Provide Options');
expect(res).toHaveProperty('statusMessage', TEST_LANGUAGE.statusMessages.provideOptions);
const allowedMethods = res.headers.allow?.split(',').map((s) => s.trim()) ?? []; const allowedMethods = res.headers.allow?.split(',').map((s) => s.trim()) ?? [];
expect(allowedMethods).toContain('GET'); expect(allowedMethods).toContain('GET');
expect(allowedMethods).toContain('HEAD'); expect(allowedMethods).toContain('HEAD');
@@ -254,7 +261,7 @@ describe.only('happy path', () => {
}); });


expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_CREATED); expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_CREATED);
expect(res).toHaveProperty('statusMessage', 'Piano Created');
expect(res).toHaveProperty('statusMessage', TEST_LANGUAGE.statusMessages.resourceCreated);
expect(res.headers).toHaveProperty('content-type', expect.stringContaining(ACCEPT)); expect(res.headers).toHaveProperty('content-type', expect.stringContaining(ACCEPT));
expect(res.headers).toHaveProperty('location', `${BASE_PATH}/pianos/2`); expect(res.headers).toHaveProperty('location', `${BASE_PATH}/pianos/2`);


@@ -276,13 +283,13 @@ describe.only('happy path', () => {
}); });


expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_NO_CONTENT); expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_NO_CONTENT);
expect(res).toHaveProperty('statusMessage', 'Provide Options');
expect(res).toHaveProperty('statusMessage', TEST_LANGUAGE.statusMessages.provideOptions);
const allowedMethods = res.headers.allow?.split(',').map((s) => s.trim()) ?? []; const allowedMethods = res.headers.allow?.split(',').map((s) => s.trim()) ?? [];
expect(allowedMethods).toContain('POST'); expect(allowedMethods).toContain('POST');
}); });
}); });


describe.only('patching items', () => {
describe('patching items', () => {
const existingResource = { const existingResource = {
id: 1, id: 1,
brand: 'Yamaha' brand: 'Yamaha'
@@ -322,7 +329,7 @@ describe.only('happy path', () => {
}); });


expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_NO_CONTENT); expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_NO_CONTENT);
expect(res).toHaveProperty('statusMessage', 'Provide Options');
expect(res).toHaveProperty('statusMessage', TEST_LANGUAGE.statusMessages.provideOptions);
const allowedMethods = res.headers.allow?.split(',').map((s) => s.trim()) ?? []; const allowedMethods = res.headers.allow?.split(',').map((s) => s.trim()) ?? [];
expect(allowedMethods).toContain('PATCH'); expect(allowedMethods).toContain('PATCH');
const acceptPatch = res.headers['accept-patch']?.split(',').map((s) => s.trim()) ?? []; const acceptPatch = res.headers['accept-patch']?.split(',').map((s) => s.trim()) ?? [];
@@ -346,7 +353,7 @@ describe.only('happy path', () => {
}); });


expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_OK); expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_OK);
expect(res).toHaveProperty('statusMessage', 'Piano Patched');
expect(res).toHaveProperty('statusMessage', TEST_LANGUAGE.statusMessages.resourcePatched);
expect(res.headers).toHaveProperty('content-type', expect.stringContaining(ACCEPT)); expect(res.headers).toHaveProperty('content-type', expect.stringContaining(ACCEPT));


if (typeof resData === 'undefined') { if (typeof resData === 'undefined') {
@@ -366,7 +373,7 @@ describe.only('happy path', () => {
Piano.canPatch(false).canPatch(['delta']); Piano.canPatch(false).canPatch(['delta']);
}); });


it.only('returns data', async () => {
it('returns data', async () => {
const [res, resData] = await client({ const [res, resData] = await client({
method: 'PATCH', method: 'PATCH',
path: `${BASE_PATH}/pianos/${existingResource.id}`, path: `${BASE_PATH}/pianos/${existingResource.id}`,
@@ -383,7 +390,7 @@ describe.only('happy path', () => {
}); });


expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_OK); expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_OK);
expect(res).toHaveProperty('statusMessage', 'Piano Patched');
expect(res).toHaveProperty('statusMessage', TEST_LANGUAGE.statusMessages.resourcePatched);
expect(res.headers).toHaveProperty('content-type', expect.stringContaining(ACCEPT)); expect(res.headers).toHaveProperty('content-type', expect.stringContaining(ACCEPT));


if (typeof resData === 'undefined') { if (typeof resData === 'undefined') {
@@ -433,7 +440,7 @@ describe.only('happy path', () => {
}); });


expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_OK); expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_OK);
expect(res).toHaveProperty('statusMessage', 'Piano Replaced');
expect(res).toHaveProperty('statusMessage', TEST_LANGUAGE.statusMessages.resourceReplaced);
expect(res.headers).toHaveProperty('content-type', expect.stringContaining(ACCEPT)); expect(res.headers).toHaveProperty('content-type', expect.stringContaining(ACCEPT));


if (typeof resData === 'undefined') { if (typeof resData === 'undefined') {
@@ -465,7 +472,7 @@ describe.only('happy path', () => {
}); });


expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_CREATED); expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_CREATED);
expect(res).toHaveProperty('statusMessage', 'Piano Created');
expect(res).toHaveProperty('statusMessage', TEST_LANGUAGE.statusMessages.resourceCreated);
expect(res.headers).toHaveProperty('content-type', expect.stringContaining(ACCEPT)); expect(res.headers).toHaveProperty('content-type', expect.stringContaining(ACCEPT));
expect(res.headers).toHaveProperty('location', `${BASE_PATH}/pianos/${newId}`); expect(res.headers).toHaveProperty('location', `${BASE_PATH}/pianos/${newId}`);


@@ -487,7 +494,7 @@ describe.only('happy path', () => {
}); });


expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_NO_CONTENT); expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_NO_CONTENT);
expect(res).toHaveProperty('statusMessage', 'Provide Options');
expect(res).toHaveProperty('statusMessage', TEST_LANGUAGE.statusMessages.provideOptions);
const allowedMethods = res.headers.allow?.split(',').map((s) => s.trim()) ?? []; const allowedMethods = res.headers.allow?.split(',').map((s) => s.trim()) ?? [];
expect(allowedMethods).toContain('PUT'); expect(allowedMethods).toContain('PUT');
}); });
@@ -526,7 +533,7 @@ describe.only('happy path', () => {
}); });


expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_NO_CONTENT); expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_NO_CONTENT);
expect(res).toHaveProperty('statusMessage', 'Piano Deleted');
expect(res).toHaveProperty('statusMessage', TEST_LANGUAGE.statusMessages.resourceDeleted);
expect(res.headers).not.toHaveProperty('content-type'); expect(res.headers).not.toHaveProperty('content-type');
expect(resData).toBeUndefined(); expect(resData).toBeUndefined();
}); });
@@ -538,7 +545,7 @@ describe.only('happy path', () => {
}); });


expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_NO_CONTENT); expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_NO_CONTENT);
expect(res).toHaveProperty('statusMessage', 'Provide Options');
expect(res).toHaveProperty('statusMessage', TEST_LANGUAGE.statusMessages.provideOptions);
const allowedMethods = res.headers.allow?.split(',').map((s) => s.trim()) ?? []; const allowedMethods = res.headers.allow?.split(',').map((s) => s.trim()) ?? [];
expect(allowedMethods).toContain('DELETE'); expect(allowedMethods).toContain('DELETE');
}); });

test/e2e/http/error-handling.test.ts → test/handlers/http/error-handling.test.ts View File

@@ -11,10 +11,10 @@ import {constants} from 'http2';
import {Backend} from '../../../src/backend'; import {Backend} from '../../../src/backend';
import {application, resource, validation as v, Resource, Application, Delta} from '../../../src/common'; import {application, resource, validation as v, Resource, Application, Delta} from '../../../src/common';
import { autoIncrement } from '../../fixtures'; import { autoIncrement } from '../../fixtures';
import {createTestClient, TestClient, DummyDataSource, DummyError} from '../../utils';
import {createTestClient, TestClient, DummyDataSource, DummyError, TEST_LANGUAGE} from '../../utils';
import {DataSource} from '../../../src/backend/data-source'; import {DataSource} from '../../../src/backend/data-source';


const PORT = 4001;
const PORT = 3001;
const HOST = '127.0.0.1'; const HOST = '127.0.0.1';
const BASE_PATH = '/api'; const BASE_PATH = '/api';
const ACCEPT = 'application/json'; const ACCEPT = 'application/json';
@@ -50,6 +50,7 @@ describe('error handling', () => {
app = application({ app = application({
name: 'piano-service', name: 'piano-service',
}) })
.language(TEST_LANGUAGE)
.resource(Piano); .resource(Piano);


dataSource = new DummyDataSource(); dataSource = new DummyDataSource();
@@ -117,7 +118,7 @@ describe('error handling', () => {
}); });


expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_INTERNAL_SERVER_ERROR); expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_INTERNAL_SERVER_ERROR);
expect(res).toHaveProperty('statusMessage', 'Unable To Fetch Piano Collection');
expect(res).toHaveProperty('statusMessage', TEST_LANGUAGE.statusMessages.unableToFetchResourceCollection);
}); });


it('throws on HEAD method', async () => { it('throws on HEAD method', async () => {
@@ -131,7 +132,7 @@ describe('error handling', () => {
}); });


expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_INTERNAL_SERVER_ERROR); expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_INTERNAL_SERVER_ERROR);
expect(res).toHaveProperty('statusMessage', 'Unable To Fetch Piano Collection');
expect(res).toHaveProperty('statusMessage', TEST_LANGUAGE.statusMessages.unableToFetchResourceCollection);
}); });
}); });


@@ -155,7 +156,7 @@ describe('error handling', () => {
}); });


expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_INTERNAL_SERVER_ERROR); expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_INTERNAL_SERVER_ERROR);
expect(res).toHaveProperty('statusMessage', 'Unable To Fetch Piano');
expect(res).toHaveProperty('statusMessage', TEST_LANGUAGE.statusMessages.unableToFetchResource);
}); });


it('throws on HEAD method', async () => { it('throws on HEAD method', async () => {
@@ -169,7 +170,7 @@ describe('error handling', () => {
}); });


expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_INTERNAL_SERVER_ERROR); expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_INTERNAL_SERVER_ERROR);
expect(res).toHaveProperty('statusMessage', 'Unable To Fetch Piano');
expect(res).toHaveProperty('statusMessage', TEST_LANGUAGE.statusMessages.unableToFetchResource);
}); });


it('throws on item not found', async () => { it('throws on item not found', async () => {
@@ -228,7 +229,7 @@ describe('error handling', () => {
}); });


expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_INTERNAL_SERVER_ERROR); expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_INTERNAL_SERVER_ERROR);
expect(res).toHaveProperty('statusMessage', 'Unable To Assign ID From Piano Data Source');
expect(res).toHaveProperty('statusMessage', TEST_LANGUAGE.statusMessages.unableToAssignIdFromResourceDataSource);
}); });


it('throws on error creating resource', async () => { it('throws on error creating resource', async () => {
@@ -247,7 +248,7 @@ describe('error handling', () => {
}); });


expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_INTERNAL_SERVER_ERROR); expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_INTERNAL_SERVER_ERROR);
expect(res).toHaveProperty('statusMessage', 'Unable To Create Piano');
expect(res).toHaveProperty('statusMessage', TEST_LANGUAGE.statusMessages.unableToCreateResource);
}); });
}); });


@@ -279,7 +280,7 @@ describe('error handling', () => {
}); });


expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_INTERNAL_SERVER_ERROR); expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_INTERNAL_SERVER_ERROR);
expect(res).toHaveProperty('statusMessage', 'Unable To Fetch Piano');
expect(res).toHaveProperty('statusMessage', TEST_LANGUAGE.statusMessages.unableToFetchResource);
Piano.canPatch(false); Piano.canPatch(false);
}); });


@@ -299,7 +300,7 @@ describe('error handling', () => {
}); });


expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_NOT_FOUND); expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_NOT_FOUND);
expect(res).toHaveProperty('statusMessage', 'Patch Non-Existing Piano');
expect(res).toHaveProperty('statusMessage', TEST_LANGUAGE.statusMessages.patchNonExistingResource);
Piano.canPatch(false); Piano.canPatch(false);
}); });


@@ -327,7 +328,7 @@ describe('error handling', () => {
}); });


expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_UNSUPPORTED_MEDIA_TYPE); expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_UNSUPPORTED_MEDIA_TYPE);
expect(res).toHaveProperty('statusMessage', 'Invalid Piano Patch Type');
expect(res).toHaveProperty('statusMessage', TEST_LANGUAGE.statusMessages.invalidResourcePatchType);
}); });
}); });


@@ -351,7 +352,7 @@ describe('error handling', () => {
}); });


expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_UNSUPPORTED_MEDIA_TYPE); expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_UNSUPPORTED_MEDIA_TYPE);
expect(res).toHaveProperty('statusMessage', 'Invalid Piano Patch Type');
expect(res).toHaveProperty('statusMessage', TEST_LANGUAGE.statusMessages.invalidResourcePatchType);
}); });


it('throws on operating with a delta to an attribute outside the schema', async () => { it('throws on operating with a delta to an attribute outside the schema', async () => {
@@ -371,7 +372,7 @@ describe('error handling', () => {
}); });


expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_UNPROCESSABLE_ENTITY); expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_UNPROCESSABLE_ENTITY);
expect(res).toHaveProperty('statusMessage', 'Invalid Piano Patch');
expect(res).toHaveProperty('statusMessage', TEST_LANGUAGE.statusMessages.invalidResourcePatch);
}); });


it('throws on operating a delta with mismatched value type', async () => { it('throws on operating a delta with mismatched value type', async () => {
@@ -391,10 +392,10 @@ describe('error handling', () => {
}); });


expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_UNPROCESSABLE_ENTITY); expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_UNPROCESSABLE_ENTITY);
expect(res).toHaveProperty('statusMessage', 'Invalid Piano Patch');
expect(res).toHaveProperty('statusMessage', TEST_LANGUAGE.statusMessages.invalidResourcePatch);
}); });


it.skip('throws on performing an invalid delta', async () => {
it('throws on performing an invalid delta', async () => {
const [res] = await client({ const [res] = await client({
method: 'PATCH', method: 'PATCH',
path: `${BASE_PATH}/pianos/${existingResource.id}`, path: `${BASE_PATH}/pianos/${existingResource.id}`,
@@ -411,12 +412,12 @@ describe('error handling', () => {
}); });


expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_UNPROCESSABLE_ENTITY); expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_UNPROCESSABLE_ENTITY);
expect(res).toHaveProperty('statusMessage', 'Invalid Piano Patch');
expect(res).toHaveProperty('statusMessage', TEST_LANGUAGE.statusMessages.invalidResourcePatch);
}); });
}); });
}); });


describe.skip('emplacing items', () => {
describe('emplacing items', () => {
const existingResource = { const existingResource = {
id: 1, id: 1,
brand: 'Yamaha' brand: 'Yamaha'
@@ -440,6 +441,21 @@ describe('error handling', () => {
afterEach(() => { afterEach(() => {
Piano.canEmplace(false); Piano.canEmplace(false);
}); });

it('throws on unable to emplace', async () => {
vi
.spyOn(DummyDataSource.prototype, 'emplace')
.mockImplementationOnce(() => { throw new DummyError() });

const [res] = await client({
method: 'PUT',
path: `${BASE_PATH}/pianos/${existingResource.id}`,
body: newData,
});

expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_INTERNAL_SERVER_ERROR);
expect(res).toHaveProperty('statusMessage', TEST_LANGUAGE.statusMessages.unableToEmplaceResource);
});
}); });


describe('deleting items', () => { describe('deleting items', () => {
@@ -469,7 +485,7 @@ describe('error handling', () => {
}); });


expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_INTERNAL_SERVER_ERROR); expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_INTERNAL_SERVER_ERROR);
expect(res).toHaveProperty('statusMessage', 'Unable To Fetch Piano');
expect(res).toHaveProperty('statusMessage', TEST_LANGUAGE.statusMessages.unableToFetchResource);
}); });


it('throws on item not found', async () => { it('throws on item not found', async () => {
@@ -483,7 +499,7 @@ describe('error handling', () => {
}); });


expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_NOT_FOUND); expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_NOT_FOUND);
expect(res).toHaveProperty('statusMessage', 'Delete Non-Existing Piano');
expect(res).toHaveProperty('statusMessage', TEST_LANGUAGE.statusMessages.deleteNonExistingResource);
}); });


it('throws on unable to delete item', async () => { it('throws on unable to delete item', async () => {
@@ -501,7 +517,7 @@ describe('error handling', () => {
}); });


expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_INTERNAL_SERVER_ERROR); expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_INTERNAL_SERVER_ERROR);
expect(res).toHaveProperty('statusMessage', 'Unable To Delete Piano');
expect(res).toHaveProperty('statusMessage', TEST_LANGUAGE.statusMessages.unableToDeleteResource);
}); });
}); });
}); });

+ 50
- 0
test/utils.ts View File

@@ -1,6 +1,7 @@
import {IncomingHttpHeaders, IncomingMessage, OutgoingHttpHeaders, request, RequestOptions} from 'http'; import {IncomingHttpHeaders, IncomingMessage, OutgoingHttpHeaders, request, RequestOptions} from 'http';
import {Method} from '../src/backend/common'; import {Method} from '../src/backend/common';
import {DataSource} from '../src/backend/data-source'; import {DataSource} from '../src/backend/data-source';
import {FALLBACK_LANGUAGE, Language} from '../src/common';


interface ClientParams { interface ClientParams {
method: Method; method: Method;
@@ -188,3 +189,52 @@ export class DummyDataSource implements DataSource {
this.resource.dataSource = this; this.resource.dataSource = this;
} }
} }

export const TEST_LANGUAGE: Language = {
name: FALLBACK_LANGUAGE.name,
statusMessages: {
unableToInitializeResourceDataSource: 'unableToInitializeResourceDataSource',
unableToFetchResourceCollection: 'unableToFetchResourceCollection',
unableToFetchResource: 'unableToFetchResource',
resourceIdNotGiven: 'resourceIdNotGiven',
languageNotAcceptable: 'languageNotAcceptable',
encodingNotAcceptable: 'encodingNotAcceptable',
mediaTypeNotAcceptable: 'mediaTypeNotAcceptable',
methodNotAllowed: 'methodNotAllowed',
urlNotFound: 'urlNotFound',
badRequest: 'badRequest',
ok: 'ok',
resourceCollectionFetched: 'resourceCollectionFetched',
resourceFetched: 'resourceFetched',
resourceNotFound: 'resourceNotFound',
deleteNonExistingResource: 'deleteNonExistingResource',
unableToCreateResource: 'unableToCreateResource',
unableToBindResourceDataSource: 'unableToBindResourceDataSource',
unableToGenerateIdFromResourceDataSource: 'unableToGenerateIdFromResourceDataSource',
unableToAssignIdFromResourceDataSource: 'unableToAssignIdFromResourceDataSource',
unableToEmplaceResource: 'unableToEmplaceResource',
unableToSerializeResponse: 'unableToSerializeResponse',
unableToEncodeResponse: 'unableToEncodeResponse',
unableToDeleteResource: 'unableToDeleteResource',
unableToDeserializeResource: 'unableToDeserializeResource',
unableToDecodeResource: 'unableToDecodeResource',
resourceDeleted: 'resourceDeleted',
unableToDeserializeRequest: 'unableToDeserializeRequest',
patchNonExistingResource: 'patchNonExistingResource',
unableToPatchResource: 'unableToPatchResource',
invalidResourcePatch: 'invalidResourcePatch',
invalidResourcePatchType: 'invalidResourcePatchType',
invalidResource: 'invalidResource',
resourcePatched: 'resourcePatched',
resourceCreated: 'resourceCreated',
resourceReplaced: 'resourceReplaced',
notImplemented: 'notImplemented',
provideOptions: 'provideOptions',
internalServerError: 'internalServerError',
},
bodies: {
languageNotAcceptable: [],
encodingNotAcceptable: [],
mediaTypeNotAcceptable: [],
},
};

Loading…
Cancel
Save