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.

242 lines
7.2 KiB

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