api.spec.jsx 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  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 undefined', function() {
  30. expect(
  31. paramsToQueryArgs({
  32. foo: 'bar',
  33. bar: 'baz', // paramsToQueryArgs ignores these
  34. })
  35. ).toBeUndefined();
  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. });
  54. describe('Client', function() {
  55. beforeEach(function() {
  56. jest.spyOn($, 'ajax');
  57. });
  58. describe('cancel()', function() {
  59. it('should abort any open XHR requests', function() {
  60. const req1 = new Request({
  61. abort: jest.fn(),
  62. });
  63. const req2 = new Request({
  64. abort: jest.fn(),
  65. });
  66. api.activeRequests = {
  67. 1: req1,
  68. 2: req2,
  69. };
  70. api.clear();
  71. expect(req1.xhr.abort).toHaveBeenCalledTimes(1);
  72. expect(req2.xhr.abort).toHaveBeenCalledTimes(1);
  73. });
  74. });
  75. });
  76. it('does not call success callback if 302 was returned because of a project slug change', function() {
  77. const successCb = jest.fn();
  78. api.activeRequests = {id: {alive: true}};
  79. api.wrapCallback('id', successCb)({
  80. responseJSON: {
  81. detail: {
  82. code: PROJECT_MOVED,
  83. message: '...',
  84. extra: {
  85. slug: 'new-slug',
  86. },
  87. },
  88. },
  89. });
  90. expect(successCb).not.toHaveBeenCalled();
  91. });
  92. it('handles error callback', function() {
  93. jest.spyOn(api, 'wrapCallback').mockImplementation((id, func) => func);
  94. const errorCb = jest.fn();
  95. const args = ['test', true, 1];
  96. api.handleRequestError(
  97. {
  98. id: 'test',
  99. path: 'test',
  100. requestOptions: {error: errorCb},
  101. },
  102. ...args
  103. );
  104. expect(errorCb).toHaveBeenCalledWith(...args);
  105. });
  106. it('handles undefined error callback', function() {
  107. expect(() =>
  108. api.handleRequestError(
  109. {
  110. id: 'test',
  111. path: 'test',
  112. requestOptions: {},
  113. },
  114. {},
  115. {}
  116. )
  117. ).not.toThrow();
  118. });
  119. describe('bulkUpdate()', function() {
  120. beforeEach(function() {
  121. jest.spyOn(api, '_wrapRequest');
  122. jest.spyOn(GroupActions, 'update'); // stub GroupActions.update call from api.update
  123. });
  124. it('should use itemIds as query if provided', function() {
  125. api.bulkUpdate({
  126. orgId: '1337',
  127. projectId: '1337',
  128. itemIds: [1, 2, 3],
  129. data: {status: 'unresolved'},
  130. query: 'is:resolved',
  131. });
  132. expect(api._wrapRequest).toHaveBeenCalledTimes(1);
  133. expect(api._wrapRequest).toHaveBeenCalledWith(
  134. '/projects/1337/1337/issues/',
  135. expect.objectContaining({query: {id: [1, 2, 3]}}),
  136. undefined
  137. );
  138. });
  139. it('should use query as query if itemIds are absent', function() {
  140. api.bulkUpdate({
  141. orgId: '1337',
  142. projectId: '1337',
  143. itemIds: null,
  144. data: {status: 'unresolved'},
  145. query: 'is:resolved',
  146. });
  147. expect(api._wrapRequest).toHaveBeenCalledTimes(1);
  148. expect(api._wrapRequest).toHaveBeenCalledWith(
  149. '/projects/1337/1337/issues/',
  150. expect.objectContaining({query: {query: 'is:resolved'}}),
  151. undefined
  152. );
  153. });
  154. });
  155. describe('merge()', function() {
  156. // TODO: this is totally copypasta from the test above. We need to refactor
  157. // these API methods/tests.
  158. beforeEach(function() {
  159. jest.spyOn(api, '_wrapRequest');
  160. jest.spyOn(GroupActions, 'merge'); // stub GroupActions.merge call from api.merge
  161. });
  162. it('should use itemIds as query if provided', function() {
  163. api.merge({
  164. orgId: '1337',
  165. projectId: '1337',
  166. itemIds: [1, 2, 3],
  167. data: {status: 'unresolved'},
  168. query: 'is:resolved',
  169. });
  170. expect(api._wrapRequest).toHaveBeenCalledTimes(1);
  171. expect(api._wrapRequest).toHaveBeenCalledWith(
  172. '/projects/1337/1337/issues/',
  173. expect.objectContaining({query: {id: [1, 2, 3]}}),
  174. undefined
  175. );
  176. });
  177. it('should use query as query if itemIds are absent', function() {
  178. api.merge({
  179. orgId: '1337',
  180. projectId: '1337',
  181. itemIds: null,
  182. data: {status: 'unresolved'},
  183. query: 'is:resolved',
  184. });
  185. expect(api._wrapRequest).toHaveBeenCalledTimes(1);
  186. expect(api._wrapRequest).toHaveBeenCalledWith(
  187. '/projects/1337/1337/issues/',
  188. expect.objectContaining({query: {query: 'is:resolved'}}),
  189. undefined
  190. );
  191. });
  192. });
  193. describe('Sentry reporting', function() {
  194. beforeEach(function() {
  195. jest.spyOn($, 'ajax');
  196. $.ajax.mockReset();
  197. Sentry.captureException.mockClear();
  198. $.ajax.mockImplementation(async ({error}) => {
  199. await tick();
  200. error({
  201. status: 404,
  202. statusText: 'Not Found',
  203. responseJSON: {detail: 'Item was not found'},
  204. });
  205. return {};
  206. });
  207. });
  208. it('reports correct error and stacktrace to Sentry', async function() {
  209. api.request('/some/url/');
  210. await tick();
  211. const errorObjectSentryCalled = Sentry.captureException.mock.calls[0][0];
  212. expect(errorObjectSentryCalled.name).toBe('NotFoundError');
  213. expect(errorObjectSentryCalled.message).toBe('GET /some/url/ 404');
  214. // First line of stack should be this test case
  215. expect(errorObjectSentryCalled.stack.split('\n')[1]).toContain('api.spec.jsx');
  216. });
  217. it('reports correct error and stacktrace to Sentry when using promises', async function() {
  218. await expect(
  219. api.requestPromise('/some/url/')
  220. ).rejects.toThrowErrorMatchingInlineSnapshot('"GET /some/url/ 404"');
  221. expect(Sentry.captureException).toHaveBeenCalled();
  222. });
  223. });
  224. });