api.spec.jsx 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  1. import $ from 'jquery';
  2. import {Client, Request, paramsToQueryArgs} from 'app/api';
  3. import GroupActions from 'app/actions/groupActions';
  4. import {PROJECT_MOVED} from 'app/constants/apiErrorCodes';
  5. import * as Sentry from '@sentry/browser';
  6. jest.unmock('app/api');
  7. describe('api', function() {
  8. let api;
  9. beforeEach(function() {
  10. api = new Client();
  11. });
  12. describe('paramsToQueryArgs()', function() {
  13. it('should convert itemIds properties to id array', function() {
  14. expect(
  15. paramsToQueryArgs({
  16. itemIds: [1, 2, 3],
  17. query: 'is:unresolved', // itemIds takes precedence
  18. })
  19. ).toEqual({id: [1, 2, 3]});
  20. });
  21. it('should extract query property if no itemIds', function() {
  22. expect(
  23. paramsToQueryArgs({
  24. query: 'is:unresolved',
  25. foo: 'bar',
  26. })
  27. ).toEqual({query: 'is:unresolved'});
  28. });
  29. it('should convert params w/o itemIds or query to empty object', function() {
  30. expect(
  31. paramsToQueryArgs({
  32. foo: 'bar',
  33. bar: 'baz', // paramsToQueryArgs ignores these
  34. })
  35. ).toEqual({});
  36. });
  37. it('should keep environment when query is provided', function() {
  38. expect(
  39. paramsToQueryArgs({
  40. query: 'is:unresolved',
  41. environment: 'production',
  42. })
  43. ).toEqual({query: 'is:unresolved', environment: 'production'});
  44. });
  45. it('should exclude environment when it is null/undefined', function() {
  46. expect(
  47. paramsToQueryArgs({
  48. query: 'is:unresolved',
  49. environment: null,
  50. })
  51. ).toEqual({query: 'is:unresolved'});
  52. });
  53. it('should handle non-empty projects', function() {
  54. expect(
  55. paramsToQueryArgs({
  56. itemIds: [1, 2, 3],
  57. project: [1],
  58. })
  59. ).toEqual({id: [1, 2, 3], project: [1]});
  60. expect(
  61. paramsToQueryArgs({
  62. itemIds: [1, 2, 3],
  63. project: [],
  64. })
  65. ).toEqual({id: [1, 2, 3]});
  66. expect(
  67. paramsToQueryArgs({
  68. itemIds: [1, 2, 3],
  69. project: null,
  70. })
  71. ).toEqual({id: [1, 2, 3]});
  72. });
  73. });
  74. describe('Client', function() {
  75. beforeEach(function() {
  76. jest.spyOn($, 'ajax');
  77. });
  78. describe('cancel()', function() {
  79. it('should abort any open XHR requests', function() {
  80. const req1 = new Request({
  81. abort: jest.fn(),
  82. });
  83. const req2 = new Request({
  84. abort: jest.fn(),
  85. });
  86. api.activeRequests = {
  87. 1: req1,
  88. 2: req2,
  89. };
  90. api.clear();
  91. expect(req1.xhr.abort).toHaveBeenCalledTimes(1);
  92. expect(req2.xhr.abort).toHaveBeenCalledTimes(1);
  93. });
  94. });
  95. });
  96. it('does not call success callback if 302 was returned because of a project slug change', function() {
  97. const successCb = jest.fn();
  98. api.activeRequests = {id: {alive: true}};
  99. api.wrapCallback('id', successCb)({
  100. responseJSON: {
  101. detail: {
  102. code: PROJECT_MOVED,
  103. message: '...',
  104. extra: {
  105. slug: 'new-slug',
  106. },
  107. },
  108. },
  109. });
  110. expect(successCb).not.toHaveBeenCalled();
  111. });
  112. it('handles error callback', function() {
  113. jest.spyOn(api, 'wrapCallback').mockImplementation((id, func) => func);
  114. const errorCb = jest.fn();
  115. const args = ['test', true, 1];
  116. api.handleRequestError(
  117. {
  118. id: 'test',
  119. path: 'test',
  120. requestOptions: {error: errorCb},
  121. },
  122. ...args
  123. );
  124. expect(errorCb).toHaveBeenCalledWith(...args);
  125. });
  126. it('handles undefined error callback', function() {
  127. expect(() =>
  128. api.handleRequestError(
  129. {
  130. id: 'test',
  131. path: 'test',
  132. requestOptions: {},
  133. },
  134. {},
  135. {}
  136. )
  137. ).not.toThrow();
  138. });
  139. describe('bulkUpdate()', function() {
  140. beforeEach(function() {
  141. jest.spyOn(api, '_wrapRequest');
  142. jest.spyOn(GroupActions, 'update'); // stub GroupActions.update call from api.update
  143. });
  144. it('should use itemIds as query if provided', function() {
  145. api.bulkUpdate({
  146. orgId: '1337',
  147. projectId: '1337',
  148. itemIds: [1, 2, 3],
  149. data: {status: 'unresolved'},
  150. query: 'is:resolved',
  151. });
  152. expect(api._wrapRequest).toHaveBeenCalledTimes(1);
  153. expect(api._wrapRequest).toHaveBeenCalledWith(
  154. '/projects/1337/1337/issues/',
  155. expect.objectContaining({query: {id: [1, 2, 3]}}),
  156. undefined
  157. );
  158. });
  159. it('should use query as query if itemIds are absent', function() {
  160. api.bulkUpdate({
  161. orgId: '1337',
  162. projectId: '1337',
  163. itemIds: null,
  164. data: {status: 'unresolved'},
  165. query: 'is:resolved',
  166. });
  167. expect(api._wrapRequest).toHaveBeenCalledTimes(1);
  168. expect(api._wrapRequest).toHaveBeenCalledWith(
  169. '/projects/1337/1337/issues/',
  170. expect.objectContaining({query: {query: 'is:resolved'}}),
  171. undefined
  172. );
  173. });
  174. it('should apply project option', function() {
  175. api.bulkUpdate({
  176. orgId: '1337',
  177. project: [99],
  178. itemIds: [1, 2, 3],
  179. data: {status: 'unresolved'},
  180. });
  181. expect(api._wrapRequest).toHaveBeenCalledTimes(1);
  182. expect(api._wrapRequest).toHaveBeenCalledWith(
  183. '/organizations/1337/issues/',
  184. expect.objectContaining({query: {id: [1, 2, 3], project: [99]}}),
  185. undefined
  186. );
  187. });
  188. });
  189. describe('merge()', function() {
  190. // TODO: this is totally copypasta from the test above. We need to refactor
  191. // these API methods/tests.
  192. beforeEach(function() {
  193. jest.spyOn(api, '_wrapRequest');
  194. jest.spyOn(GroupActions, 'merge'); // stub GroupActions.merge call from api.merge
  195. });
  196. it('should use itemIds as query if provided', function() {
  197. api.merge({
  198. orgId: '1337',
  199. projectId: '1337',
  200. itemIds: [1, 2, 3],
  201. data: {status: 'unresolved'},
  202. query: 'is:resolved',
  203. });
  204. expect(api._wrapRequest).toHaveBeenCalledTimes(1);
  205. expect(api._wrapRequest).toHaveBeenCalledWith(
  206. '/projects/1337/1337/issues/',
  207. expect.objectContaining({query: {id: [1, 2, 3]}}),
  208. undefined
  209. );
  210. });
  211. it('should use query as query if itemIds are absent', function() {
  212. api.merge({
  213. orgId: '1337',
  214. projectId: '1337',
  215. itemIds: null,
  216. data: {status: 'unresolved'},
  217. query: 'is:resolved',
  218. });
  219. expect(api._wrapRequest).toHaveBeenCalledTimes(1);
  220. expect(api._wrapRequest).toHaveBeenCalledWith(
  221. '/projects/1337/1337/issues/',
  222. expect.objectContaining({query: {query: 'is:resolved'}}),
  223. undefined
  224. );
  225. });
  226. });
  227. describe('Sentry reporting', function() {
  228. beforeEach(function() {
  229. jest.spyOn($, 'ajax');
  230. $.ajax.mockReset();
  231. Sentry.captureException.mockClear();
  232. $.ajax.mockImplementation(async ({error}) => {
  233. await tick();
  234. error({
  235. status: 404,
  236. statusText: 'Not Found',
  237. responseJSON: {detail: 'Item was not found'},
  238. });
  239. return {};
  240. });
  241. });
  242. it('reports correct error and stacktrace to Sentry', async function() {
  243. api.request('/some/url/');
  244. await tick();
  245. const errorObjectSentryCalled = Sentry.captureException.mock.calls[0][0];
  246. expect(errorObjectSentryCalled.name).toBe('NotFoundError');
  247. expect(errorObjectSentryCalled.message).toBe('GET /some/url/ 404');
  248. // First line of stack should be this test case
  249. expect(errorObjectSentryCalled.stack.split('\n')[1]).toContain('api.spec.jsx');
  250. });
  251. it('reports correct error and stacktrace to Sentry when using promises', async function() {
  252. await expect(
  253. api.requestPromise('/some/url/')
  254. ).rejects.toThrowErrorMatchingInlineSnapshot('"GET /some/url/ 404"');
  255. expect(Sentry.captureException).toHaveBeenCalled();
  256. });
  257. });
  258. });