sentryAppDetailedView.spec.tsx 13 KB

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