HATEOAS-first backend framework.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

243 lines
7.3 KiB

  1. import {IncomingHttpHeaders, IncomingMessage, OutgoingHttpHeaders, request, RequestOptions} from 'http';
  2. import {Method} from '../src/backend/common';
  3. import {DataSource} from '../src/backend/data-source';
  4. import {FALLBACK_LANGUAGE, Language} from '../src/common';
  5. interface ClientParams {
  6. method: Method;
  7. path: string;
  8. headers?: IncomingHttpHeaders;
  9. body?: unknown;
  10. }
  11. type ResponseBody = Buffer | string | object;
  12. export interface TestClient {
  13. (params: ClientParams): Promise<[IncomingMessage, ResponseBody?]>;
  14. acceptMediaType(mediaType: string): this;
  15. acceptLanguage(language: string): this;
  16. acceptCharset(charset: string): this;
  17. contentType(mediaType: string): this;
  18. contentCharset(charset: string): this;
  19. }
  20. export const createTestClient = (options: Omit<RequestOptions, 'method' | 'path'>): TestClient => {
  21. const additionalHeaders: OutgoingHttpHeaders = {};
  22. const client = (params: ClientParams) => new Promise<[IncomingMessage, ResponseBody?]>((resolve, reject) => {
  23. const {
  24. ...etcAdditionalHeaders
  25. } = additionalHeaders;
  26. const headers: OutgoingHttpHeaders = {
  27. ...(options.headers ?? {}),
  28. ...etcAdditionalHeaders,
  29. };
  30. let contentTypeHeader: string | undefined;
  31. if (typeof params.body !== 'undefined') {
  32. contentTypeHeader = headers['content-type'] = params.headers?.['content-type'] ?? 'application/json';
  33. }
  34. const req = request({
  35. ...options,
  36. method: params.method,
  37. path: params.path,
  38. headers,
  39. });
  40. req.on('response', (res) => {
  41. res.on('error', (err) => {
  42. reject(err);
  43. });
  44. let resBuffer: Buffer | undefined;
  45. res.on('data', (c) => {
  46. resBuffer = (
  47. typeof resBuffer === 'undefined'
  48. ? Buffer.from(c)
  49. : Buffer.concat([resBuffer, c])
  50. );
  51. });
  52. res.on('close', () => {
  53. const acceptHeader = Array.isArray(headers['accept']) ? headers['accept'].join('; ') : headers['accept'];
  54. const contentTypeBase = acceptHeader ?? 'application/octet-stream';
  55. const [type, subtype] = contentTypeBase.split('/');
  56. const allSubtypes = subtype.split('+');
  57. if (typeof resBuffer !== 'undefined') {
  58. if (allSubtypes.includes('json')) {
  59. const acceptCharset = (
  60. Array.isArray(headers['accept-charset'])
  61. ? headers['accept-charset'].join('; ')
  62. : headers['accept-charset']
  63. ) as BufferEncoding | undefined;
  64. resolve([res, JSON.parse(resBuffer.toString(acceptCharset ?? 'utf-8'))]);
  65. return;
  66. }
  67. if (type === 'text') {
  68. const acceptCharset = (
  69. Array.isArray(headers['accept-charset'])
  70. ? headers['accept-charset'].join('; ')
  71. : headers['accept-charset']
  72. ) as BufferEncoding | undefined;
  73. resolve([res, resBuffer.toString(acceptCharset ?? 'utf-8')]);
  74. return;
  75. }
  76. resolve([res, resBuffer]);
  77. return;
  78. }
  79. resolve([res]);
  80. });
  81. });
  82. req.on('error', (err) => {
  83. reject(err);
  84. })
  85. if (typeof params.body !== 'undefined') {
  86. const theContentTypeHeader = Array.isArray(contentTypeHeader) ? contentTypeHeader.join('; ') : contentTypeHeader?.toString();
  87. const contentTypeAll = theContentTypeHeader ?? 'application/octet-stream';
  88. const [contentTypeBase, ...contentTypeParams] = contentTypeAll.split(';').map((s) => s.replace(/\s+/g, '').trim());
  89. const charsetParam = contentTypeParams.find((s) => s.startsWith('charset='));
  90. const charset = charsetParam?.split('=')?.[1] as BufferEncoding | undefined;
  91. const [, subtype] = contentTypeBase.split('/');
  92. const allSubtypes = subtype.split('+');
  93. req.write(
  94. allSubtypes.includes('json')
  95. ? JSON.stringify(params.body)
  96. : Buffer.from(params.body?.toString() ?? '', contentTypeBase === 'text' ? charset : undefined)
  97. );
  98. }
  99. req.end();
  100. });
  101. client.acceptMediaType = function acceptMediaType(mediaType: string) {
  102. additionalHeaders['accept'] = mediaType;
  103. return this;
  104. };
  105. client.acceptLanguage = function acceptLanguage(language: string) {
  106. additionalHeaders['accept-language'] = language;
  107. return this;
  108. };
  109. client.acceptCharset = function acceptCharset(charset: string) {
  110. additionalHeaders['accept-charset'] = charset;
  111. return this;
  112. };
  113. client.contentType = function contentType(mediaType: string) {
  114. additionalHeaders['content-type'] = mediaType;
  115. return this;
  116. };
  117. client.contentCharset = function contentCharset(charset: string) {
  118. additionalHeaders['content-type'] = `${additionalHeaders['content-type']}; charset="${charset}"`;
  119. return this;
  120. };
  121. return client;
  122. };
  123. export const dummyGenerationStrategy = () => Promise.resolve();
  124. export class DummyError extends Error {}
  125. export class DummyDataSource implements DataSource {
  126. private resource?: { dataSource?: unknown };
  127. async create(): Promise<object> {
  128. return {};
  129. }
  130. async delete(): Promise<void> {}
  131. async emplace(): Promise<[object, boolean]> {
  132. return [{}, false];
  133. }
  134. async getById(): Promise<object> {
  135. return {};
  136. }
  137. async newId(): Promise<string> {
  138. return '';
  139. }
  140. async getMultiple(): Promise<object[]> {
  141. return [];
  142. }
  143. async getSingle(): Promise<object> {
  144. return {};
  145. }
  146. async getTotalCount(): Promise<number> {
  147. return 0;
  148. }
  149. async initialize(): Promise<void> {}
  150. async patch(): Promise<object> {
  151. return {};
  152. }
  153. prepareResource(rr: unknown) {
  154. this.resource = rr as unknown as { dataSource: DummyDataSource };
  155. this.resource.dataSource = this;
  156. }
  157. }
  158. export const TEST_LANGUAGE: Language = {
  159. name: FALLBACK_LANGUAGE.name,
  160. statusMessages: {
  161. unableToInitializeResourceDataSource: 'unableToInitializeResourceDataSource',
  162. unableToFetchResourceCollection: 'unableToFetchResourceCollection',
  163. unableToFetchResource: 'unableToFetchResource',
  164. resourceIdNotGiven: 'resourceIdNotGiven',
  165. languageNotAcceptable: 'languageNotAcceptable',
  166. encodingNotAcceptable: 'encodingNotAcceptable',
  167. mediaTypeNotAcceptable: 'mediaTypeNotAcceptable',
  168. methodNotAllowed: 'methodNotAllowed',
  169. urlNotFound: 'urlNotFound',
  170. badRequest: 'badRequest',
  171. ok: 'ok',
  172. resourceCollectionFetched: 'resourceCollectionFetched',
  173. resourceFetched: 'resourceFetched',
  174. resourceNotFound: 'resourceNotFound',
  175. deleteNonExistingResource: 'deleteNonExistingResource',
  176. unableToCreateResource: 'unableToCreateResource',
  177. unableToBindResourceDataSource: 'unableToBindResourceDataSource',
  178. unableToGenerateIdFromResourceDataSource: 'unableToGenerateIdFromResourceDataSource',
  179. unableToAssignIdFromResourceDataSource: 'unableToAssignIdFromResourceDataSource',
  180. unableToEmplaceResource: 'unableToEmplaceResource',
  181. unableToSerializeResponse: 'unableToSerializeResponse',
  182. unableToEncodeResponse: 'unableToEncodeResponse',
  183. unableToDeleteResource: 'unableToDeleteResource',
  184. unableToDeserializeResource: 'unableToDeserializeResource',
  185. unableToDecodeResource: 'unableToDecodeResource',
  186. resourceDeleted: 'resourceDeleted',
  187. unableToDeserializeRequest: 'unableToDeserializeRequest',
  188. patchNonExistingResource: 'patchNonExistingResource',
  189. unableToPatchResource: 'unableToPatchResource',
  190. invalidResourcePatch: 'invalidResourcePatch',
  191. invalidResourcePatchType: 'invalidResourcePatchType',
  192. invalidResource: 'invalidResource',
  193. resourcePatched: 'resourcePatched',
  194. resourceCreated: 'resourceCreated',
  195. resourceReplaced: 'resourceReplaced',
  196. notImplemented: 'notImplemented',
  197. provideOptions: 'provideOptions',
  198. internalServerError: 'internalServerError',
  199. },
  200. bodies: {
  201. languageNotAcceptable: [],
  202. encodingNotAcceptable: [],
  203. mediaTypeNotAcceptable: [],
  204. },
  205. };