HATEOAS-first backend framework.
Du kannst nicht mehr als 25 Themen auswählen Themen müssen entweder mit einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.

http.test.ts 23 KiB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103
  1. import {
  2. beforeAll,
  3. afterAll,
  4. afterEach,
  5. beforeEach,
  6. describe,
  7. expect,
  8. it,
  9. } from 'vitest';
  10. import {
  11. tmpdir
  12. } from 'os';
  13. import {
  14. mkdtemp,
  15. rm,
  16. writeFile,
  17. } from 'fs/promises';
  18. import {
  19. join
  20. } from 'path';
  21. import {request, Server} from 'http';
  22. import {constants} from 'http2';
  23. import {DataSource} from '../../src/backend/data-source';
  24. import { dataSources } from '../../src/backend';
  25. import { application, resource, validation as v, Resource } from '../../src';
  26. const PORT = 3000;
  27. const HOST = '127.0.0.1';
  28. const ACCEPT = 'application/json';
  29. const ACCEPT_LANGUAGE = 'en';
  30. const CONTENT_TYPE_CHARSET = 'utf-8';
  31. const CONTENT_TYPE = ACCEPT;
  32. const BASE_PATH = '/api';
  33. const autoIncrement = async (dataSource: DataSource) => {
  34. const data = await dataSource.getMultiple() as Record<string, string>[];
  35. const highestId = data.reduce<number>(
  36. (highestId, d) => (Number(d.id) > highestId ? Number(d.id) : highestId),
  37. -Infinity
  38. );
  39. if (Number.isFinite(highestId)) {
  40. return (highestId + 1);
  41. }
  42. return 1;
  43. };
  44. describe('yasumi HTTP', () => {
  45. let baseDir: string;
  46. beforeAll(async () => {
  47. try {
  48. baseDir = await mkdtemp(join(tmpdir(), 'yasumi-'));
  49. } catch {
  50. // noop
  51. }
  52. });
  53. afterAll(async () => {
  54. try {
  55. await rm(baseDir, {
  56. recursive: true,
  57. });
  58. } catch {
  59. // noop
  60. }
  61. });
  62. let Piano: Resource;
  63. beforeEach(() => {
  64. Piano = resource(v.object(
  65. {
  66. brand: v.string()
  67. },
  68. v.never()
  69. ))
  70. .name('Piano' as const)
  71. .route('pianos' as const)
  72. .id('id' as const, {
  73. generationStrategy: autoIncrement,
  74. serialize: (id) => id?.toString() ?? '0',
  75. deserialize: (id) => Number.isFinite(Number(id)) ? Number(id) : 0,
  76. schema: v.number(),
  77. });
  78. });
  79. let server: Server;
  80. beforeEach(() => {
  81. const app = application({
  82. name: 'piano-service',
  83. })
  84. .resource(Piano);
  85. const backend = app
  86. .createBackend({
  87. dataSource: new dataSources.jsonlFile.DataSource(baseDir),
  88. })
  89. .throwsErrorOnDeletingNotFound();
  90. server = backend.createHttpServer({
  91. basePath: BASE_PATH
  92. });
  93. return new Promise((resolve, reject) => {
  94. server.on('error', (err) => {
  95. reject(err);
  96. });
  97. server.on('listening', () => {
  98. resolve();
  99. });
  100. server.listen({
  101. port: PORT
  102. });
  103. });
  104. });
  105. afterEach(() => new Promise((resolve, reject) => {
  106. server.close((err) => {
  107. if (err) {
  108. reject(err);
  109. }
  110. resolve();
  111. });
  112. }));
  113. describe('happy path', () => {
  114. describe('serving collections', () => {
  115. beforeEach(() => {
  116. Piano.canFetchCollection();
  117. return new Promise((resolve) => {
  118. setTimeout(() => {
  119. resolve();
  120. });
  121. });
  122. });
  123. afterEach(() => {
  124. Piano.canFetchCollection(false);
  125. });
  126. it('returns options', () => {
  127. return new Promise<void>((resolve, reject) => {
  128. const req = request(
  129. {
  130. host: HOST,
  131. port: PORT,
  132. path: `${BASE_PATH}/pianos`,
  133. method: 'OPTIONS',
  134. headers: {
  135. 'Accept': ACCEPT,
  136. 'Accept-Language': ACCEPT_LANGUAGE,
  137. },
  138. },
  139. (res) => {
  140. res.on('error', (err) => {
  141. reject(err);
  142. });
  143. expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_NO_CONTENT);
  144. const allowedMethods = res.headers.allow?.split(',').map((s) => s.trim()) ?? [];
  145. expect(allowedMethods).toContain('GET');
  146. expect(allowedMethods).toContain('HEAD');
  147. resolve();
  148. },
  149. );
  150. req.on('error', (err) => {
  151. reject(err);
  152. });
  153. req.end();
  154. });
  155. });
  156. it('returns data', () => {
  157. return new Promise<void>((resolve, reject) => {
  158. const req = request(
  159. {
  160. host: HOST,
  161. port: PORT,
  162. path: `${BASE_PATH}/pianos`,
  163. method: 'GET',
  164. headers: {
  165. 'Accept': ACCEPT,
  166. 'Accept-Language': ACCEPT_LANGUAGE,
  167. },
  168. },
  169. (res) => {
  170. res.on('error', (err) => {
  171. reject(err);
  172. });
  173. expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_OK);
  174. // TODO test status messsages
  175. expect(res.headers).toHaveProperty('content-type', expect.stringContaining(ACCEPT));
  176. let resBuffer = Buffer.from('');
  177. res.on('data', (c) => {
  178. resBuffer = Buffer.concat([resBuffer, c]);
  179. });
  180. res.on('close', () => {
  181. const resBufferJson = resBuffer.toString(CONTENT_TYPE_CHARSET);
  182. const resData = JSON.parse(resBufferJson);
  183. expect(resData).toEqual([]);
  184. resolve();
  185. });
  186. },
  187. );
  188. req.on('error', (err) => {
  189. reject(err);
  190. });
  191. req.end();
  192. });
  193. });
  194. it('returns data on HEAD method', () => {
  195. return new Promise<void>((resolve, reject) => {
  196. const req = request(
  197. {
  198. host: HOST,
  199. port: PORT,
  200. path: `${BASE_PATH}/pianos`,
  201. method: 'HEAD',
  202. headers: {
  203. 'Accept': ACCEPT,
  204. 'Accept-Language': ACCEPT_LANGUAGE,
  205. },
  206. },
  207. (res) => {
  208. res.on('error', (err) => {
  209. reject(err);
  210. });
  211. expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_OK);
  212. resolve();
  213. },
  214. );
  215. req.on('error', (err) => {
  216. reject(err);
  217. });
  218. req.end();
  219. });
  220. });
  221. });
  222. describe('serving items', () => {
  223. const existingResource = {
  224. id: 1,
  225. brand: 'Yamaha'
  226. };
  227. beforeEach(async () => {
  228. const resourcePath = join(baseDir, 'pianos.jsonl');
  229. await writeFile(resourcePath, JSON.stringify(existingResource));
  230. });
  231. beforeEach(() => {
  232. Piano.canFetchItem();
  233. return new Promise((resolve) => {
  234. setTimeout(() => {
  235. resolve();
  236. });
  237. });
  238. });
  239. afterEach(() => {
  240. Piano.canFetchItem(false);
  241. });
  242. it('returns data', () => {
  243. return new Promise<void>((resolve, reject) => {
  244. // TODO all responses should have serialized ids
  245. const req = request(
  246. {
  247. host: HOST,
  248. port: PORT,
  249. path: `${BASE_PATH}/pianos/${existingResource.id}`,
  250. method: 'GET',
  251. headers: {
  252. 'Accept': ACCEPT,
  253. 'Accept-Language': ACCEPT_LANGUAGE,
  254. },
  255. },
  256. (res) => {
  257. res.on('error', (err) => {
  258. reject(err);
  259. });
  260. expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_OK);
  261. expect(res.headers).toHaveProperty('content-type', expect.stringContaining(ACCEPT));
  262. let resBuffer = Buffer.from('');
  263. res.on('data', (c) => {
  264. resBuffer = Buffer.concat([resBuffer, c]);
  265. });
  266. res.on('close', () => {
  267. const resBufferJson = resBuffer.toString(CONTENT_TYPE_CHARSET);
  268. const resData = JSON.parse(resBufferJson);
  269. expect(resData).toEqual(existingResource);
  270. resolve();
  271. });
  272. },
  273. );
  274. req.on('error', (err) => {
  275. reject(err);
  276. });
  277. req.end();
  278. });
  279. });
  280. it('returns data on HEAD method', () => {
  281. return new Promise<void>((resolve, reject) => {
  282. // TODO all responses should have serialized ids
  283. const req = request(
  284. {
  285. host: HOST,
  286. port: PORT,
  287. path: `${BASE_PATH}/pianos/${existingResource.id}`,
  288. method: 'HEAD',
  289. headers: {
  290. 'Accept': ACCEPT,
  291. 'Accept-Language': ACCEPT_LANGUAGE,
  292. },
  293. },
  294. (res) => {
  295. res.on('error', (err) => {
  296. reject(err);
  297. });
  298. expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_OK);
  299. resolve();
  300. },
  301. );
  302. req.on('error', (err) => {
  303. reject(err);
  304. });
  305. req.end();
  306. });
  307. });
  308. it('returns options', () => {
  309. return new Promise<void>((resolve, reject) => {
  310. const req = request(
  311. {
  312. host: HOST,
  313. port: PORT,
  314. path: `${BASE_PATH}/pianos/${existingResource.id}`,
  315. method: 'OPTIONS',
  316. headers: {
  317. 'Accept': ACCEPT,
  318. 'Accept-Language': ACCEPT_LANGUAGE,
  319. },
  320. },
  321. (res) => {
  322. res.on('error', (err) => {
  323. reject(err);
  324. });
  325. expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_NO_CONTENT);
  326. const allowedMethods = res.headers.allow?.split(',').map((s) => s.trim()) ?? [];
  327. expect(allowedMethods).toContain('GET');
  328. expect(allowedMethods).toContain('HEAD');
  329. resolve();
  330. },
  331. );
  332. req.on('error', (err) => {
  333. reject(err);
  334. });
  335. req.end();
  336. });
  337. });
  338. });
  339. describe('creating items', () => {
  340. const existingResource = {
  341. id: 1,
  342. brand: 'Yamaha'
  343. };
  344. const newResourceData = {
  345. brand: 'K. Kawai'
  346. };
  347. beforeEach(async () => {
  348. const resourcePath = join(baseDir, 'pianos.jsonl');
  349. await writeFile(resourcePath, JSON.stringify(existingResource));
  350. });
  351. beforeEach(() => {
  352. Piano.canCreate();
  353. });
  354. afterEach(() => {
  355. Piano.canCreate(false);
  356. });
  357. it('returns data', () => {
  358. return new Promise<void>((resolve, reject) => {
  359. const req = request(
  360. {
  361. host: HOST,
  362. port: PORT,
  363. path: `${BASE_PATH}/pianos`,
  364. method: 'POST',
  365. headers: {
  366. 'Accept': ACCEPT,
  367. 'Accept-Language': ACCEPT_LANGUAGE,
  368. 'Content-Type': `${CONTENT_TYPE}; charset="${CONTENT_TYPE_CHARSET}"`,
  369. },
  370. },
  371. (res) => {
  372. res.on('error', (err) => {
  373. reject(err);
  374. });
  375. expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_CREATED);
  376. expect(res.headers).toHaveProperty('content-type', expect.stringContaining(ACCEPT));
  377. let resBuffer = Buffer.from('');
  378. res.on('data', (c) => {
  379. resBuffer = Buffer.concat([resBuffer, c]);
  380. });
  381. res.on('close', () => {
  382. const resBufferJson = resBuffer.toString(CONTENT_TYPE_CHARSET);
  383. const resData = JSON.parse(resBufferJson);
  384. expect(resData).toEqual({
  385. ...newResourceData,
  386. id: 2
  387. });
  388. resolve();
  389. });
  390. },
  391. );
  392. req.on('error', (err) => {
  393. reject(err);
  394. });
  395. req.write(JSON.stringify(newResourceData));
  396. req.end();
  397. });
  398. });
  399. it('returns options', () => {
  400. return new Promise<void>((resolve, reject) => {
  401. const req = request(
  402. {
  403. host: HOST,
  404. port: PORT,
  405. path: `${BASE_PATH}/pianos`,
  406. method: 'OPTIONS',
  407. headers: {
  408. 'Accept': ACCEPT,
  409. 'Accept-Language': ACCEPT_LANGUAGE,
  410. },
  411. },
  412. (res) => {
  413. res.on('error', (err) => {
  414. reject(err);
  415. });
  416. expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_NO_CONTENT);
  417. const allowedMethods = res.headers.allow?.split(',').map((s) => s.trim()) ?? [];
  418. expect(allowedMethods).toContain('POST');
  419. resolve();
  420. },
  421. );
  422. req.on('error', (err) => {
  423. reject(err);
  424. });
  425. req.end();
  426. });
  427. });
  428. });
  429. describe('patching items', () => {
  430. const existingResource = {
  431. id: 1,
  432. brand: 'Yamaha'
  433. };
  434. const patchData = {
  435. brand: 'K. Kawai'
  436. };
  437. beforeEach(async () => {
  438. const resourcePath = join(baseDir, 'pianos.jsonl');
  439. await writeFile(resourcePath, JSON.stringify(existingResource));
  440. });
  441. beforeEach(() => {
  442. Piano.canPatch();
  443. });
  444. afterEach(() => {
  445. Piano.canPatch(false);
  446. });
  447. it('returns data', () => {
  448. return new Promise<void>((resolve, reject) => {
  449. const req = request(
  450. {
  451. host: HOST,
  452. port: PORT,
  453. path: `${BASE_PATH}/pianos/${existingResource.id}`,
  454. method: 'PATCH',
  455. headers: {
  456. 'Accept': ACCEPT,
  457. 'Accept-Language': ACCEPT_LANGUAGE,
  458. 'Content-Type': `${CONTENT_TYPE}; charset="${CONTENT_TYPE_CHARSET}"`,
  459. },
  460. },
  461. (res) => {
  462. res.on('error', (err) => {
  463. reject(err);
  464. });
  465. expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_OK);
  466. expect(res.headers).toHaveProperty('content-type', expect.stringContaining(ACCEPT));
  467. let resBuffer = Buffer.from('');
  468. res.on('data', (c) => {
  469. resBuffer = Buffer.concat([resBuffer, c]);
  470. });
  471. res.on('close', () => {
  472. const resBufferJson = resBuffer.toString(CONTENT_TYPE_CHARSET);
  473. const resData = JSON.parse(resBufferJson);
  474. expect(resData).toEqual({
  475. ...existingResource,
  476. ...patchData,
  477. });
  478. resolve();
  479. });
  480. },
  481. );
  482. req.on('error', (err) => {
  483. reject(err);
  484. });
  485. req.write(JSON.stringify(patchData));
  486. req.end();
  487. });
  488. });
  489. it('returns options', () => {
  490. return new Promise<void>((resolve, reject) => {
  491. const req = request(
  492. {
  493. host: HOST,
  494. port: PORT,
  495. path: `${BASE_PATH}/pianos/${existingResource.id}`,
  496. method: 'OPTIONS',
  497. headers: {
  498. 'Accept': ACCEPT,
  499. 'Accept-Language': ACCEPT_LANGUAGE,
  500. },
  501. },
  502. (res) => {
  503. res.on('error', (err) => {
  504. reject(err);
  505. });
  506. expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_NO_CONTENT);
  507. const allowedMethods = res.headers.allow?.split(',').map((s) => s.trim()) ?? [];
  508. expect(allowedMethods).toContain('PATCH');
  509. resolve();
  510. },
  511. );
  512. req.on('error', (err) => {
  513. reject(err);
  514. });
  515. req.end();
  516. });
  517. });
  518. });
  519. describe('emplacing items', () => {
  520. const existingResource = {
  521. id: 1,
  522. brand: 'Yamaha'
  523. };
  524. const emplaceResourceData = {
  525. id: 1,
  526. brand: 'K. Kawai'
  527. };
  528. beforeEach(async () => {
  529. const resourcePath = join(baseDir, 'pianos.jsonl');
  530. await writeFile(resourcePath, JSON.stringify(existingResource));
  531. });
  532. beforeEach(() => {
  533. Piano.canEmplace();
  534. });
  535. afterEach(() => {
  536. Piano.canEmplace(false);
  537. });
  538. it('returns data for replacement', () => {
  539. return new Promise<void>((resolve, reject) => {
  540. const req = request(
  541. {
  542. host: HOST,
  543. port: PORT,
  544. path: `${BASE_PATH}/pianos/${emplaceResourceData.id}`,
  545. method: 'PUT',
  546. headers: {
  547. 'Accept': ACCEPT,
  548. 'Accept-Language': ACCEPT_LANGUAGE,
  549. 'Content-Type': `${CONTENT_TYPE}; charset="${CONTENT_TYPE_CHARSET}"`,
  550. },
  551. },
  552. (res) => {
  553. res.on('error', (err) => {
  554. reject(err);
  555. });
  556. expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_OK);
  557. expect(res.headers).toHaveProperty('content-type', expect.stringContaining(ACCEPT));
  558. let resBuffer = Buffer.from('');
  559. res.on('data', (c) => {
  560. resBuffer = Buffer.concat([resBuffer, c]);
  561. });
  562. res.on('close', () => {
  563. const resBufferJson = resBuffer.toString(CONTENT_TYPE_CHARSET);
  564. const resData = JSON.parse(resBufferJson);
  565. expect(resData).toEqual(emplaceResourceData);
  566. resolve();
  567. });
  568. },
  569. );
  570. req.on('error', (err) => {
  571. reject(err);
  572. });
  573. req.write(JSON.stringify(emplaceResourceData));
  574. req.end();
  575. });
  576. });
  577. it('returns data for creation', () => {
  578. return new Promise<void>((resolve, reject) => {
  579. const newId = 2;
  580. const req = request(
  581. {
  582. host: HOST,
  583. port: PORT,
  584. path: `${BASE_PATH}/pianos/${newId}`,
  585. method: 'PUT',
  586. headers: {
  587. 'Accept': ACCEPT,
  588. 'Accept-Language': ACCEPT_LANGUAGE,
  589. 'Content-Type': `${CONTENT_TYPE}; charset="${CONTENT_TYPE_CHARSET}"`,
  590. },
  591. },
  592. (res) => {
  593. res.on('error', (err) => {
  594. reject(err);
  595. });
  596. expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_CREATED);
  597. expect(res.headers).toHaveProperty('content-type', expect.stringContaining(ACCEPT));
  598. let resBuffer = Buffer.from('');
  599. res.on('data', (c) => {
  600. resBuffer = Buffer.concat([resBuffer, c]);
  601. });
  602. res.on('close', () => {
  603. const resBufferJson = resBuffer.toString(CONTENT_TYPE_CHARSET);
  604. const resData = JSON.parse(resBufferJson);
  605. expect(resData).toEqual({
  606. ...emplaceResourceData,
  607. id: newId,
  608. });
  609. resolve();
  610. });
  611. },
  612. );
  613. req.on('error', (err) => {
  614. reject(err);
  615. });
  616. req.write(JSON.stringify({
  617. ...emplaceResourceData,
  618. id: newId,
  619. }));
  620. req.end();
  621. });
  622. });
  623. it('returns options', () => {
  624. return new Promise<void>((resolve, reject) => {
  625. const req = request(
  626. {
  627. host: HOST,
  628. port: PORT,
  629. path: `${BASE_PATH}/pianos/${existingResource.id}`,
  630. method: 'OPTIONS',
  631. headers: {
  632. 'Accept': ACCEPT,
  633. 'Accept-Language': ACCEPT_LANGUAGE,
  634. },
  635. },
  636. (res) => {
  637. res.on('error', (err) => {
  638. reject(err);
  639. });
  640. expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_NO_CONTENT);
  641. const allowedMethods = res.headers.allow?.split(',').map((s) => s.trim()) ?? [];
  642. expect(allowedMethods).toContain('PUT');
  643. resolve();
  644. },
  645. );
  646. req.on('error', (err) => {
  647. reject(err);
  648. });
  649. req.end();
  650. });
  651. });
  652. });
  653. describe('deleting items', () => {
  654. const existingResource = {
  655. id: 1,
  656. brand: 'Yamaha'
  657. };
  658. beforeEach(async () => {
  659. const resourcePath = join(baseDir, 'pianos.jsonl');
  660. await writeFile(resourcePath, JSON.stringify(existingResource));
  661. });
  662. beforeEach(() => {
  663. Piano.canDelete();
  664. });
  665. afterEach(() => {
  666. Piano.canDelete(false);
  667. });
  668. it('responds', () => {
  669. return new Promise<void>((resolve, reject) => {
  670. const req = request(
  671. {
  672. host: HOST,
  673. port: PORT,
  674. path: `${BASE_PATH}/pianos/${existingResource.id}`,
  675. method: 'DELETE',
  676. headers: {
  677. 'Accept': ACCEPT,
  678. 'Accept-Language': ACCEPT_LANGUAGE,
  679. },
  680. },
  681. (res) => {
  682. res.on('error', (err) => {
  683. reject(err);
  684. });
  685. expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_NO_CONTENT);
  686. resolve();
  687. },
  688. );
  689. req.on('error', (err) => {
  690. reject(err);
  691. });
  692. req.end();
  693. });
  694. });
  695. it('returns options', () => {
  696. return new Promise<void>((resolve, reject) => {
  697. const req = request(
  698. {
  699. host: HOST,
  700. port: PORT,
  701. path: `${BASE_PATH}/pianos/${existingResource.id}`,
  702. method: 'OPTIONS',
  703. headers: {
  704. 'Accept': ACCEPT,
  705. 'Accept-Language': ACCEPT_LANGUAGE,
  706. },
  707. },
  708. (res) => {
  709. res.on('error', (err) => {
  710. reject(err);
  711. });
  712. expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_NO_CONTENT);
  713. const allowedMethods = res.headers.allow?.split(',').map((s) => s.trim()) ?? [];
  714. expect(allowedMethods).toContain('DELETE');
  715. resolve();
  716. },
  717. );
  718. req.on('error', (err) => {
  719. reject(err);
  720. });
  721. req.end();
  722. });
  723. });
  724. });
  725. });
  726. describe('error handling', () => {
  727. describe.skip('serving collections', () => {
  728. beforeEach(() => {
  729. Piano.canFetchCollection();
  730. return new Promise((resolve) => {
  731. setTimeout(() => {
  732. resolve();
  733. });
  734. });
  735. });
  736. afterEach(() => {
  737. Piano.canFetchCollection(false);
  738. });
  739. });
  740. describe('serving items', () => {
  741. const data = {
  742. id: 1,
  743. brand: 'Yamaha'
  744. };
  745. beforeEach(async () => {
  746. const resourcePath = join(baseDir, 'pianos.jsonl');
  747. await writeFile(resourcePath, JSON.stringify(data));
  748. });
  749. beforeEach(() => {
  750. Piano.canFetchItem();
  751. return new Promise((resolve) => {
  752. setTimeout(() => {
  753. resolve();
  754. });
  755. });
  756. });
  757. afterEach(() => {
  758. Piano.canFetchItem(false);
  759. });
  760. it('throws on item not found', () => {
  761. return new Promise<void>((resolve, reject) => {
  762. const req = request(
  763. {
  764. host: HOST,
  765. port: PORT,
  766. path: `${BASE_PATH}/pianos/2`,
  767. method: 'GET',
  768. headers: {
  769. 'Accept': ACCEPT,
  770. 'Accept-Language': ACCEPT_LANGUAGE,
  771. },
  772. },
  773. (res) => {
  774. res.on('error', (err) => {
  775. Piano.canFetchItem(false);
  776. reject(err);
  777. });
  778. expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_NOT_FOUND);
  779. resolve();
  780. },
  781. );
  782. req.on('error', (err) => {
  783. reject(err);
  784. });
  785. req.end();
  786. });
  787. });
  788. it('throws on item not found on HEAD method', () => {
  789. return new Promise<void>((resolve, reject) => {
  790. const req = request(
  791. {
  792. host: HOST,
  793. port: PORT,
  794. path: `${BASE_PATH}/pianos/2`,
  795. method: 'HEAD',
  796. headers: {
  797. 'Accept': ACCEPT,
  798. 'Accept-Language': ACCEPT_LANGUAGE,
  799. },
  800. },
  801. (res) => {
  802. res.on('error', (err) => {
  803. Piano.canFetchItem(false);
  804. reject(err);
  805. });
  806. expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_NOT_FOUND);
  807. resolve();
  808. },
  809. );
  810. req.on('error', (err) => {
  811. reject(err);
  812. });
  813. req.end();
  814. });
  815. });
  816. });
  817. describe.skip('creating items', () => {
  818. const data = {
  819. id: 1,
  820. brand: 'Yamaha'
  821. };
  822. const newData = {
  823. brand: 'K. Kawai'
  824. };
  825. beforeEach(async () => {
  826. const resourcePath = join(baseDir, 'pianos.jsonl');
  827. await writeFile(resourcePath, JSON.stringify(data));
  828. });
  829. beforeEach(() => {
  830. Piano.canCreate();
  831. });
  832. afterEach(() => {
  833. Piano.canCreate(false);
  834. });
  835. });
  836. describe('patching items', () => {
  837. const data = {
  838. id: 1,
  839. brand: 'Yamaha'
  840. };
  841. const newData = {
  842. brand: 'K. Kawai'
  843. };
  844. beforeEach(async () => {
  845. const resourcePath = join(baseDir, 'pianos.jsonl');
  846. await writeFile(resourcePath, JSON.stringify(data));
  847. });
  848. beforeEach(() => {
  849. Piano.canPatch();
  850. });
  851. afterEach(() => {
  852. Piano.canPatch(false);
  853. });
  854. it('throws on item to patch not found', () => {
  855. return new Promise<void>((resolve, reject) => {
  856. const req = request(
  857. {
  858. host: HOST,
  859. port: PORT,
  860. path: `${BASE_PATH}/pianos/2`,
  861. method: 'PATCH',
  862. headers: {
  863. 'Accept': ACCEPT,
  864. 'Accept-Language': ACCEPT_LANGUAGE,
  865. 'Content-Type': `${CONTENT_TYPE}; charset="${CONTENT_TYPE_CHARSET}"`,
  866. },
  867. },
  868. (res) => {
  869. res.on('error', (err) => {
  870. reject(err);
  871. });
  872. expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_NOT_FOUND);
  873. resolve();
  874. },
  875. );
  876. req.on('error', (err) => {
  877. reject(err);
  878. });
  879. req.write(JSON.stringify(newData));
  880. req.end();
  881. });
  882. });
  883. });
  884. describe.skip('emplacing items', () => {
  885. const data = {
  886. id: 1,
  887. brand: 'Yamaha'
  888. };
  889. const newData = {
  890. id: 1,
  891. brand: 'K. Kawai'
  892. };
  893. beforeEach(async () => {
  894. const resourcePath = join(baseDir, 'pianos.jsonl');
  895. await writeFile(resourcePath, JSON.stringify(data));
  896. });
  897. beforeEach(() => {
  898. Piano.canEmplace();
  899. });
  900. afterEach(() => {
  901. Piano.canEmplace(false);
  902. });
  903. });
  904. describe('deleting items', () => {
  905. const data = {
  906. id: 1,
  907. brand: 'Yamaha'
  908. };
  909. beforeEach(async () => {
  910. const resourcePath = join(baseDir, 'pianos.jsonl');
  911. await writeFile(resourcePath, JSON.stringify(data));
  912. });
  913. beforeEach(() => {
  914. Piano.canDelete();
  915. });
  916. afterEach(() => {
  917. Piano.canDelete(false);
  918. });
  919. it('throws on item not found', () => {
  920. return new Promise<void>((resolve, reject) => {
  921. const req = request(
  922. {
  923. host: HOST,
  924. port: PORT,
  925. path: `${BASE_PATH}/pianos/2`,
  926. method: 'DELETE',
  927. headers: {
  928. 'Accept': ACCEPT,
  929. 'Accept-Language': ACCEPT_LANGUAGE,
  930. },
  931. },
  932. (res) => {
  933. res.on('error', (err) => {
  934. reject(err);
  935. });
  936. expect(res).toHaveProperty('statusCode', constants.HTTP_STATUS_NOT_FOUND);
  937. resolve();
  938. },
  939. );
  940. req.on('error', (err) => {
  941. reject(err);
  942. });
  943. req.end();
  944. });
  945. });
  946. });
  947. });
  948. });