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; path: string; headers?: IncomingHttpHeaders; body?: unknown; } type ResponseBody = Buffer | string | object; export interface TestClient { (params: ClientParams): Promise<[IncomingMessage, ResponseBody?]>; acceptMediaType(mediaType: string): this; acceptLanguage(language: string): this; acceptCharset(charset: string): this; contentType(mediaType: string): this; contentCharset(charset: string): this; } export const createTestClient = (options: Omit): TestClient => { const additionalHeaders: OutgoingHttpHeaders = {}; const client = (params: ClientParams) => new Promise<[IncomingMessage, ResponseBody?]>((resolve, reject) => { const { ...etcAdditionalHeaders } = additionalHeaders; const headers: OutgoingHttpHeaders = { ...(options.headers ?? {}), ...etcAdditionalHeaders, }; let contentTypeHeader: string | undefined; if (typeof params.body !== 'undefined') { contentTypeHeader = headers['content-type'] = params.headers?.['content-type'] ?? 'application/json'; } const req = request({ ...options, method: params.method, path: params.path, headers, }); req.on('response', (res) => { res.on('error', (err) => { reject(err); }); let resBuffer: Buffer | undefined; res.on('data', (c) => { resBuffer = ( typeof resBuffer === 'undefined' ? Buffer.from(c) : Buffer.concat([resBuffer, c]) ); }); res.on('close', () => { const acceptHeader = Array.isArray(headers['accept']) ? headers['accept'].join('; ') : headers['accept']; const contentTypeBase = acceptHeader ?? 'application/octet-stream'; const [type, subtype] = contentTypeBase.split('/'); const allSubtypes = subtype.split('+'); if (typeof resBuffer !== 'undefined') { if (allSubtypes.includes('json')) { const acceptCharset = ( Array.isArray(headers['accept-charset']) ? headers['accept-charset'].join('; ') : headers['accept-charset'] ) as BufferEncoding | undefined; resolve([res, JSON.parse(resBuffer.toString(acceptCharset ?? 'utf-8'))]); return; } if (type === 'text') { const acceptCharset = ( Array.isArray(headers['accept-charset']) ? headers['accept-charset'].join('; ') : headers['accept-charset'] ) as BufferEncoding | undefined; resolve([res, resBuffer.toString(acceptCharset ?? 'utf-8')]); return; } resolve([res, resBuffer]); return; } resolve([res]); }); }); req.on('error', (err) => { reject(err); }) if (typeof params.body !== 'undefined') { const theContentTypeHeader = Array.isArray(contentTypeHeader) ? contentTypeHeader.join('; ') : contentTypeHeader?.toString(); const contentTypeAll = theContentTypeHeader ?? 'application/octet-stream'; const [contentTypeBase, ...contentTypeParams] = contentTypeAll.split(';').map((s) => s.replace(/\s+/g, '').trim()); const charsetParam = contentTypeParams.find((s) => s.startsWith('charset=')); const charset = charsetParam?.split('=')?.[1] as BufferEncoding | undefined; const [, subtype] = contentTypeBase.split('/'); const allSubtypes = subtype.split('+'); req.write( allSubtypes.includes('json') ? JSON.stringify(params.body) : Buffer.from(params.body?.toString() ?? '', contentTypeBase === 'text' ? charset : undefined) ); } req.end(); }); client.acceptMediaType = function acceptMediaType(mediaType: string) { additionalHeaders['accept'] = mediaType; return this; }; client.acceptLanguage = function acceptLanguage(language: string) { additionalHeaders['accept-language'] = language; return this; }; client.acceptCharset = function acceptCharset(charset: string) { additionalHeaders['accept-charset'] = charset; return this; }; client.contentType = function contentType(mediaType: string) { additionalHeaders['content-type'] = mediaType; return this; }; client.contentCharset = function contentCharset(charset: string) { additionalHeaders['content-type'] = `${additionalHeaders['content-type']}; charset="${charset}"`; return this; }; return client; }; export class DummyError extends Error {} export class DummyDataSource implements DataSource { private resource?: { dataSource?: unknown }; async create(): Promise { return {}; } async delete(): Promise {} async emplace(): Promise<[object, boolean]> { return [{}, false]; } async getById(): Promise { return {}; } async newId(): Promise { return ''; } async getMultiple(): Promise { return []; } async getSingle(): Promise { return {}; } async getTotalCount(): Promise { return 0; } async initialize(): Promise {} async patch(): Promise { return {}; } prepareResource(rr: unknown) { this.resource = rr as unknown as { dataSource: DummyDataSource }; 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: [], }, };