Include tests for emplace, modify fallback content negotiation logic.master
@@ -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` |
@@ -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,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; | ||||
@@ -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'); | ||||
}); | }); |
@@ -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); | |||||
}); | }); | ||||
}); | }); | ||||
}); | }); |
@@ -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: [], | |||||
}, | |||||
}; |