FetchUtilsTests.js 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. const { expect } = require('chai')
  2. const { FetchError, AbortError } = require('node-fetch')
  3. const { Readable } = require('stream')
  4. const { once } = require('events')
  5. const { TestServer } = require('./helpers/TestServer')
  6. const {
  7. fetchJson,
  8. fetchStream,
  9. fetchNothing,
  10. fetchString,
  11. RequestFailedError,
  12. } = require('../..')
  13. const PORT = 30001
  14. describe('fetch-utils', function () {
  15. before(async function () {
  16. this.server = new TestServer()
  17. await this.server.start(PORT)
  18. this.url = path => `http://localhost:${PORT}${path}`
  19. })
  20. after(async function () {
  21. await this.server.stop()
  22. })
  23. describe('fetchJson', function () {
  24. it('parses a JSON response', async function () {
  25. const json = await fetchJson(this.url('/json/hello'))
  26. expect(json).to.deep.equal({ msg: 'hello' })
  27. })
  28. it('parses JSON in the request', async function () {
  29. const json = await fetchJson(this.url('/json/add'), {
  30. method: 'POST',
  31. json: { a: 2, b: 3 },
  32. })
  33. expect(json).to.deep.equal({ sum: 5 })
  34. })
  35. it('accepts stringified JSON as body', async function () {
  36. const json = await fetchJson(this.url('/json/add'), {
  37. method: 'POST',
  38. body: JSON.stringify({ a: 2, b: 3 }),
  39. headers: { 'Content-Type': 'application/json' },
  40. })
  41. expect(json).to.deep.equal({ sum: 5 })
  42. })
  43. it('throws a FetchError when the payload is not JSON', async function () {
  44. await expect(fetchJson(this.url('/hello'))).to.be.rejectedWith(FetchError)
  45. })
  46. it('aborts the request if JSON parsing fails', async function () {
  47. await expect(fetchJson(this.url('/large'))).to.be.rejectedWith(FetchError)
  48. await expectRequestAborted(this.server.lastReq)
  49. })
  50. it('handles errors when the payload is JSON', async function () {
  51. await expect(fetchJson(this.url('/json/500'))).to.be.rejectedWith(
  52. RequestFailedError
  53. )
  54. await expectRequestAborted(this.server.lastReq)
  55. })
  56. it('handles errors when the payload is not JSON', async function () {
  57. await expect(fetchJson(this.url('/500'))).to.be.rejectedWith(
  58. RequestFailedError
  59. )
  60. await expectRequestAborted(this.server.lastReq)
  61. })
  62. it('supports abort signals', async function () {
  63. await expect(
  64. fetchJson(this.url('/hang'), { signal: AbortSignal.timeout(10) })
  65. ).to.be.rejectedWith(AbortError)
  66. await expectRequestAborted(this.server.lastReq)
  67. })
  68. it('supports basic auth', async function () {
  69. const json = await fetchJson(this.url('/json/basic-auth'), {
  70. basicAuth: { user: 'user', password: 'pass' },
  71. })
  72. expect(json).to.deep.equal({ key: 'verysecret' })
  73. })
  74. it("destroys the request body if it doesn't get consumed", async function () {
  75. const stream = Readable.from(infiniteIterator())
  76. await fetchJson(this.url('/json/ignore-request'), {
  77. method: 'POST',
  78. body: stream,
  79. })
  80. expect(stream.destroyed).to.be.true
  81. })
  82. })
  83. describe('fetchStream', function () {
  84. it('returns a stream', async function () {
  85. const stream = await fetchStream(this.url('/large'))
  86. const text = await streamToString(stream)
  87. expect(text).to.equal(this.server.largePayload)
  88. })
  89. it('aborts the request when the stream is destroyed', async function () {
  90. const stream = await fetchStream(this.url('/large'))
  91. stream.destroy()
  92. await expectRequestAborted(this.server.lastReq)
  93. })
  94. it('aborts the request when the request body is destroyed', async function () {
  95. const stream = Readable.from(infiniteIterator())
  96. const promise = fetchStream(this.url('/hang'), {
  97. method: 'POST',
  98. body: stream,
  99. })
  100. stream.destroy()
  101. await expect(promise).to.be.rejectedWith(AbortError)
  102. await expectRequestAborted(this.server.lastReq)
  103. })
  104. it('handles errors', async function () {
  105. await expect(fetchStream(this.url('/500'))).to.be.rejectedWith(
  106. RequestFailedError
  107. )
  108. await expectRequestAborted(this.server.lastReq)
  109. })
  110. it('supports abort signals', async function () {
  111. await expect(
  112. fetchStream(this.url('/hang'), { signal: AbortSignal.timeout(10) })
  113. ).to.be.rejectedWith(AbortError)
  114. await expectRequestAborted(this.server.lastReq)
  115. })
  116. it('destroys the request body when an error occurs', async function () {
  117. const stream = Readable.from(infiniteIterator())
  118. await expect(
  119. fetchStream(this.url('/hang'), {
  120. body: stream,
  121. signal: AbortSignal.timeout(10),
  122. })
  123. ).to.be.rejectedWith(AbortError)
  124. expect(stream.destroyed).to.be.true
  125. })
  126. })
  127. describe('fetchNothing', function () {
  128. it('closes the connection', async function () {
  129. await fetchNothing(this.url('/large'))
  130. await expectRequestAborted(this.server.lastReq)
  131. })
  132. it('aborts the request when the request body is destroyed', async function () {
  133. const stream = Readable.from(infiniteIterator())
  134. const promise = fetchNothing(this.url('/hang'), {
  135. method: 'POST',
  136. body: stream,
  137. })
  138. stream.destroy()
  139. await expect(promise).to.be.rejectedWith(AbortError)
  140. await expectRequestAborted(this.server.lastReq)
  141. })
  142. it("doesn't abort the request if the request body ends normally", async function () {
  143. const stream = Readable.from('hello there')
  144. await fetchNothing(this.url('/sink'), { method: 'POST', body: stream })
  145. })
  146. it('handles errors', async function () {
  147. await expect(fetchNothing(this.url('/500'))).to.be.rejectedWith(
  148. RequestFailedError
  149. )
  150. await expectRequestAborted(this.server.lastReq)
  151. })
  152. it('supports abort signals', async function () {
  153. await expect(
  154. fetchNothing(this.url('/hang'), { signal: AbortSignal.timeout(10) })
  155. ).to.be.rejectedWith(AbortError)
  156. await expectRequestAborted(this.server.lastReq)
  157. })
  158. it('destroys the request body when an error occurs', async function () {
  159. const stream = Readable.from(infiniteIterator())
  160. await expect(
  161. fetchNothing(this.url('/hang'), {
  162. body: stream,
  163. signal: AbortSignal.timeout(10),
  164. })
  165. ).to.be.rejectedWith(AbortError)
  166. expect(stream.destroyed).to.be.true
  167. })
  168. })
  169. describe('fetchString', function () {
  170. it('returns a string', async function () {
  171. const body = await fetchString(this.url('/hello'))
  172. expect(body).to.equal('hello')
  173. })
  174. it('handles errors', async function () {
  175. await expect(fetchJson(this.url('/500'))).to.be.rejectedWith(
  176. RequestFailedError
  177. )
  178. await expectRequestAborted(this.server.lastReq)
  179. })
  180. })
  181. })
  182. async function streamToString(stream) {
  183. let s = ''
  184. for await (const chunk of stream) {
  185. s += chunk
  186. }
  187. return s
  188. }
  189. async function* infiniteIterator() {
  190. let i = 1
  191. while (true) {
  192. yield `chunk ${i++}\n`
  193. }
  194. }
  195. async function expectRequestAborted(req) {
  196. if (!req.destroyed) {
  197. await once(req, 'close')
  198. expect(req.destroyed).to.be.true
  199. }
  200. }