fetch.test.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. import 'whatwg-fetch'; // fetch polyfill needed for PhantomJs rendering
  2. import {
  3. isContentTypeApplicationJson,
  4. parseBody,
  5. parseCredentials,
  6. parseHeaders,
  7. parseInitFromOptions,
  8. parseResponseBody,
  9. parseUrlFromOptions,
  10. } from './fetch';
  11. jest.mock('@grafana/data', () => ({
  12. ...(jest.requireActual('@grafana/data') as unknown as object),
  13. deprecationWarning: () => {},
  14. }));
  15. describe('parseUrlFromOptions', () => {
  16. it.each`
  17. params | url | expected
  18. ${undefined} | ${'api/dashboard'} | ${'api/dashboard'}
  19. ${{ key: 'value' }} | ${'api/dashboard'} | ${'api/dashboard?key=value'}
  20. ${{ key: undefined }} | ${'api/dashboard'} | ${'api/dashboard'}
  21. ${{ firstKey: 'first value', secondValue: 'second value' }} | ${'api/dashboard'} | ${'api/dashboard?firstKey=first%20value&secondValue=second%20value'}
  22. ${{ firstKey: 'first value', secondValue: undefined }} | ${'api/dashboard'} | ${'api/dashboard?firstKey=first%20value'}
  23. ${{ id: [1, 2, 3] }} | ${'api/dashboard'} | ${'api/dashboard?id=1&id=2&id=3'}
  24. ${{ id: [] }} | ${'api/dashboard'} | ${'api/dashboard'}
  25. `(
  26. "when called with params: '$params' and url: '$url' then result should be '$expected'",
  27. ({ params, url, expected }) => {
  28. expect(parseUrlFromOptions({ params, url })).toEqual(expected);
  29. }
  30. );
  31. });
  32. describe('parseInitFromOptions', () => {
  33. it.each`
  34. method | data | withCredentials | credentials | expected
  35. ${undefined} | ${undefined} | ${undefined} | ${undefined} | ${{ method: undefined, headers: { map: { accept: 'application/json, text/plain, */*' } }, body: undefined, credentials: 'same-origin' }}
  36. ${'GET'} | ${undefined} | ${undefined} | ${undefined} | ${{ method: 'GET', headers: { map: { accept: 'application/json, text/plain, */*' } }, body: undefined, credentials: 'same-origin' }}
  37. ${'POST'} | ${{ id: '0' }} | ${undefined} | ${undefined} | ${{ method: 'POST', headers: { map: { 'content-type': 'application/json', accept: 'application/json, text/plain, */*' } }, body: '{"id":"0"}', credentials: 'same-origin' }}
  38. ${'PUT'} | ${{ id: '0' }} | ${undefined} | ${undefined} | ${{ method: 'PUT', headers: { map: { 'content-type': 'application/json', accept: 'application/json, text/plain, */*' } }, body: '{"id":"0"}', credentials: 'same-origin' }}
  39. ${'monkey'} | ${undefined} | ${undefined} | ${'omit'} | ${{ method: 'monkey', headers: { map: { accept: 'application/json, text/plain, */*' } }, body: undefined, credentials: 'omit' }}
  40. ${'GET'} | ${undefined} | ${true} | ${undefined} | ${{ method: 'GET', headers: { map: { accept: 'application/json, text/plain, */*' } }, body: undefined, credentials: 'include' }}
  41. ${'GET'} | ${undefined} | ${true} | ${'omit'} | ${{ method: 'GET', headers: { map: { accept: 'application/json, text/plain, */*' } }, body: undefined, credentials: 'omit' }}
  42. `(
  43. "when called with method: '$method', data: '$data', withCredentials: '$withCredentials' and credentials: '$credentials' then result should be '$expected'",
  44. ({ method, data, withCredentials, credentials, expected }) => {
  45. expect(parseInitFromOptions({ method, data, withCredentials, credentials, url: '' })).toEqual(expected);
  46. }
  47. );
  48. });
  49. describe('parseHeaders', () => {
  50. it.each`
  51. options | expected
  52. ${undefined} | ${{ map: { accept: 'application/json, text/plain, */*' } }}
  53. ${{ propKey: 'some prop value' }} | ${{ map: { accept: 'application/json, text/plain, */*' } }}
  54. ${{ method: 'GET' }} | ${{ map: { accept: 'application/json, text/plain, */*' } }}
  55. ${{ method: 'POST' }} | ${{ map: { accept: 'application/json, text/plain, */*', 'content-type': 'application/json' } }}
  56. ${{ method: 'PUT' }} | ${{ map: { accept: 'application/json, text/plain, */*', 'content-type': 'application/json' } }}
  57. ${{ headers: { 'content-type': 'application/json' } }} | ${{ map: { accept: 'application/json, text/plain, */*', 'content-type': 'application/json' } }}
  58. ${{ method: 'GET', headers: { 'content-type': 'application/json' } }} | ${{ map: { accept: 'application/json, text/plain, */*', 'content-type': 'application/json' } }}
  59. ${{ method: 'POST', headers: { 'content-type': 'application/json' } }} | ${{ map: { accept: 'application/json, text/plain, */*', 'content-type': 'application/json' } }}
  60. ${{ method: 'PUT', headers: { 'content-type': 'application/json' } }} | ${{ map: { accept: 'application/json, text/plain, */*', 'content-type': 'application/json' } }}
  61. ${{ headers: { 'cOnTent-tYpe': 'application/json' } }} | ${{ map: { accept: 'application/json, text/plain, */*', 'content-type': 'application/json' } }}
  62. ${{ headers: { 'content-type': 'AppLiCatIon/JsOn' } }} | ${{ map: { accept: 'application/json, text/plain, */*', 'content-type': 'AppLiCatIon/JsOn' } }}
  63. ${{ headers: { 'cOnTent-tYpe': 'AppLiCatIon/JsOn' } }} | ${{ map: { accept: 'application/json, text/plain, */*', 'content-type': 'AppLiCatIon/JsOn' } }}
  64. ${{ headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }} | ${{ map: { accept: 'application/json, text/plain, */*', 'content-type': 'application/x-www-form-urlencoded' } }}
  65. ${{ method: 'GET', headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }} | ${{ map: { accept: 'application/json, text/plain, */*', 'content-type': 'application/x-www-form-urlencoded' } }}
  66. ${{ method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }} | ${{ map: { accept: 'application/json, text/plain, */*', 'content-type': 'application/x-www-form-urlencoded' } }}
  67. ${{ method: 'PUT', headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }} | ${{ map: { accept: 'application/json, text/plain, */*', 'content-type': 'application/x-www-form-urlencoded' } }}
  68. ${{ headers: { Accept: 'text/plain' } }} | ${{ map: { accept: 'text/plain' } }}
  69. ${{ headers: { Auth: 'Basic asdasdasd' } }} | ${{ map: { accept: 'application/json, text/plain, */*', auth: 'Basic asdasdasd' } }}
  70. `("when called with options: '$options' then the result should be '$expected'", ({ options, expected }) => {
  71. expect(parseHeaders(options)).toEqual(expected);
  72. });
  73. });
  74. describe('isContentTypeApplicationJson', () => {
  75. it.each`
  76. headers | expected
  77. ${undefined} | ${false}
  78. ${new Headers({ 'cOnTent-tYpe': 'application/json' })} | ${true}
  79. ${new Headers({ 'content-type': 'AppLiCatIon/JsOn' })} | ${true}
  80. ${new Headers({ 'cOnTent-tYpe': 'AppLiCatIon/JsOn' })} | ${true}
  81. ${new Headers({ 'content-type': 'application/x-www-form-urlencoded' })} | ${false}
  82. ${new Headers({ auth: 'Basic akdjasdkjalksdjasd' })} | ${false}
  83. `("when called with headers: 'headers' then the result should be '$expected'", ({ headers, expected }) => {
  84. expect(isContentTypeApplicationJson(headers)).toEqual(expected);
  85. });
  86. });
  87. describe('parseBody', () => {
  88. it.each`
  89. options | isAppJson | expected
  90. ${undefined} | ${false} | ${undefined}
  91. ${undefined} | ${true} | ${undefined}
  92. ${{ data: undefined }} | ${false} | ${undefined}
  93. ${{ data: undefined }} | ${true} | ${undefined}
  94. ${{ data: 'some data' }} | ${false} | ${'some data'}
  95. ${{ data: 'some data' }} | ${true} | ${'some data'}
  96. ${{ data: { id: '0' } }} | ${false} | ${new URLSearchParams({ id: '0' })}
  97. ${{ data: { id: '0' } }} | ${true} | ${'{"id":"0"}'}
  98. `(
  99. "when called with options: '$options' and isAppJson: '$isAppJson' then the result should be '$expected'",
  100. ({ options, isAppJson, expected }) => {
  101. expect(parseBody(options, isAppJson)).toEqual(expected);
  102. }
  103. );
  104. });
  105. describe('parseCredentials', () => {
  106. it.each`
  107. options | expected
  108. ${undefined} | ${undefined}
  109. ${{}} | ${'same-origin'}
  110. ${{ credentials: undefined }} | ${'same-origin'}
  111. ${{ credentials: undefined, withCredentials: undefined }} | ${'same-origin'}
  112. ${{ credentials: undefined, withCredentials: false }} | ${'same-origin'}
  113. ${{ credentials: undefined, withCredentials: true }} | ${'include'}
  114. ${{ credentials: 'invalid' }} | ${'invalid'}
  115. ${{ credentials: 'invalid', withCredentials: undefined }} | ${'invalid'}
  116. ${{ credentials: 'invalid', withCredentials: false }} | ${'invalid'}
  117. ${{ credentials: 'invalid', withCredentials: true }} | ${'invalid'}
  118. ${{ credentials: 'omit' }} | ${'omit'}
  119. ${{ credentials: 'omit', withCredentials: undefined }} | ${'omit'}
  120. ${{ credentials: 'omit', withCredentials: false }} | ${'omit'}
  121. ${{ credentials: 'omit', withCredentials: true }} | ${'omit'}
  122. `(
  123. "when called with options: '$options' then the result should be '$expected'",
  124. ({ options, isAppJson, expected }) => {
  125. expect(parseCredentials(options)).toEqual(expected);
  126. }
  127. );
  128. });
  129. describe('parseResponseBody', () => {
  130. let rsp: Response;
  131. beforeEach(() => {
  132. rsp = new Response();
  133. });
  134. it('parses json', async () => {
  135. const value = { hello: 'world' };
  136. const body = await parseResponseBody(
  137. {
  138. ...rsp,
  139. json: jest.fn().mockImplementationOnce(() => value),
  140. },
  141. 'json'
  142. );
  143. expect(body).toEqual(value);
  144. });
  145. it('returns an empty object {} when the response is empty but is declared as JSON type', async () => {
  146. rsp.headers.set('Content-Length', '0');
  147. jest.spyOn(console, 'warn').mockImplementation();
  148. const json = jest.fn();
  149. const body = await parseResponseBody(
  150. {
  151. ...rsp,
  152. json,
  153. },
  154. 'json'
  155. );
  156. expect(body).toEqual({});
  157. expect(json).not.toHaveBeenCalled();
  158. expect(console.warn).toHaveBeenCalledTimes(1);
  159. });
  160. it('parses text', async () => {
  161. const value = 'RAW TEXT';
  162. const body = await parseResponseBody(
  163. {
  164. ...rsp,
  165. text: jest.fn().mockImplementationOnce(() => value),
  166. },
  167. 'text'
  168. );
  169. expect(body).toEqual(value);
  170. });
  171. it('undefined text', async () => {
  172. const value = 'RAW TEXT';
  173. const body = await parseResponseBody({
  174. ...rsp,
  175. text: jest.fn().mockImplementationOnce(() => value),
  176. });
  177. expect(body).toEqual(value);
  178. });
  179. it('undefined as parsed json', async () => {
  180. const value = { hello: 'world' };
  181. const body = await parseResponseBody({
  182. ...rsp,
  183. text: jest.fn().mockImplementationOnce(() => JSON.stringify(value)),
  184. });
  185. expect(body).toEqual(value);
  186. });
  187. });