HATEOAS-first backend framework.
您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

http.test.ts 23 KiB

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