sentryAppDetailedView.spec.tsx 13 KB

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