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.

241 lines
7.2 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 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. };