apiSource.spec.jsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427
  1. import {mountWithTheme} from 'sentry-test/enzyme';
  2. import {ApiSource} from 'sentry/components/search/sources/apiSource';
  3. describe('ApiSource', function () {
  4. let wrapper;
  5. const org = TestStubs.Organization();
  6. let orgsMock;
  7. let projectsMock;
  8. let teamsMock;
  9. let membersMock;
  10. let shortIdMock;
  11. let eventIdMock;
  12. let allMocks;
  13. beforeEach(function () {
  14. MockApiClient.clearMockResponses();
  15. MockApiClient.addMockResponse({
  16. url: '/organizations/',
  17. query: 'test-1',
  18. body: [TestStubs.Organization({slug: 'test-org'})],
  19. });
  20. orgsMock = MockApiClient.addMockResponse({
  21. url: '/organizations/',
  22. query: 'foo',
  23. body: [TestStubs.Organization({slug: 'foo-org'})],
  24. });
  25. projectsMock = MockApiClient.addMockResponse({
  26. url: '/organizations/org-slug/projects/',
  27. query: 'foo',
  28. body: [TestStubs.Project({slug: 'foo-project'})],
  29. });
  30. teamsMock = MockApiClient.addMockResponse({
  31. url: '/organizations/org-slug/teams/',
  32. query: 'foo',
  33. body: [TestStubs.Team({slug: 'foo-team'})],
  34. });
  35. membersMock = MockApiClient.addMockResponse({
  36. url: '/organizations/org-slug/members/',
  37. query: 'foo',
  38. body: TestStubs.Members(),
  39. });
  40. shortIdMock = MockApiClient.addMockResponse({
  41. url: '/organizations/org-slug/shortids/test-1/',
  42. query: 'TEST-1',
  43. body: TestStubs.ShortIdQueryResult(),
  44. });
  45. eventIdMock = MockApiClient.addMockResponse({
  46. url: '/organizations/org-slug/eventids/12345678901234567890123456789012/',
  47. query: '12345678901234567890123456789012',
  48. body: TestStubs.EventIdQueryResult(),
  49. });
  50. MockApiClient.addMockResponse({
  51. url: '/organizations/org-slug/plugins/?plugins=_all',
  52. query: {plugins: '_all'},
  53. body: [],
  54. });
  55. MockApiClient.addMockResponse({
  56. url: '/organizations/org-slug/plugins/configs/',
  57. body: [],
  58. });
  59. MockApiClient.addMockResponse({
  60. url: '/organizations/org-slug/config/integrations/',
  61. body: [],
  62. });
  63. MockApiClient.addMockResponse({
  64. url: '/sentry-apps/?status=published',
  65. body: [],
  66. });
  67. MockApiClient.addMockResponse({
  68. url: '/doc-integrations/',
  69. body: [],
  70. });
  71. MockApiClient.addMockResponse({
  72. url: '/organizations/org-slug/shortids/foo-t/',
  73. body: [],
  74. });
  75. allMocks = {orgsMock, projectsMock, teamsMock, membersMock, shortIdMock, eventIdMock};
  76. });
  77. it('queries all API endpoints', function () {
  78. const mock = jest.fn().mockReturnValue(null);
  79. wrapper = mountWithTheme(
  80. <ApiSource params={{orgId: org.slug}} query="foo">
  81. {mock}
  82. </ApiSource>
  83. );
  84. expect(orgsMock).toHaveBeenCalled();
  85. expect(projectsMock).toHaveBeenCalled();
  86. expect(teamsMock).toHaveBeenCalled();
  87. expect(membersMock).toHaveBeenCalled();
  88. expect(shortIdMock).not.toHaveBeenCalled();
  89. expect(eventIdMock).not.toHaveBeenCalled();
  90. });
  91. it('only queries for shortids when query matches shortid format', async function () {
  92. const mock = jest.fn().mockReturnValue(null);
  93. wrapper = mountWithTheme(
  94. <ApiSource params={{orgId: org.slug}} query="test-">
  95. {mock}
  96. </ApiSource>
  97. );
  98. await tick();
  99. expect(shortIdMock).not.toHaveBeenCalled();
  100. // Reset all mocks
  101. Object.values(allMocks).forEach(m => m.mockReset);
  102. // This is a valid short id now
  103. wrapper.setProps({query: 'test-1'});
  104. await tick();
  105. wrapper.update();
  106. expect(shortIdMock).toHaveBeenCalled();
  107. // These may not be desired behavior in future, but lets specify the expectation regardless
  108. expect(orgsMock).toHaveBeenCalled();
  109. expect(projectsMock).toHaveBeenCalled();
  110. expect(teamsMock).toHaveBeenCalled();
  111. expect(membersMock).toHaveBeenCalled();
  112. expect(eventIdMock).not.toHaveBeenCalled();
  113. expect(mock).toHaveBeenLastCalledWith(
  114. expect.objectContaining({
  115. results: [
  116. {
  117. item: expect.objectContaining({
  118. title: 'group type',
  119. description: 'group description',
  120. sourceType: 'issue',
  121. resultType: 'issue',
  122. to: '/org-slug/project-slug/issues/1/',
  123. }),
  124. score: 1,
  125. refIndex: 0,
  126. },
  127. ],
  128. })
  129. );
  130. });
  131. it('only queries for eventids when query matches eventid format of 32 chars', async function () {
  132. const mock = jest.fn().mockReturnValue(null);
  133. wrapper = mountWithTheme(
  134. <ApiSource params={{orgId: org.slug}} query="1234567890123456789012345678901">
  135. {mock}
  136. </ApiSource>
  137. );
  138. await tick();
  139. expect(eventIdMock).not.toHaveBeenCalled();
  140. // Reset all mocks
  141. Object.values(allMocks).forEach(m => m.mockReset);
  142. // This is a valid short id now
  143. wrapper.setProps({query: '12345678901234567890123456789012'});
  144. wrapper.update();
  145. await tick();
  146. expect(eventIdMock).toHaveBeenCalled();
  147. // These may not be desired behavior in future, but lets specify the expectation regardless
  148. expect(orgsMock).toHaveBeenCalled();
  149. expect(projectsMock).toHaveBeenCalled();
  150. expect(teamsMock).toHaveBeenCalled();
  151. expect(membersMock).toHaveBeenCalled();
  152. expect(shortIdMock).not.toHaveBeenCalled();
  153. expect(mock).toHaveBeenLastCalledWith(
  154. expect.objectContaining({
  155. results: [
  156. {
  157. item: expect.objectContaining({
  158. title: 'event type',
  159. description: 'event description',
  160. sourceType: 'event',
  161. resultType: 'event',
  162. to: '/org-slug/project-slug/issues/1/events/12345678901234567890123456789012/',
  163. }),
  164. score: 1,
  165. refIndex: 0,
  166. },
  167. ],
  168. })
  169. );
  170. });
  171. it('only queries org endpoint if there is no org in context', function () {
  172. const mock = jest.fn().mockReturnValue(null);
  173. wrapper = mountWithTheme(
  174. <ApiSource params={{}} query="foo">
  175. {mock}
  176. </ApiSource>
  177. );
  178. expect(orgsMock).toHaveBeenCalled();
  179. expect(projectsMock).not.toHaveBeenCalled();
  180. expect(teamsMock).not.toHaveBeenCalled();
  181. expect(membersMock).not.toHaveBeenCalled();
  182. });
  183. it('render function is called with correct results', async function () {
  184. const mock = jest.fn().mockReturnValue(null);
  185. wrapper = mountWithTheme(
  186. <ApiSource params={{orgId: org.slug}} organization={org} query="foo">
  187. {mock}
  188. </ApiSource>
  189. );
  190. await tick();
  191. wrapper.update();
  192. expect(mock).toHaveBeenLastCalledWith({
  193. isLoading: false,
  194. results: expect.arrayContaining([
  195. expect.objectContaining({
  196. item: expect.objectContaining({
  197. model: expect.objectContaining({
  198. slug: 'foo-org',
  199. }),
  200. sourceType: 'organization',
  201. resultType: 'settings',
  202. to: '/settings/foo-org/',
  203. }),
  204. matches: expect.anything(),
  205. score: expect.anything(),
  206. }),
  207. expect.objectContaining({
  208. item: expect.objectContaining({
  209. model: expect.objectContaining({
  210. slug: 'foo-org',
  211. }),
  212. sourceType: 'organization',
  213. resultType: 'route',
  214. to: '/foo-org/',
  215. }),
  216. matches: expect.anything(),
  217. score: expect.anything(),
  218. }),
  219. expect.objectContaining({
  220. item: expect.objectContaining({
  221. model: expect.objectContaining({
  222. slug: 'foo-project',
  223. }),
  224. sourceType: 'project',
  225. resultType: 'route',
  226. to: '/organizations/org-slug/projects/foo-project/?project=2',
  227. }),
  228. matches: expect.anything(),
  229. score: expect.anything(),
  230. }),
  231. expect.objectContaining({
  232. item: expect.objectContaining({
  233. model: expect.objectContaining({
  234. slug: 'foo-project',
  235. }),
  236. sourceType: 'project',
  237. resultType: 'route',
  238. to: '/organizations/org-slug/alerts/rules/?project=2',
  239. }),
  240. matches: expect.anything(),
  241. score: expect.anything(),
  242. }),
  243. expect.objectContaining({
  244. item: expect.objectContaining({
  245. model: expect.objectContaining({
  246. slug: 'foo-project',
  247. }),
  248. sourceType: 'project',
  249. resultType: 'settings',
  250. to: '/settings/org-slug/projects/foo-project/',
  251. }),
  252. matches: expect.anything(),
  253. score: expect.anything(),
  254. }),
  255. expect.objectContaining({
  256. item: expect.objectContaining({
  257. model: expect.objectContaining({
  258. slug: 'foo-team',
  259. }),
  260. sourceType: 'team',
  261. resultType: 'settings',
  262. to: '/settings/org-slug/teams/foo-team/',
  263. }),
  264. matches: expect.anything(),
  265. score: expect.anything(),
  266. }),
  267. ]),
  268. });
  269. // The return values here are because of fuzzy search matching.
  270. // There are no members that match
  271. expect(mock.mock.calls[1][0].results).toHaveLength(6);
  272. });
  273. it('render function is called with correct results when API requests partially succeed', async function () {
  274. const mock = jest.fn().mockReturnValue(null);
  275. MockApiClient.addMockResponse({
  276. url: '/organizations/org-slug/projects/',
  277. query: 'foo',
  278. statusCode: 500,
  279. });
  280. wrapper = mountWithTheme(
  281. <ApiSource params={{orgId: org.slug}} query="foo">
  282. {mock}
  283. </ApiSource>
  284. );
  285. await tick();
  286. wrapper.update();
  287. expect(mock).toHaveBeenLastCalledWith({
  288. isLoading: false,
  289. results: expect.arrayContaining([
  290. expect.objectContaining({
  291. item: expect.objectContaining({
  292. model: expect.objectContaining({
  293. slug: 'foo-org',
  294. }),
  295. }),
  296. }),
  297. expect.objectContaining({
  298. item: expect.objectContaining({
  299. model: expect.objectContaining({
  300. slug: 'foo-org',
  301. }),
  302. }),
  303. }),
  304. expect.objectContaining({
  305. item: expect.objectContaining({
  306. model: expect.objectContaining({
  307. slug: 'foo-team',
  308. }),
  309. }),
  310. }),
  311. ]),
  312. });
  313. // The return values here are because of fuzzy search matching.
  314. // There are no members that match
  315. expect(mock.mock.calls[1][0].results).toHaveLength(3);
  316. });
  317. it('render function is updated as query changes', async function () {
  318. const mock = jest.fn().mockReturnValue(null);
  319. wrapper = mountWithTheme(
  320. <ApiSource params={{orgId: org.slug}} query="foo">
  321. {mock}
  322. </ApiSource>
  323. );
  324. await tick();
  325. wrapper.update();
  326. // The return values here are because of fuzzy search matching.
  327. // There are no members that match
  328. expect(mock.mock.calls[1][0].results).toHaveLength(6);
  329. expect(mock.mock.calls[1][0].results[0].item.model.slug).toBe('foo-org');
  330. mock.mockClear();
  331. wrapper.setProps({query: 'foo-t'});
  332. await tick();
  333. wrapper.update();
  334. // Still have 4 results, but is re-ordered
  335. expect(mock.mock.calls[0][0].results).toHaveLength(6);
  336. expect(mock.mock.calls[0][0].results[0].item.model.slug).toBe('foo-team');
  337. });
  338. describe('API queries', function () {
  339. let mock;
  340. beforeAll(function () {
  341. mock = jest.fn().mockReturnValue(null);
  342. wrapper = mountWithTheme(
  343. <ApiSource params={{orgId: org.slug}} query="">
  344. {mock}
  345. </ApiSource>
  346. );
  347. });
  348. it('does not call API with empty query string', function () {
  349. expect(projectsMock).not.toHaveBeenCalled();
  350. });
  351. it('calls API when query string length is 1 char', function () {
  352. wrapper.setProps({query: 'f'});
  353. wrapper.update();
  354. expect(projectsMock).toHaveBeenCalledTimes(1);
  355. });
  356. it('calls API when query string length increases from 1 -> 2', function () {
  357. wrapper.setProps({query: 'fo'});
  358. wrapper.update();
  359. expect(projectsMock).toHaveBeenCalledTimes(1);
  360. });
  361. it('does not query API when query string > 2 chars', function () {
  362. // Should not query API when query is > 2 chars
  363. wrapper.setProps({query: 'foo'});
  364. wrapper.update();
  365. expect(projectsMock).toHaveBeenCalledTimes(0);
  366. });
  367. it('does not query API when query string 3 -> 4 chars', function () {
  368. wrapper.setProps({query: 'foob'});
  369. wrapper.update();
  370. expect(projectsMock).toHaveBeenCalledTimes(0);
  371. });
  372. it('re-queries API if first 2 characters are different', function () {
  373. wrapper.setProps({query: 'ba'});
  374. wrapper.update();
  375. expect(projectsMock).toHaveBeenCalledTimes(1);
  376. });
  377. it('does not requery if query string is the same', function () {
  378. wrapper.setProps({query: 'ba'});
  379. wrapper.update();
  380. expect(projectsMock).toHaveBeenCalledTimes(0);
  381. });
  382. it('queries if we go from 2 chars -> 1 char', function () {
  383. wrapper.setProps({query: 'b'});
  384. wrapper.update();
  385. expect(projectsMock).toHaveBeenCalledTimes(1);
  386. });
  387. });
  388. });