Include tests for emplace, modify fallback content negotiation logic.master
@@ -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` |
@@ -384,40 +384,43 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr | |||
const handleMiddlewareError = (processRequestErrRaw: Error) => (resourceReq: ResourceRequestContext, res: http.ServerResponse<RequestContext>) => { | |||
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<string, string> = { | |||
'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') { | |||
@@ -4,7 +4,7 @@ import Negotiator from 'negotiator'; | |||
declare module '../../../../common' { | |||
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; | |||
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; | |||
@@ -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'); | |||
}); |
@@ -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); | |||
}); | |||
}); | |||
}); |
@@ -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: [], | |||
}, | |||
}; |