sentryAppDetailedView.spec.tsx 13 KB

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