import $ from 'jquery'; import {Client, Request, paramsToQueryArgs} from 'app/api'; import GroupActions from 'app/actions/groupActions'; import {PROJECT_MOVED} from 'app/constants/apiErrorCodes'; import * as Sentry from '@sentry/browser'; jest.unmock('app/api'); describe('api', function() { let api; beforeEach(function() { api = new Client(); }); describe('paramsToQueryArgs()', function() { it('should convert itemIds properties to id array', function() { expect( paramsToQueryArgs({ itemIds: [1, 2, 3], query: 'is:unresolved', // itemIds takes precedence }) ).toEqual({id: [1, 2, 3]}); }); it('should extract query property if no itemIds', function() { expect( paramsToQueryArgs({ query: 'is:unresolved', foo: 'bar', }) ).toEqual({query: 'is:unresolved'}); }); it('should convert params w/o itemIds or query to empty object', function() { expect( paramsToQueryArgs({ foo: 'bar', bar: 'baz', // paramsToQueryArgs ignores these }) ).toEqual({}); }); it('should keep environment when query is provided', function() { expect( paramsToQueryArgs({ query: 'is:unresolved', environment: 'production', }) ).toEqual({query: 'is:unresolved', environment: 'production'}); }); it('should exclude environment when it is null/undefined', function() { expect( paramsToQueryArgs({ query: 'is:unresolved', environment: null, }) ).toEqual({query: 'is:unresolved'}); }); it('should handle non-empty projects', function() { expect( paramsToQueryArgs({ itemIds: [1, 2, 3], project: [1], }) ).toEqual({id: [1, 2, 3], project: [1]}); expect( paramsToQueryArgs({ itemIds: [1, 2, 3], project: [], }) ).toEqual({id: [1, 2, 3]}); expect( paramsToQueryArgs({ itemIds: [1, 2, 3], project: null, }) ).toEqual({id: [1, 2, 3]}); }); }); describe('Client', function() { beforeEach(function() { jest.spyOn($, 'ajax'); }); describe('cancel()', function() { it('should abort any open XHR requests', function() { const req1 = new Request({ abort: jest.fn(), }); const req2 = new Request({ abort: jest.fn(), }); api.activeRequests = { 1: req1, 2: req2, }; api.clear(); expect(req1.xhr.abort).toHaveBeenCalledTimes(1); expect(req2.xhr.abort).toHaveBeenCalledTimes(1); }); }); }); it('does not call success callback if 302 was returned because of a project slug change', function() { const successCb = jest.fn(); api.activeRequests = {id: {alive: true}}; api.wrapCallback('id', successCb)({ responseJSON: { detail: { code: PROJECT_MOVED, message: '...', extra: { slug: 'new-slug', }, }, }, }); expect(successCb).not.toHaveBeenCalled(); }); it('handles error callback', function() { jest.spyOn(api, 'wrapCallback').mockImplementation((id, func) => func); const errorCb = jest.fn(); const args = ['test', true, 1]; api.handleRequestError( { id: 'test', path: 'test', requestOptions: {error: errorCb}, }, ...args ); expect(errorCb).toHaveBeenCalledWith(...args); }); it('handles undefined error callback', function() { expect(() => api.handleRequestError( { id: 'test', path: 'test', requestOptions: {}, }, {}, {} ) ).not.toThrow(); }); describe('bulkUpdate()', function() { beforeEach(function() { jest.spyOn(api, '_wrapRequest'); jest.spyOn(GroupActions, 'update'); // stub GroupActions.update call from api.update }); it('should use itemIds as query if provided', function() { api.bulkUpdate({ orgId: '1337', projectId: '1337', itemIds: [1, 2, 3], data: {status: 'unresolved'}, query: 'is:resolved', }); expect(api._wrapRequest).toHaveBeenCalledTimes(1); expect(api._wrapRequest).toHaveBeenCalledWith( '/projects/1337/1337/issues/', expect.objectContaining({query: {id: [1, 2, 3]}}), undefined ); }); it('should use query as query if itemIds are absent', function() { api.bulkUpdate({ orgId: '1337', projectId: '1337', itemIds: null, data: {status: 'unresolved'}, query: 'is:resolved', }); expect(api._wrapRequest).toHaveBeenCalledTimes(1); expect(api._wrapRequest).toHaveBeenCalledWith( '/projects/1337/1337/issues/', expect.objectContaining({query: {query: 'is:resolved'}}), undefined ); }); it('should apply project option', function() { api.bulkUpdate({ orgId: '1337', project: [99], itemIds: [1, 2, 3], data: {status: 'unresolved'}, }); expect(api._wrapRequest).toHaveBeenCalledTimes(1); expect(api._wrapRequest).toHaveBeenCalledWith( '/organizations/1337/issues/', expect.objectContaining({query: {id: [1, 2, 3], project: [99]}}), undefined ); }); }); describe('merge()', function() { // TODO: this is totally copypasta from the test above. We need to refactor // these API methods/tests. beforeEach(function() { jest.spyOn(api, '_wrapRequest'); jest.spyOn(GroupActions, 'merge'); // stub GroupActions.merge call from api.merge }); it('should use itemIds as query if provided', function() { api.merge({ orgId: '1337', projectId: '1337', itemIds: [1, 2, 3], data: {status: 'unresolved'}, query: 'is:resolved', }); expect(api._wrapRequest).toHaveBeenCalledTimes(1); expect(api._wrapRequest).toHaveBeenCalledWith( '/projects/1337/1337/issues/', expect.objectContaining({query: {id: [1, 2, 3]}}), undefined ); }); it('should use query as query if itemIds are absent', function() { api.merge({ orgId: '1337', projectId: '1337', itemIds: null, data: {status: 'unresolved'}, query: 'is:resolved', }); expect(api._wrapRequest).toHaveBeenCalledTimes(1); expect(api._wrapRequest).toHaveBeenCalledWith( '/projects/1337/1337/issues/', expect.objectContaining({query: {query: 'is:resolved'}}), undefined ); }); }); describe('Sentry reporting', function() { beforeEach(function() { jest.spyOn($, 'ajax'); $.ajax.mockReset(); Sentry.captureException.mockClear(); $.ajax.mockImplementation(async ({error}) => { await tick(); error({ status: 404, statusText: 'Not Found', responseJSON: {detail: 'Item was not found'}, }); return {}; }); }); it('reports correct error and stacktrace to Sentry', async function() { api.request('/some/url/'); await tick(); const errorObjectSentryCalled = Sentry.captureException.mock.calls[0][0]; expect(errorObjectSentryCalled.name).toBe('NotFoundError'); expect(errorObjectSentryCalled.message).toBe('GET /some/url/ 404'); // First line of stack should be this test case expect(errorObjectSentryCalled.stack.split('\n')[1]).toContain('api.spec.jsx'); }); it('reports correct error and stacktrace to Sentry when using promises', async function() { await expect( api.requestPromise('/some/url/') ).rejects.toThrowErrorMatchingInlineSnapshot('"GET /some/url/ 404"'); expect(Sentry.captureException).toHaveBeenCalled(); }); }); });