sentryAppDetailedView.spec.jsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423
  1. import {mountWithTheme} from 'sentry-test/enzyme';
  2. import {initializeOrg} from 'sentry-test/initializeOrg';
  3. import {mockRouterPush} from 'sentry-test/mockRouterPush';
  4. import {Client} from 'sentry/api';
  5. import SentryAppDetailedView from 'sentry/views/organizationIntegrations/sentryAppDetailedView';
  6. const mockResponse = mocks => {
  7. mocks.forEach(([url, body, method = 'GET']) =>
  8. Client.addMockResponse({
  9. url,
  10. body,
  11. method,
  12. })
  13. );
  14. };
  15. describe('SentryAppDetailedView', function () {
  16. const org = TestStubs.Organization();
  17. let wrapper;
  18. const {router} = initializeOrg({
  19. projects: [
  20. {isMember: true, isBookmarked: true},
  21. {isMember: true, slug: 'new-project', id: 3},
  22. ],
  23. organization: {
  24. features: ['events'],
  25. },
  26. router: {
  27. location: {
  28. pathname: '/organizations/org-slug/events/',
  29. query: {},
  30. },
  31. },
  32. });
  33. describe('Published Sentry App', function () {
  34. let createRequest;
  35. let deleteRequest;
  36. let sentryAppInteractionRequest;
  37. beforeEach(() => {
  38. Client.clearMockResponses();
  39. sentryAppInteractionRequest = MockApiClient.addMockResponse({
  40. url: `/sentry-apps/clickup/interaction/`,
  41. method: 'POST',
  42. statusCode: 200,
  43. body: {},
  44. });
  45. mockResponse([
  46. [
  47. '/sentry-apps/clickup/',
  48. {
  49. status: 'published',
  50. scopes: [],
  51. isAlertable: false,
  52. clientSecret:
  53. '193583e573d14d61832de96a9efc32ceb64e59a494284f58b50328a656420a55',
  54. overview: null,
  55. verifyInstall: false,
  56. owner: {id: 1, slug: 'sentry'},
  57. slug: 'clickup',
  58. name: 'ClickUp',
  59. uuid: '5d547ecb-7eb8-4ed2-853b-40256177d526',
  60. author: 'Nisanthan',
  61. webhookUrl: 'http://localhost:7000',
  62. clientId: 'c215db1accc040919e0b0dce058e0ecf4ea062bb82174d70aee8eba62351be24',
  63. redirectUrl: null,
  64. allowedOrigins: [],
  65. events: [],
  66. schema: {},
  67. },
  68. ],
  69. [
  70. '/sentry-apps/clickup/features/',
  71. [
  72. {
  73. featureGate: 'integrations-api',
  74. description:
  75. 'ClickUp can **utilize the Sentry API** to pull data or update resources in Sentry (with permissions granted, of course).',
  76. },
  77. ],
  78. ],
  79. [`/organizations/${org.slug}/sentry-app-installations/`, []],
  80. ]);
  81. createRequest = Client.addMockResponse({
  82. url: `/organizations/${org.slug}/sentry-app-installations/`,
  83. body: {
  84. status: 'installed',
  85. organization: {slug: `${org.slug}`},
  86. app: {uuid: '5d547ecb-7eb8-4ed2-853b-40256177d526', slug: 'clickup'},
  87. code: '1dc8b0a28b7f45959d01bbc99d9bd568',
  88. uuid: '687323fd-9fa4-4f8f-9bee-ca0089224b3e',
  89. },
  90. method: 'POST',
  91. });
  92. deleteRequest = Client.addMockResponse({
  93. url: '/sentry-app-installations/687323fd-9fa4-4f8f-9bee-ca0089224b3e/',
  94. body: {},
  95. method: 'DELETE',
  96. });
  97. wrapper = mountWithTheme(
  98. <SentryAppDetailedView
  99. params={{integrationSlug: 'clickup', orgId: org.slug}}
  100. location={{query: {}}}
  101. />
  102. );
  103. });
  104. it('records interaction request', () => {
  105. expect(sentryAppInteractionRequest).toHaveBeenCalledWith(
  106. `/sentry-apps/clickup/interaction/`,
  107. expect.objectContaining({
  108. method: 'POST',
  109. data: {
  110. tsdbField: 'sentry_app_viewed',
  111. },
  112. })
  113. );
  114. });
  115. it('shows the Integration name and install status', function () {
  116. expect(wrapper.find('Name').props().children).toEqual('ClickUp');
  117. expect(wrapper.find('IntegrationStatus').props().status).toEqual('Not Installed');
  118. });
  119. it('shows the Accept & Install button', function () {
  120. expect(wrapper.find('InstallButton').props().disabled).toEqual(false);
  121. expect(wrapper.find('InstallButton').props().children).toEqual('Accept & Install');
  122. });
  123. describe('onClick:', function () {
  124. let wrapperState;
  125. it('installs app', async function () {
  126. wrapper.find('InstallButton').simulate('click');
  127. await tick();
  128. await tick();
  129. expect(createRequest).toHaveBeenCalled();
  130. wrapper.update();
  131. wrapperState = wrapper;
  132. expect(wrapper.find('IntegrationStatus').props().status).toEqual('Installed');
  133. expect(wrapper.find('StyledUninstallButton').exists()).toEqual(true);
  134. });
  135. it('uninstalls app', async function () {
  136. expect(wrapperState.find('StyledUninstallButton')).toHaveLength(1);
  137. wrapperState.find('StyledUninstallButton').simulate('click');
  138. await tick();
  139. wrapperState.find('Confirm').props().onConfirm();
  140. await tick();
  141. expect(deleteRequest).toHaveBeenCalled();
  142. });
  143. });
  144. });
  145. describe('Internal Sentry App', function () {
  146. beforeEach(() => {
  147. Client.clearMockResponses();
  148. MockApiClient.addMockResponse({
  149. url: `/sentry-apps/my-headband-washer-289499/interaction/`,
  150. method: 'POST',
  151. statusCode: 200,
  152. body: {},
  153. });
  154. mockResponse([
  155. [
  156. '/sentry-apps/my-headband-washer-289499/',
  157. {
  158. status: 'internal',
  159. scopes: [
  160. 'project:read',
  161. 'team:read',
  162. 'team:write',
  163. 'project:releases',
  164. 'event:read',
  165. 'org:read',
  166. 'member:read',
  167. 'member:write',
  168. ],
  169. isAlertable: false,
  170. clientSecret:
  171. '8f47dcef40f7486f9bacfeca257022e092a483add7cf4d619993b9ace9775a79',
  172. overview: null,
  173. verifyInstall: false,
  174. owner: {id: 1, slug: 'sentry'},
  175. slug: 'my-headband-washer-289499',
  176. name: 'My Headband Washer',
  177. uuid: 'a806ab10-9608-4a4f-8dd9-ca6d6c09f9f5',
  178. author: 'Sentry',
  179. webhookUrl: 'https://myheadbandwasher.com',
  180. clientId: 'a6d35972d4164ef18845b1e2ca954fe70ac196e0b20d4d1e8760a38772cf6f1c',
  181. redirectUrl: null,
  182. allowedOrigins: [],
  183. events: [],
  184. schema: {},
  185. },
  186. ],
  187. [
  188. '/sentry-apps/my-headband-washer-289499/features/',
  189. [
  190. {
  191. featureGate: 'integrations-api',
  192. description:
  193. 'My Headband Washer can **utilize the Sentry API** to pull data or update resources in Sentry (with permissions granted, of course).',
  194. },
  195. ],
  196. ],
  197. [`/organizations/${org.slug}/sentry-app-installations/`, []],
  198. ]);
  199. wrapper = mountWithTheme(
  200. <SentryAppDetailedView
  201. params={{integrationSlug: 'my-headband-washer-289499', orgId: org.slug}}
  202. location={{query: {}}}
  203. router={router}
  204. />
  205. );
  206. mockRouterPush(wrapper, router);
  207. });
  208. it('should get redirected to Developer Settings', () => {
  209. expect(router.push).toHaveBeenLastCalledWith(
  210. `/settings/${org.slug}/developer-settings/my-headband-washer-289499/`
  211. );
  212. });
  213. });
  214. describe('Unpublished Sentry App without Redirect Url', function () {
  215. let createRequest;
  216. beforeEach(() => {
  217. Client.clearMockResponses();
  218. MockApiClient.addMockResponse({
  219. url: `/sentry-apps/la-croix-monitor/interaction/`,
  220. method: 'POST',
  221. statusCode: 200,
  222. body: {},
  223. });
  224. mockResponse([
  225. [
  226. '/sentry-apps/la-croix-monitor/',
  227. {
  228. status: 'unpublished',
  229. scopes: [
  230. 'project:read',
  231. 'project:write',
  232. 'team:read',
  233. 'project:releases',
  234. 'event:read',
  235. 'org:read',
  236. ],
  237. isAlertable: false,
  238. clientSecret:
  239. '2b2aeb743c3745ab832e03bf02a7d91851908d379646499f900cd115780e8b2b',
  240. overview: null,
  241. verifyInstall: false,
  242. owner: {id: 1, slug: 'sentry'},
  243. slug: 'la-croix-monitor',
  244. name: 'La Croix Monitor',
  245. uuid: 'a59c8fcc-2f27-49f8-af9e-02661fc3e8d7',
  246. author: 'La Croix',
  247. webhookUrl: 'https://lacroix.com',
  248. clientId: '8cc36458a0f94c93816e06dce7d808f882cbef59af6040d2b9ec4d67092c80f1',
  249. redirectUrl: null,
  250. allowedOrigins: [],
  251. events: [],
  252. schema: {},
  253. },
  254. ],
  255. [
  256. '/sentry-apps/la-croix-monitor/features/',
  257. [
  258. {
  259. featureGate: 'integrations-api',
  260. description:
  261. 'La Croix Monitor can **utilize the Sentry API** to pull data or update resources in Sentry (with permissions granted, of course).',
  262. },
  263. ],
  264. ],
  265. [`/organizations/${org.slug}/sentry-app-installations/`, []],
  266. ]);
  267. createRequest = Client.addMockResponse({
  268. url: `/organizations/${org.slug}/sentry-app-installations/`,
  269. body: {
  270. status: 'installed',
  271. organization: {slug: 'sentry'},
  272. app: {uuid: 'a59c8fcc-2f27-49f8-af9e-02661fc3e8d7', slug: 'la-croix-monitor'},
  273. code: '21c87231918a4e5c85d9b9e799c07382',
  274. uuid: '258ad77c-7e6c-4cfe-8a40-6171cff30d61',
  275. },
  276. method: 'POST',
  277. });
  278. wrapper = mountWithTheme(
  279. <SentryAppDetailedView
  280. params={{integrationSlug: 'la-croix-monitor', orgId: org.slug}}
  281. location={{query: {}}}
  282. />
  283. );
  284. });
  285. it('shows the Integration name and install status', function () {
  286. expect(wrapper.find('Name').props().children).toEqual('La Croix Monitor');
  287. expect(wrapper.find('IntegrationStatus').props().status).toEqual('Not Installed');
  288. });
  289. it('shows the Accept & Install button', function () {
  290. expect(wrapper.find('InstallButton').props().disabled).toEqual(false);
  291. expect(wrapper.find('InstallButton').props().children).toEqual('Accept & Install');
  292. });
  293. it('onClick: installs app', async function () {
  294. wrapper.find('InstallButton').simulate('click');
  295. await tick();
  296. expect(createRequest).toHaveBeenCalled();
  297. wrapper.update();
  298. expect(wrapper.find('IntegrationStatus').props().status).toEqual('Installed');
  299. expect(wrapper.find('StyledUninstallButton').exists()).toEqual(true);
  300. });
  301. });
  302. describe('Unpublished Sentry App with Redirect Url', function () {
  303. let createRequest;
  304. beforeEach(() => {
  305. Client.clearMockResponses();
  306. MockApiClient.addMockResponse({
  307. url: `/sentry-apps/go-to-google/interaction/`,
  308. method: 'POST',
  309. statusCode: 200,
  310. body: {},
  311. });
  312. mockResponse([
  313. [
  314. '/sentry-apps/go-to-google/',
  315. {
  316. status: 'unpublished',
  317. scopes: ['project:read', 'team:read'],
  318. isAlertable: false,
  319. clientSecret:
  320. '6405a4a7b8084cdf8dbea53b53e2163983deb428b78e4c6997bc408d44d93878',
  321. overview: null,
  322. verifyInstall: false,
  323. owner: {id: 1, slug: 'sentry'},
  324. slug: 'go-to-google',
  325. name: 'Go to Google',
  326. uuid: 'a4b8f364-4300-41ac-b8af-d8791ad50e77',
  327. author: 'Nisanthan Nanthakumar',
  328. webhookUrl: 'https://www.google.com',
  329. clientId: '0974b5df6b57480b99c2e1f238eef769ef2c27ec156d4791a26903a896d5807e',
  330. redirectUrl: 'https://www.google.com',
  331. allowedOrigins: [],
  332. events: [],
  333. schema: {},
  334. },
  335. ],
  336. [
  337. '/sentry-apps/go-to-google/features/',
  338. [
  339. {
  340. featureGate: 'integrations-api',
  341. description:
  342. 'Go to Google can **utilize the Sentry API** to pull data or update resources in Sentry (with permissions granted, of course).',
  343. },
  344. ],
  345. ],
  346. [`/organizations/${org.slug}/sentry-app-installations/`, []],
  347. ]);
  348. createRequest = Client.addMockResponse({
  349. url: `/organizations/${org.slug}/sentry-app-installations/`,
  350. body: {
  351. status: 'installed',
  352. organization: {slug: 'sentry'},
  353. app: {uuid: 'a4b8f364-4300-41ac-b8af-d8791ad50e77', slug: 'go-to-google'},
  354. code: '1f0e7c1b99b940abac7a19b86e69bbe1',
  355. uuid: '4d803538-fd42-4278-b410-492f5ab677b5',
  356. },
  357. method: 'POST',
  358. });
  359. wrapper = mountWithTheme(
  360. <SentryAppDetailedView
  361. params={{integrationSlug: 'go-to-google', orgId: org.slug}}
  362. location={{query: {}}}
  363. router={router}
  364. />
  365. );
  366. mockRouterPush(wrapper, router);
  367. });
  368. it('shows the Integration name and install status', function () {
  369. expect(wrapper.find('Name').props().children).toEqual('Go to Google');
  370. expect(wrapper.find('IntegrationStatus').props().status).toEqual('Not Installed');
  371. });
  372. it('shows the Accept & Install button', function () {
  373. expect(wrapper.find('InstallButton').props().disabled).toEqual(false);
  374. expect(wrapper.find('InstallButton').props().children).toEqual('Accept & Install');
  375. });
  376. it('onClick: redirects url', async function () {
  377. window.location.assign = jest.fn();
  378. wrapper.find('InstallButton').simulate('click');
  379. await tick();
  380. expect(createRequest).toHaveBeenCalled();
  381. wrapper.update();
  382. expect(window.location.assign).toHaveBeenLastCalledWith(
  383. 'https://www.google.com/?code=1f0e7c1b99b940abac7a19b86e69bbe1&installationId=4d803538-fd42-4278-b410-492f5ab677b5&orgSlug=org-slug'
  384. );
  385. });
  386. });
  387. });