diff --git a/TODO.md b/TODO.md index 9c579f6..377a333 100644 --- a/TODO.md +++ b/TODO.md @@ -26,3 +26,7 @@ - [ ] Implement RPC endpoints - [ ] 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. +- [ ] OpenAPI support + - [ ] Swagger docs plugin + - [ ] Plugin support +- [ ] Add option to reject content negotiation params with `406 Not Acceptable` diff --git a/src/backend/servers/http/core.ts b/src/backend/servers/http/core.ts index 449ba2f..d7e3469 100644 --- a/src/backend/servers/http/core.ts +++ b/src/backend/servers/http/core.ts @@ -384,40 +384,43 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr const handleMiddlewareError = (processRequestErrRaw: Error) => (resourceReq: ResourceRequestContext, res: http.ServerResponse) => { const finalErr = processRequestErrRaw as ErrorPlainResponse; 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 serialized; 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) { - res.statusMessage = resourceReq.backend.cn.language.statusMessages['unableToSerializeResponse']?.replace( + res.statusMessage = language.statusMessages['unableToSerializeResponse']?.replace( /\$RESOURCE/g, - resourceReq.resource!.state.itemName) ?? ''; + resourceReq.resource.state.itemName) ?? ''; res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR); res.end(); return; } try { - encoded = typeof serialized !== 'undefined' ? resourceReq.backend.cn.charset.encode(serialized) : undefined; + encoded = typeof serialized !== 'undefined' ? charset.encode(serialized) : undefined; } 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.end(); return; } 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) .join('; '); - res.statusMessage = resourceReq.backend.cn.language.statusMessages[ + res.statusMessage = language.statusMessages[ finalErr.statusMessage ?? 'internalServerError' ]?.replace(/\$RESOURCE/g, - resourceReq.resource!.state.itemName); + resourceReq.resource.state.itemName); res.writeHead(finalErr.statusCode ?? constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, headers); if (typeof encoded !== 'undefined') { 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 if (typeof plainReq.resource !== 'undefined') { 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 const effectiveMiddlewares = ( typeof resourceReq.resourceId === 'string' @@ -452,7 +459,7 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr ...( middlewareState.headers ?? {} ), - 'Content-Language': resourceReq.cn.language.name, + 'Content-Language': language.name, }; if (middlewareState instanceof http.ServerResponse) { // TODO streaming responses @@ -464,10 +471,10 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr if (typeof middlewareState.body !== 'undefined') { let serialized; try { - serialized = resourceReq.cn.mediaType.serialize(middlewareState.body); + serialized = mediaType.serialize(middlewareState.body); } catch (cause) { const headers: Record = { - 'Content-Language': resourceReq.backend.cn.language.name, + 'Content-Language': language.name, }; if (resourceReq.method === 'POST') { 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}`; }).join(','); } - res.statusMessage = resourceReq.backend.cn.language.statusMessages['unableToSerializeResponse']?.replace( + res.statusMessage = language.statusMessages['unableToSerializeResponse']?.replace( /\$RESOURCE/g, resourceReq.resource!.state.itemName) ?? ''; res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, headers); @@ -491,24 +498,24 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr } try { - encoded = resourceReq.cn.charset.encode(serialized); + encoded = charset.encode(serialized); } 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) ?? ''; res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, { - 'Content-Language': resourceReq.backend.cn.language.name, + 'Content-Language': language.name, }); res.end(); return; } headers['Content-Type'] = [ - resourceReq.cn.mediaType.name, - `charset=${resourceReq.cn.charset.name}`, + mediaType.name, + `charset=${charset.name}`, ].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.writeHead(middlewareState.statusCode, headers); if (typeof encoded !== 'undefined') { diff --git a/src/backend/servers/http/decorators/backend/content-negotiation.ts b/src/backend/servers/http/decorators/backend/content-negotiation.ts index 6896647..0c50c21 100644 --- a/src/backend/servers/http/decorators/backend/content-negotiation.ts +++ b/src/backend/servers/http/decorators/backend/content-negotiation.ts @@ -4,7 +4,7 @@ import Negotiator from 'negotiator'; declare module '../../../../common' { interface RequestContext { - cn: ContentNegotiation; + cn: Partial; } } @@ -20,9 +20,9 @@ export const decorateRequestWithContentNegotiation: RequestDecorator = (req) => const mediaTypeCandidate = negotiator.mediaType(availableMediaTypes.map((l) => l.name)) ?? req.backend.cn.mediaType.name; 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; diff --git a/test/e2e/features.test.ts b/test/features/decorators.test.ts similarity index 100% rename from test/e2e/features.test.ts rename to test/features/decorators.test.ts diff --git a/test/e2e/http/default.test.ts b/test/handlers/http/default.test.ts similarity index 86% rename from test/e2e/http/default.test.ts rename to test/handlers/http/default.test.ts index b0a42d7..56182ab 100644 --- a/test/e2e/http/default.test.ts +++ b/test/handlers/http/default.test.ts @@ -10,9 +10,15 @@ import { } from 'vitest'; import {constants} from 'http2'; 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 {createTestClient, DummyDataSource, TestClient} from '../../utils'; +import {createTestClient, DummyDataSource, TEST_LANGUAGE, TestClient} from '../../utils'; import {DataSource} from '../../../src/backend/data-source'; const PORT = 3000; @@ -24,7 +30,7 @@ const ACCEPT_CHARSET = 'utf-8'; const CONTENT_TYPE_CHARSET = 'utf-8'; const CONTENT_TYPE = ACCEPT; -describe.only('happy path', () => { +describe('happy path', () => { let Piano: Resource; let app: Application; let dataSource: DataSource; @@ -51,6 +57,7 @@ describe.only('happy path', () => { app = application({ name: 'piano-service', }) + .language(TEST_LANGUAGE) .resource(Piano); dataSource = new DummyDataSource(); @@ -120,7 +127,7 @@ describe.only('happy path', () => { }); 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)); if (typeof resData === 'undefined') { @@ -138,7 +145,7 @@ describe.only('happy path', () => { }); 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 () => { @@ -148,7 +155,7 @@ describe.only('happy path', () => { }); 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()) ?? []; expect(allowedMethods).toContain('GET'); expect(allowedMethods).toContain('HEAD'); @@ -182,7 +189,7 @@ describe.only('happy path', () => { }); 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)); if (typeof resData === 'undefined') { 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('statusMessage', 'Piano Fetched'); + expect(res).toHaveProperty('statusMessage', TEST_LANGUAGE.statusMessages.resourceFetched); }); it('returns options', async () => { @@ -209,7 +216,7 @@ describe.only('happy path', () => { }); 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()) ?? []; expect(allowedMethods).toContain('GET'); expect(allowedMethods).toContain('HEAD'); @@ -254,7 +261,7 @@ describe.only('happy path', () => { }); 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('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('statusMessage', 'Provide Options'); + expect(res).toHaveProperty('statusMessage', TEST_LANGUAGE.statusMessages.provideOptions); const allowedMethods = res.headers.allow?.split(',').map((s) => s.trim()) ?? []; expect(allowedMethods).toContain('POST'); }); }); - describe.only('patching items', () => { + describe('patching items', () => { const existingResource = { id: 1, brand: 'Yamaha' @@ -322,7 +329,7 @@ describe.only('happy path', () => { }); 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()) ?? []; expect(allowedMethods).toContain('PATCH'); 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('statusMessage', 'Piano Patched'); + expect(res).toHaveProperty('statusMessage', TEST_LANGUAGE.statusMessages.resourcePatched); expect(res.headers).toHaveProperty('content-type', expect.stringContaining(ACCEPT)); if (typeof resData === 'undefined') { @@ -366,7 +373,7 @@ describe.only('happy path', () => { Piano.canPatch(false).canPatch(['delta']); }); - it.only('returns data', async () => { + it('returns data', async () => { const [res, resData] = await client({ method: 'PATCH', 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('statusMessage', 'Piano Patched'); + expect(res).toHaveProperty('statusMessage', TEST_LANGUAGE.statusMessages.resourcePatched); expect(res.headers).toHaveProperty('content-type', expect.stringContaining(ACCEPT)); if (typeof resData === 'undefined') { @@ -433,7 +440,7 @@ describe.only('happy path', () => { }); 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)); if (typeof resData === 'undefined') { @@ -465,7 +472,7 @@ describe.only('happy path', () => { }); 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('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('statusMessage', 'Provide Options'); + expect(res).toHaveProperty('statusMessage', TEST_LANGUAGE.statusMessages.provideOptions); const allowedMethods = res.headers.allow?.split(',').map((s) => s.trim()) ?? []; expect(allowedMethods).toContain('PUT'); }); @@ -526,7 +533,7 @@ describe.only('happy path', () => { }); 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(resData).toBeUndefined(); }); @@ -538,7 +545,7 @@ describe.only('happy path', () => { }); 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()) ?? []; expect(allowedMethods).toContain('DELETE'); }); diff --git a/test/e2e/http/error-handling.test.ts b/test/handlers/http/error-handling.test.ts similarity index 82% rename from test/e2e/http/error-handling.test.ts rename to test/handlers/http/error-handling.test.ts index 90d8427..0d17cdd 100644 --- a/test/e2e/http/error-handling.test.ts +++ b/test/handlers/http/error-handling.test.ts @@ -11,10 +11,10 @@ import {constants} from 'http2'; import {Backend} from '../../../src/backend'; import {application, resource, validation as v, Resource, Application, Delta} from '../../../src/common'; 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'; -const PORT = 4001; +const PORT = 3001; const HOST = '127.0.0.1'; const BASE_PATH = '/api'; const ACCEPT = 'application/json'; @@ -50,6 +50,7 @@ describe('error handling', () => { app = application({ name: 'piano-service', }) + .language(TEST_LANGUAGE) .resource(Piano); dataSource = new DummyDataSource(); @@ -117,7 +118,7 @@ describe('error handling', () => { }); 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 () => { @@ -131,7 +132,7 @@ describe('error handling', () => { }); 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('statusMessage', 'Unable To Fetch Piano'); + expect(res).toHaveProperty('statusMessage', TEST_LANGUAGE.statusMessages.unableToFetchResource); }); 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('statusMessage', 'Unable To Fetch Piano'); + expect(res).toHaveProperty('statusMessage', TEST_LANGUAGE.statusMessages.unableToFetchResource); }); 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('statusMessage', 'Unable To Assign ID From Piano Data Source'); + expect(res).toHaveProperty('statusMessage', TEST_LANGUAGE.statusMessages.unableToAssignIdFromResourceDataSource); }); 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('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('statusMessage', 'Unable To Fetch Piano'); + expect(res).toHaveProperty('statusMessage', TEST_LANGUAGE.statusMessages.unableToFetchResource); Piano.canPatch(false); }); @@ -299,7 +300,7 @@ describe('error handling', () => { }); 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); }); @@ -327,7 +328,7 @@ describe('error handling', () => { }); 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('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 () => { @@ -371,7 +372,7 @@ describe('error handling', () => { }); 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 () => { @@ -391,10 +392,10 @@ describe('error handling', () => { }); 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({ method: 'PATCH', 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('statusMessage', 'Invalid Piano Patch'); + expect(res).toHaveProperty('statusMessage', TEST_LANGUAGE.statusMessages.invalidResourcePatch); }); }); }); - describe.skip('emplacing items', () => { + describe('emplacing items', () => { const existingResource = { id: 1, brand: 'Yamaha' @@ -440,6 +441,21 @@ describe('error handling', () => { afterEach(() => { 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', () => { @@ -469,7 +485,7 @@ describe('error handling', () => { }); 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 () => { @@ -483,7 +499,7 @@ describe('error handling', () => { }); 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 () => { @@ -501,7 +517,7 @@ describe('error handling', () => { }); 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); }); }); }); diff --git a/test/utils.ts b/test/utils.ts index 7b669ef..b3d7b17 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -1,6 +1,7 @@ import {IncomingHttpHeaders, IncomingMessage, OutgoingHttpHeaders, request, RequestOptions} from 'http'; import {Method} from '../src/backend/common'; import {DataSource} from '../src/backend/data-source'; +import {FALLBACK_LANGUAGE, Language} from '../src/common'; interface ClientParams { method: Method; @@ -188,3 +189,52 @@ export class DummyDataSource implements DataSource { 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: [], + }, +};