HATEOAS-first backend framework.
Non puoi selezionare più di 25 argomenti Gli argomenti devono iniziare con una lettera o un numero, possono includere trattini ('-') e possono essere lunghi fino a 35 caratteri.

error-handling.test.ts 9.5 KiB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400
  1. import {
  2. beforeAll,
  3. afterAll,
  4. afterEach,
  5. beforeEach,
  6. describe,
  7. expect,
  8. it, vi,
  9. } from 'vitest';
  10. import {request} from 'http';
  11. import {constants} from 'http2';
  12. import {Backend} from '../../../src/backend';
  13. import {application, resource, validation as v, Resource, Application} from '../../../src/common';
  14. import { autoIncrement } from '../../fixtures';
  15. import {createTestClient, TestClient, DummyDataSource, DummyError} from '../../utils';
  16. import {DataSource} from '../../../src/backend/data-source';
  17. const PORT = 4001;
  18. const HOST = '127.0.0.1';
  19. const BASE_PATH = '/api';
  20. const ACCEPT = 'application/json';
  21. const ACCEPT_LANGUAGE = 'en';
  22. const ACCEPT_CHARSET = 'utf-8';
  23. const CONTENT_TYPE_CHARSET = 'utf-8';
  24. const CONTENT_TYPE = ACCEPT;
  25. describe('error handling', () => {
  26. let Piano: Resource;
  27. let app: Application;
  28. let dataSource: DataSource;
  29. let backend: Backend;
  30. let server: ReturnType<Backend['createHttpServer']>;
  31. let client: TestClient;
  32. beforeAll(() => {
  33. Piano = resource(v.object(
  34. {
  35. brand: v.string()
  36. },
  37. v.never()
  38. ))
  39. .name('Piano' as const)
  40. .route('pianos' as const)
  41. .id('id' as const, {
  42. generationStrategy: autoIncrement,
  43. serialize: (id) => id?.toString() ?? '0',
  44. deserialize: (id) => Number.isFinite(Number(id)) ? Number(id) : 0,
  45. schema: v.number(),
  46. });
  47. app = application({
  48. name: 'piano-service',
  49. })
  50. .resource(Piano);
  51. dataSource = new DummyDataSource();
  52. backend = app.createBackend({
  53. dataSource,
  54. });
  55. server = backend.createHttpServer({
  56. basePath: BASE_PATH
  57. });
  58. client = createTestClient({
  59. host: HOST,
  60. port: PORT,
  61. })
  62. .acceptMediaType(ACCEPT)
  63. .acceptLanguage(ACCEPT_LANGUAGE)
  64. .acceptCharset(ACCEPT_CHARSET)
  65. .contentType(CONTENT_TYPE)
  66. .contentCharset(CONTENT_TYPE_CHARSET);
  67. return new Promise((resolve, reject) => {
  68. server.on('error', (err) => {
  69. reject(err);
  70. });
  71. server.on('listening', () => {
  72. resolve();
  73. });
  74. server.listen({
  75. port: PORT
  76. });
  77. });
  78. });
  79. afterAll(() => new Promise<void>((resolve, reject) => {
  80. server.close((err) => {
  81. if (err) {
  82. reject(err);
  83. }
  84. resolve();
  85. });
  86. }));
  87. describe('serving collections', () => {
  88. beforeEach(() => {
  89. Piano.canFetchCollection();
  90. });
  91. afterEach(() => {
  92. Piano.canFetchCollection(false);
  93. });
  94. it('throws on query', async () => {
  95. vi
  96. .spyOn(DummyDataSource.prototype, 'getMultiple')
  97. .mockImplementationOnce(() => { throw new DummyError() });
  98. const [res] = await client({
  99. method: 'GET',
  100. path: `${BASE_PATH}/pianos`,
  101. });
  102. expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_INTERNAL_SERVER_ERROR);
  103. expect(res).toHaveProperty('statusMessage', 'Unable To Fetch Piano Collection');
  104. });
  105. it('throws on HEAD method', async () => {
  106. vi
  107. .spyOn(DummyDataSource.prototype, 'getMultiple')
  108. .mockImplementationOnce(() => { throw new DummyError() });
  109. const [res] = await client({
  110. method: 'HEAD',
  111. path: `${BASE_PATH}/pianos`,
  112. });
  113. expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_INTERNAL_SERVER_ERROR);
  114. expect(res).toHaveProperty('statusMessage', 'Unable To Fetch Piano Collection');
  115. });
  116. });
  117. describe('serving items', () => {
  118. beforeEach(() => {
  119. Piano.canFetchItem();
  120. });
  121. afterEach(() => {
  122. Piano.canFetchItem(false);
  123. });
  124. it('throws on query', async () => {
  125. vi
  126. .spyOn(DummyDataSource.prototype, 'getById')
  127. .mockImplementationOnce(() => { throw new DummyError() });
  128. const [res] = await client({
  129. method: 'GET',
  130. path: `${BASE_PATH}/pianos/2`,
  131. });
  132. expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_INTERNAL_SERVER_ERROR);
  133. expect(res).toHaveProperty('statusMessage', 'Unable To Fetch Piano');
  134. });
  135. it('throws on HEAD method', async () => {
  136. vi
  137. .spyOn(DummyDataSource.prototype, 'getById')
  138. .mockImplementationOnce(() => { throw new DummyError() });
  139. const [res] = await client({
  140. method: 'HEAD',
  141. path: `${BASE_PATH}/pianos/2`,
  142. });
  143. expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_INTERNAL_SERVER_ERROR);
  144. expect(res).toHaveProperty('statusMessage', 'Unable To Fetch Piano');
  145. });
  146. it('throws on item not found', async () => {
  147. vi
  148. .spyOn(DummyDataSource.prototype, 'getById')
  149. .mockResolvedValueOnce(null as never);
  150. const [res] = await client({
  151. method: 'GET',
  152. path: `${BASE_PATH}/pianos/2`,
  153. });
  154. expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_NOT_FOUND);
  155. });
  156. it('throws on item not found on HEAD method', async () => {
  157. const getById = vi.spyOn(DummyDataSource.prototype, 'getById');
  158. getById.mockResolvedValueOnce(null as never);
  159. const [res] = await client({
  160. method: 'HEAD',
  161. path: `${BASE_PATH}/pianos/2`,
  162. });
  163. expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_NOT_FOUND);
  164. });
  165. });
  166. describe('creating items', () => {
  167. const newData = {
  168. brand: 'K. Kawai'
  169. };
  170. const existingResource = {
  171. ...newData,
  172. id: 1,
  173. };
  174. beforeEach(() => {
  175. Piano.canCreate();
  176. });
  177. afterEach(() => {
  178. Piano.canCreate(false);
  179. });
  180. it('throws on error assigning ID', async () => {
  181. vi
  182. .spyOn(DummyDataSource.prototype, 'newId')
  183. .mockImplementationOnce(() => { throw new DummyError() });
  184. const [res] = await client({
  185. method: 'POST',
  186. path: `${BASE_PATH}/pianos`,
  187. body: newData,
  188. });
  189. expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_INTERNAL_SERVER_ERROR);
  190. expect(res).toHaveProperty('statusMessage', 'Unable To Assign ID From Piano Data Source');
  191. });
  192. it('throws on error creating resource', async () => {
  193. vi
  194. .spyOn(DummyDataSource.prototype, 'newId')
  195. .mockResolvedValueOnce(existingResource.id as never);
  196. vi
  197. .spyOn(DummyDataSource.prototype, 'create')
  198. .mockImplementationOnce(() => { throw new DummyError() });
  199. const [res] = await client({
  200. method: 'POST',
  201. path: `${BASE_PATH}/pianos`,
  202. body: newData,
  203. });
  204. expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_INTERNAL_SERVER_ERROR);
  205. expect(res).toHaveProperty('statusMessage', 'Unable To Create Piano');
  206. });
  207. });
  208. describe('patching items', () => {
  209. const existingResource = {
  210. id: 1,
  211. brand: 'Yamaha'
  212. };
  213. const newData = {
  214. brand: 'K. Kawai'
  215. };
  216. beforeEach(() => {
  217. Piano.canPatch();
  218. });
  219. afterEach(() => {
  220. Piano.canPatch(false);
  221. });
  222. // TODO add more tests
  223. it('throws on unable to fetch existing item', async () => {
  224. vi
  225. .spyOn(DummyDataSource.prototype, 'getById')
  226. .mockImplementationOnce(() => { throw new DummyError() });
  227. const [res] = await client({
  228. method: 'PATCH',
  229. path: `${BASE_PATH}/pianos/${existingResource.id}`,
  230. body: newData,
  231. headers: {
  232. 'content-type': 'application/merge-patch+json',
  233. },
  234. });
  235. expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_INTERNAL_SERVER_ERROR);
  236. expect(res).toHaveProperty('statusMessage', 'Unable To Fetch Piano');
  237. });
  238. it('throws on item to patch not found', async () => {
  239. vi
  240. .spyOn(DummyDataSource.prototype, 'getById')
  241. .mockResolvedValueOnce(null as never);
  242. const [res] = await client({
  243. method: 'PATCH',
  244. path: `${BASE_PATH}/pianos/${existingResource.id}`,
  245. body: newData,
  246. headers: {
  247. 'content-type': 'application/merge-patch+json',
  248. },
  249. });
  250. expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_NOT_FOUND);
  251. expect(res).toHaveProperty('statusMessage', 'Patch Non-Existing Piano');
  252. });
  253. });
  254. describe.skip('emplacing items', () => {
  255. const existingResource = {
  256. id: 1,
  257. brand: 'Yamaha'
  258. };
  259. beforeEach(() => {
  260. vi
  261. .spyOn(DummyDataSource.prototype, 'getById')
  262. .mockResolvedValueOnce(existingResource as never);
  263. });
  264. const newData = {
  265. id: 1,
  266. brand: 'K. Kawai'
  267. };
  268. beforeEach(() => {
  269. Piano.canEmplace();
  270. });
  271. afterEach(() => {
  272. Piano.canEmplace(false);
  273. });
  274. });
  275. describe('deleting items', () => {
  276. const existingResource = {
  277. id: 1,
  278. brand: 'Yamaha'
  279. };
  280. beforeEach(() => {
  281. Piano.canDelete();
  282. backend.throwsErrorOnDeletingNotFound();
  283. });
  284. afterEach(() => {
  285. Piano.canDelete(false);
  286. backend.throwsErrorOnDeletingNotFound(false);
  287. });
  288. it('throws on unable to check if item exists', async () => {
  289. vi
  290. .spyOn(DummyDataSource.prototype, 'getById')
  291. .mockImplementationOnce(() => { throw new DummyError() });
  292. const [res] = await client({
  293. method: 'DELETE',
  294. path: `${BASE_PATH}/pianos/2`,
  295. });
  296. expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_INTERNAL_SERVER_ERROR);
  297. expect(res).toHaveProperty('statusMessage', 'Unable To Fetch Piano');
  298. });
  299. it('throws on item not found', async () => {
  300. vi
  301. .spyOn(DummyDataSource.prototype, 'getById')
  302. .mockResolvedValueOnce(null as never);
  303. const [res] = await client({
  304. method: 'DELETE',
  305. path: `${BASE_PATH}/pianos/${existingResource.id}`,
  306. });
  307. expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_NOT_FOUND);
  308. expect(res).toHaveProperty('statusMessage', 'Delete Non-Existing Piano');
  309. });
  310. it('throws on unable to delete item', async () => {
  311. vi
  312. .spyOn(DummyDataSource.prototype, 'getById')
  313. .mockResolvedValueOnce(existingResource as never);
  314. vi
  315. .spyOn(DummyDataSource.prototype, 'delete')
  316. .mockImplementationOnce(() => { throw new DummyError() });
  317. const [res] = await client({
  318. method: 'DELETE',
  319. path: `${BASE_PATH}/pianos/${existingResource.id}`,
  320. });
  321. expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_INTERNAL_SERVER_ERROR);
  322. expect(res).toHaveProperty('statusMessage', 'Unable To Delete Piano');
  323. });
  324. });
  325. });