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.

481 lines
17 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. // odd that request() uses OutgoingHttpHeaders instead of IncomingHttpHeaders...
  26. const headers: OutgoingHttpHeaders = {
  27. ...(options.headers ?? {}),
  28. ...etcAdditionalHeaders,
  29. ...(params.headers ?? {}),
  30. };
  31. let contentTypeHeader: string | undefined;
  32. if (typeof params.body !== 'undefined') {
  33. contentTypeHeader = headers['content-type'] = params.headers?.['content-type'] ?? 'application/json';
  34. }
  35. const req = request({
  36. ...options,
  37. method: params.method,
  38. path: params.path,
  39. headers,
  40. });
  41. req.on('response', (res) => {
  42. // if (req.method.toUpperCase() === 'QUERY') {
  43. // res.statusMessage = '';
  44. // res.statusCode = 200;
  45. // }
  46. res.on('error', (err) => {
  47. reject(err);
  48. });
  49. let resBuffer: Buffer | undefined;
  50. res.on('data', (c) => {
  51. resBuffer = (
  52. typeof resBuffer === 'undefined'
  53. ? Buffer.from(c)
  54. : Buffer.concat([resBuffer, c])
  55. );
  56. });
  57. res.on('close', () => {
  58. const acceptHeader = Array.isArray(headers['accept']) ? headers['accept'].join('; ') : headers['accept'];
  59. const contentTypeBase = acceptHeader ?? 'application/octet-stream';
  60. const [type, subtype] = contentTypeBase.split('/');
  61. const allSubtypes = subtype.split('+');
  62. if (typeof resBuffer !== 'undefined') {
  63. if (allSubtypes.includes('json')) {
  64. const acceptCharset = (
  65. Array.isArray(headers['accept-charset'])
  66. ? headers['accept-charset'].join('; ')
  67. : headers['accept-charset']
  68. ) as BufferEncoding | undefined;
  69. resolve([res, JSON.parse(resBuffer.toString(acceptCharset ?? 'utf-8'))]);
  70. return;
  71. }
  72. if (type === 'text') {
  73. const acceptCharset = (
  74. Array.isArray(headers['accept-charset'])
  75. ? headers['accept-charset'].join('; ')
  76. : headers['accept-charset']
  77. ) as BufferEncoding | undefined;
  78. resolve([res, resBuffer.toString(acceptCharset ?? 'utf-8')]);
  79. return;
  80. }
  81. resolve([res, resBuffer]);
  82. return;
  83. }
  84. resolve([res]);
  85. });
  86. });
  87. req.on('error', (err) => {
  88. reject(err);
  89. })
  90. if (typeof params.body !== 'undefined') {
  91. const theContentTypeHeader = Array.isArray(contentTypeHeader) ? contentTypeHeader.join('; ') : contentTypeHeader?.toString();
  92. const contentTypeAll = theContentTypeHeader ?? 'application/octet-stream';
  93. const [contentTypeBase, ...contentTypeParams] = contentTypeAll.split(';').map((s) => s.replace(/\s+/g, '').trim());
  94. const charsetParam = contentTypeParams.find((s) => s.startsWith('charset='));
  95. const charset = charsetParam?.split('=')?.[1] as BufferEncoding | undefined;
  96. const [, subtype] = contentTypeBase.split('/');
  97. const allSubtypes = subtype.split('+');
  98. req.write(
  99. allSubtypes.includes('json')
  100. ? JSON.stringify(params.body)
  101. : Buffer.from(params.body?.toString() ?? '', contentTypeBase === 'text' ? charset : undefined)
  102. );
  103. }
  104. req.end();
  105. });
  106. client.acceptMediaType = function acceptMediaType(mediaType: string) {
  107. additionalHeaders['accept'] = mediaType;
  108. return this;
  109. };
  110. client.acceptLanguage = function acceptLanguage(language: string) {
  111. additionalHeaders['accept-language'] = language;
  112. return this;
  113. };
  114. client.acceptCharset = function acceptCharset(charset: string) {
  115. additionalHeaders['accept-charset'] = charset;
  116. return this;
  117. };
  118. client.contentType = function contentType(mediaType: string) {
  119. additionalHeaders['content-type'] = mediaType;
  120. return this;
  121. };
  122. client.contentCharset = function contentCharset(charset: string) {
  123. additionalHeaders['content-type'] = `${additionalHeaders['content-type']}; charset="${charset}"`;
  124. return this;
  125. };
  126. return client;
  127. };
  128. export const dummyGenerationStrategy = () => Promise.resolve();
  129. export class DummyError extends Error {}
  130. export class DummyDataSource implements DataSource {
  131. private resource?: { dataSource?: unknown };
  132. async create(): Promise<object> {
  133. return {};
  134. }
  135. async delete(): Promise<void> {}
  136. async emplace(): Promise<[object, boolean]> {
  137. return [{}, false];
  138. }
  139. async getById(): Promise<object> {
  140. return {};
  141. }
  142. async newId(): Promise<string> {
  143. return '';
  144. }
  145. async getMultiple(): Promise<object[]> {
  146. return [];
  147. }
  148. async getSingle(): Promise<object> {
  149. return {};
  150. }
  151. async getTotalCount(): Promise<number> {
  152. return 0;
  153. }
  154. async initialize(): Promise<void> {}
  155. async patch(): Promise<object> {
  156. return {};
  157. }
  158. prepareResource(rr: unknown) {
  159. this.resource = rr as unknown as { dataSource: DummyDataSource };
  160. this.resource.dataSource = this;
  161. }
  162. }
  163. export const TEST_LANGUAGE: Language = {
  164. name: FALLBACK_LANGUAGE.name,
  165. statusMessages: {
  166. resourceCollectionQueried: '$Resource Collection Queried',
  167. unableToSerializeResponse: 'Unable To Serialize Response',
  168. unableToEncodeResponse: 'Unable To Encode Response',
  169. unableToBindResourceDataSource: 'Unable To Bind $RESOURCE Data Source',
  170. unableToInitializeResourceDataSource: 'Unable To Initialize $RESOURCE Data Source',
  171. unableToFetchResourceCollection: 'Unable To Fetch $RESOURCE Collection',
  172. unableToFetchResource: 'Unable To Fetch $RESOURCE',
  173. unableToDeleteResource: 'Unable To Delete $RESOURCE',
  174. languageNotAcceptable: 'Language Not Acceptable',
  175. characterSetNotAcceptable: 'Character Set Not Acceptable',
  176. unableToDeserializeResource: 'Unable To Deserialize $RESOURCE',
  177. unableToDecodeResource: 'Unable To Decode $RESOURCE',
  178. mediaTypeNotAcceptable: 'Media Type Not Acceptable',
  179. methodNotAllowed: 'Method Not Allowed',
  180. urlNotFound: 'URL Not Found',
  181. badRequest: 'Bad Request',
  182. ok: 'OK',
  183. provideOptions: 'Provide Options',
  184. resourceCollectionFetched: '$RESOURCE Collection Fetched',
  185. resourceFetched: '$RESOURCE Fetched',
  186. resourceNotFound: '$RESOURCE Not Found',
  187. deleteNonExistingResource: 'Delete Non-Existing $RESOURCE',
  188. resourceDeleted: '$RESOURCE Deleted',
  189. unableToDeserializeRequest: 'Unable To Deserialize Request',
  190. patchNonExistingResource: 'Patch Non-Existing $RESOURCE',
  191. unableToPatchResource: 'Unable To Patch $RESOURCE',
  192. invalidResourcePatch: 'Invalid $RESOURCE Patch',
  193. invalidResourcePatchType: 'Invalid $RESOURCE Patch Type',
  194. invalidResource: 'Invalid $RESOURCE',
  195. resourcePatched: '$RESOURCE Patched',
  196. resourceCreated: '$RESOURCE Created',
  197. resourceReplaced: '$RESOURCE Replaced',
  198. unableToGenerateIdFromResourceDataSource: 'Unable To Generate ID From $RESOURCE Data Source',
  199. unableToAssignIdFromResourceDataSource: 'Unable To Assign ID From $RESOURCE Data Source',
  200. unableToEmplaceResource: 'Unable To Emplace $RESOURCE',
  201. resourceIdNotGiven: '$RESOURCE ID Not Given',
  202. unableToCreateResource: 'Unable To Create $RESOURCE',
  203. notImplemented: 'Not Implemented',
  204. internalServerError: 'Internal Server Error',
  205. },
  206. bodies: {
  207. badRequest: [
  208. 'An invalid request has been made.',
  209. [
  210. 'Check if the request body has all the required attributes for this endpoint.',
  211. 'Check if the request body has only the valid attributes for this endpoint.',
  212. 'Check if the request body matches the schema for the resource associated with this endpoint.',
  213. 'Check if the request is appropriate for this endpoint.',
  214. ],
  215. ],
  216. languageNotAcceptable: [
  217. 'The server could not process a response suitable for the client\'s provided language requirement.',
  218. [
  219. 'Choose from the available languages on this service.',
  220. 'Contact the administrator to provide localization for the client\'s given requirements.',
  221. ],
  222. ],
  223. characterSetNotAcceptable: [
  224. 'The server could not process a response suitable for the client\'s provided character set requirement.',
  225. [
  226. 'Choose from the available character sets on this service.',
  227. 'Contact the administrator to provide localization for the client\'s given requirements.',
  228. ],
  229. ],
  230. mediaTypeNotAcceptable: [
  231. 'The server could not process a response suitable for the client\'s provided media type requirement.',
  232. [
  233. 'Choose from the available media types on this service.',
  234. 'Contact the administrator to provide localization for the client\'s given requirements.',
  235. ],
  236. ],
  237. deleteNonExistingResource: [
  238. 'The client has attempted to delete a resource that does not exist.',
  239. [
  240. 'Ensure that the resource still exists.',
  241. 'Ensure that the correct method is provided.',
  242. ],
  243. ],
  244. internalServerError: [
  245. 'An unknown error has occurred within the service.',
  246. [
  247. 'Try the request again at a later time.',
  248. 'Contact the administrator if the service remains in a degraded or non-functional state.',
  249. ],
  250. ],
  251. invalidResource: [
  252. 'The request has an invalid structure or is missing some attributes.',
  253. [
  254. 'Check if the request body has all the required attributes for this endpoint.',
  255. 'Check if the request body has only the valid attributes for this endpoint.',
  256. 'Check if the request body matches the schema for the resource associated with this endpoint.',
  257. ],
  258. ],
  259. invalidResourcePatch: [
  260. 'The request has an invalid patch data.',
  261. [
  262. 'Check if the appropriate patch type is specified on the request data.',
  263. 'Check if the request body has all the required attributes for this endpoint.',
  264. 'Check if the request body has only the valid attributes for this endpoint.',
  265. 'Check if the request body matches the schema for the resource associated with this endpoint.',
  266. ],
  267. ],
  268. invalidResourcePatchType: [
  269. 'The request has an invalid or unsupported kind of patch data.',
  270. [
  271. 'Check if the appropriate patch type is specified on the request data.',
  272. 'Check if the request body has all the required attributes for this endpoint.',
  273. 'Check if the request body has only the valid attributes for this endpoint.',
  274. 'Check if the request body matches the schema for the resource associated with this endpoint.',
  275. ],
  276. ],
  277. methodNotAllowed: [
  278. 'A request with an invalid or unsupported method has been made.',
  279. [
  280. 'Check if the request method is appropriate for this endpoint.',
  281. 'Check if the client is authorized to perform the method on this endpoint.',
  282. ]
  283. ],
  284. notImplemented: [
  285. 'The service does not have any implementation for the accessed endpoint.',
  286. [
  287. 'Try the request again at a later time.',
  288. 'Contact the administrator if the service remains in a degraded or non-functional state.',
  289. ],
  290. ],
  291. patchNonExistingResource: [
  292. 'The client has attempted to patch a resource that does not exist.',
  293. [
  294. 'Ensure that the resource still exists.',
  295. 'Ensure that the correct method is provided.',
  296. ],
  297. ],
  298. resourceIdNotGiven: [
  299. 'The resource ID is not provided for the accessed endpoint.',
  300. [
  301. 'Check if the resource ID is provided and valid in the URL.',
  302. 'Check if the request method is appropriate for this endpoint.',
  303. ],
  304. ],
  305. unableToAssignIdFromResourceDataSource: [
  306. 'The resource could not be assigned an ID from the associated data source.',
  307. [
  308. 'Try the request again at a later time.',
  309. 'Contact the administrator regarding missing configuration or unavailability of dependencies.',
  310. ],
  311. ],
  312. unableToBindResourceDataSource: [
  313. 'The resource could not be associated from the data source.',
  314. [
  315. 'Try the request again at a later time.',
  316. 'Contact the administrator regarding missing configuration or unavailability of dependencies.',
  317. ],
  318. ],
  319. unableToCreateResource: [
  320. 'An error has occurred on creating the resource.',
  321. [
  322. 'Check if the request method is appropriate for this endpoint.',
  323. 'Check if the request body has all the required attributes for this endpoint.',
  324. 'Check if the request body has only the valid attributes for this endpoint.',
  325. 'Check if the request body matches the schema for the resource associated with this endpoint.',
  326. 'Try the request again at a later time.',
  327. 'Contact the administrator regarding missing configuration or unavailability of dependencies.',
  328. ],
  329. ],
  330. unableToDecodeResource: [
  331. 'The resource byte array could not be decoded for the provided character set.',
  332. [
  333. 'Choose from the available character sets on this service.',
  334. 'Contact the administrator to provide localization for the client\'s given requirements.',
  335. ],
  336. ],
  337. unableToDeleteResource: [
  338. 'An error has occurred on deleting the resource.',
  339. [
  340. 'Check if the request method is appropriate for this endpoint.',
  341. 'Try the request again at a later time.',
  342. 'Contact the administrator regarding missing configuration or unavailability of dependencies.',
  343. ],
  344. ],
  345. unableToDeserializeRequest: [
  346. 'The decoded request byte array could not be deserialized for the provided media type.',
  347. [
  348. 'Choose from the available media types on this service.',
  349. 'Contact the administrator to provide localization for the client\'s given requirements.',
  350. ],
  351. ],
  352. unableToDeserializeResource: [
  353. 'The decoded resource could not be deserialized for the provided media type.',
  354. [
  355. 'Choose from the available media types on this service.',
  356. 'Contact the administrator to provide localization for the client\'s given requirements.',
  357. ],
  358. ],
  359. unableToEmplaceResource: [
  360. 'An error has occurred on emplacing the resource.',
  361. [
  362. 'Check if the request method is appropriate for this endpoint.',
  363. 'Check if the request body has all the required attributes for this endpoint.',
  364. 'Check if the request body has only the valid attributes for this endpoint.',
  365. 'Check if the request body matches the schema for the resource associated with this endpoint.',
  366. 'Try the request again at a later time.',
  367. 'Contact the administrator regarding missing configuration or unavailability of dependencies.',
  368. ],
  369. ],
  370. unableToEncodeResponse: [
  371. 'The response data could not be encoded for the provided character set.',
  372. [
  373. 'Choose from the available character sets on this service.',
  374. 'Contact the administrator to provide localization for the client\'s given requirements.',
  375. ],
  376. ],
  377. unableToFetchResource: [
  378. 'An error has occurred on fetching the resource.',
  379. [
  380. 'Check if the request method is appropriate for this endpoint.',
  381. 'Try the request again at a later time.',
  382. 'Contact the administrator regarding missing configuration or unavailability of dependencies.',
  383. ],
  384. ],
  385. unableToFetchResourceCollection: [
  386. 'An error has occurred on fetching the resource collection.',
  387. [
  388. 'Check if the request method is appropriate for this endpoint.',
  389. 'Try the request again at a later time.',
  390. 'Contact the administrator regarding missing configuration or unavailability of dependencies.',
  391. ],
  392. ],
  393. unableToGenerateIdFromResourceDataSource: [
  394. 'The associated data source for the resource could not produce an ID.',
  395. [
  396. 'Try the request again at a later time.',
  397. 'Contact the administrator regarding missing configuration or unavailability of dependencies.',
  398. ],
  399. ],
  400. unableToInitializeResourceDataSource: [
  401. 'The associated data source for the resource could not be connected for usage.',
  402. [
  403. 'Try the request again at a later time.',
  404. 'Contact the administrator regarding missing configuration or unavailability of dependencies.',
  405. ],
  406. ],
  407. unableToPatchResource: [
  408. 'An error has occurred on patching the resource.',
  409. [
  410. 'Check if the request method is appropriate for this endpoint.',
  411. 'Check if the request body has all the required attributes for this endpoint.',
  412. 'Check if the request body has only the valid attributes for this endpoint.',
  413. 'Check if the request body matches the schema for the resource associated with this endpoint.',
  414. 'Try the request again at a later time.',
  415. 'Contact the administrator regarding missing configuration or unavailability of dependencies.',
  416. ],
  417. ],
  418. unableToSerializeResponse: [
  419. 'The response data could not be serialized for the provided media type.',
  420. [
  421. 'Choose from the available media types on this service.',
  422. 'Contact the administrator to provide localization for the client\'s given requirements.',
  423. ],
  424. ],
  425. urlNotFound: [
  426. 'An endpoint in the provided URL could not be found.',
  427. [
  428. 'Check if the request URL is correct.',
  429. 'Try the request again at a later time.',
  430. 'Contact the administrator regarding missing configuration or unavailability of dependencies.',
  431. ],
  432. ],
  433. resourceNotFound: [
  434. 'The resource in the provided URL could not be found.',
  435. [
  436. 'Check if the request URL is correct.',
  437. 'Try the request again at a later time.',
  438. 'Contact the administrator regarding missing configuration or unavailability of dependencies.',
  439. ],
  440. ],
  441. },
  442. };