sentryApplicationDetails.spec.tsx 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583
  1. import selectEvent from 'react-select-event';
  2. import {Organization} from 'sentry-fixture/organization';
  3. import {SentryApp} from 'sentry-fixture/sentryApp';
  4. import {SentryAppToken} from 'sentry-fixture/sentryAppToken';
  5. import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary';
  6. import SentryApplicationDetails from 'sentry/views/settings/organizationDeveloperSettings/sentryApplicationDetails';
  7. describe('Sentry Application Details', function () {
  8. let org;
  9. let sentryApp;
  10. let token;
  11. let createAppRequest;
  12. let editAppRequest;
  13. const maskedValue = '*'.repeat(64);
  14. const router = TestStubs.router();
  15. beforeEach(() => {
  16. MockApiClient.clearMockResponses();
  17. org = Organization({features: ['sentry-app-logo-upload']});
  18. });
  19. describe('Creating a new public Sentry App', () => {
  20. function renderComponent() {
  21. return render(
  22. <SentryApplicationDetails
  23. router={router}
  24. location={router.location}
  25. routes={router.routes}
  26. routeParams={{}}
  27. route={{path: 'new-public/'}}
  28. params={{}}
  29. />,
  30. {context: TestStubs.routerContext([{organization: org}])}
  31. );
  32. }
  33. beforeEach(() => {
  34. createAppRequest = MockApiClient.addMockResponse({
  35. url: '/sentry-apps/',
  36. method: 'POST',
  37. body: [],
  38. });
  39. });
  40. it('has inputs for redirectUrl and verifyInstall', () => {
  41. renderComponent();
  42. expect(
  43. screen.getByRole('checkbox', {name: 'Verify Installation'})
  44. ).toBeInTheDocument();
  45. expect(screen.getByRole('textbox', {name: 'Redirect URL'})).toBeInTheDocument();
  46. });
  47. it('shows empty scopes and no credentials', function () {
  48. renderComponent();
  49. expect(screen.getByText('Permissions')).toBeInTheDocument();
  50. // new app starts off with no scopes selected
  51. expect(screen.getByRole('checkbox', {name: 'issue'})).not.toBeChecked();
  52. expect(screen.getByRole('checkbox', {name: 'error'})).not.toBeChecked();
  53. expect(screen.getByRole('checkbox', {name: 'comment'})).not.toBeChecked();
  54. });
  55. it('does not show logo upload fields', function () {
  56. renderComponent();
  57. expect(screen.queryByText('Logo')).not.toBeInTheDocument();
  58. expect(screen.queryByText('Small Icon')).not.toBeInTheDocument();
  59. });
  60. it('saves', async function () {
  61. renderComponent();
  62. await userEvent.type(screen.getByRole('textbox', {name: 'Name'}), 'Test App');
  63. await userEvent.type(screen.getByRole('textbox', {name: 'Author'}), 'Sentry');
  64. await userEvent.type(
  65. screen.getByRole('textbox', {name: 'Webhook URL'}),
  66. 'https://webhook.com'
  67. );
  68. await userEvent.type(
  69. screen.getByRole('textbox', {name: 'Redirect URL'}),
  70. 'https://webhook.com/setup'
  71. );
  72. await userEvent.click(screen.getByRole('textbox', {name: 'Schema'}));
  73. await userEvent.paste('{}');
  74. await userEvent.click(screen.getByRole('checkbox', {name: 'Alert Rule Action'}));
  75. await selectEvent.select(screen.getByRole('textbox', {name: 'Member'}), 'Admin');
  76. await selectEvent.select(
  77. screen.getByRole('textbox', {name: 'Issue & Event'}),
  78. 'Admin'
  79. );
  80. await userEvent.click(screen.getByRole('checkbox', {name: 'issue'}));
  81. await userEvent.click(screen.getByRole('button', {name: 'Save Changes'}));
  82. const data = {
  83. name: 'Test App',
  84. author: 'Sentry',
  85. organization: org.slug,
  86. redirectUrl: 'https://webhook.com/setup',
  87. webhookUrl: 'https://webhook.com',
  88. scopes: expect.arrayContaining([
  89. 'member:read',
  90. 'member:admin',
  91. 'event:read',
  92. 'event:admin',
  93. ]),
  94. events: ['issue'],
  95. isInternal: false,
  96. verifyInstall: true,
  97. isAlertable: true,
  98. allowedOrigins: [],
  99. schema: {},
  100. };
  101. expect(createAppRequest).toHaveBeenCalledWith(
  102. '/sentry-apps/',
  103. expect.objectContaining({
  104. data,
  105. method: 'POST',
  106. })
  107. );
  108. });
  109. });
  110. describe('Creating a new internal Sentry App', () => {
  111. function renderComponent() {
  112. return render(
  113. <SentryApplicationDetails
  114. router={router}
  115. location={router.location}
  116. routes={router.routes}
  117. routeParams={{}}
  118. route={{path: 'new-internal/'}}
  119. params={{}}
  120. />,
  121. {context: TestStubs.routerContext([{organization: org}])}
  122. );
  123. }
  124. it('does not show logo upload fields', function () {
  125. renderComponent();
  126. expect(screen.queryByText('Logo')).not.toBeInTheDocument();
  127. expect(screen.queryByText('Small Icon')).not.toBeInTheDocument();
  128. });
  129. it('no inputs for redirectUrl and verifyInstall', () => {
  130. renderComponent();
  131. expect(
  132. screen.queryByRole('checkbox', {name: 'Verify Installation'})
  133. ).not.toBeInTheDocument();
  134. expect(
  135. screen.queryByRole('textbox', {name: 'Redirect URL'})
  136. ).not.toBeInTheDocument();
  137. });
  138. });
  139. describe('Renders public app', function () {
  140. function renderComponent() {
  141. return render(
  142. <SentryApplicationDetails
  143. router={router}
  144. location={router.location}
  145. routes={router.routes}
  146. routeParams={{}}
  147. route={router.routes[0]}
  148. params={{appSlug: sentryApp.slug}}
  149. />,
  150. {
  151. context: TestStubs.routerContext([{organization: org}]),
  152. }
  153. );
  154. }
  155. beforeEach(() => {
  156. sentryApp = SentryApp();
  157. sentryApp.events = ['issue'];
  158. MockApiClient.addMockResponse({
  159. url: `/sentry-apps/${sentryApp.slug}/`,
  160. body: sentryApp,
  161. });
  162. MockApiClient.addMockResponse({
  163. url: `/sentry-apps/${sentryApp.slug}/api-tokens/`,
  164. body: [],
  165. });
  166. });
  167. it('shows logo upload fields', function () {
  168. renderComponent();
  169. expect(screen.getByText('Logo')).toBeInTheDocument();
  170. expect(screen.getByText('Small Icon')).toBeInTheDocument();
  171. });
  172. it('has inputs for redirectUrl and verifyInstall', () => {
  173. renderComponent();
  174. expect(
  175. screen.getByRole('checkbox', {name: 'Verify Installation'})
  176. ).toBeInTheDocument();
  177. expect(screen.getByRole('textbox', {name: 'Redirect URL'})).toBeInTheDocument();
  178. });
  179. it('shows application data', function () {
  180. renderComponent();
  181. selectEvent.openMenu(screen.getByRole('textbox', {name: 'Project'}));
  182. expect(screen.getByRole('menuitemradio', {name: 'Read'})).toBeChecked();
  183. });
  184. it('renders clientId and clientSecret for public apps', function () {
  185. renderComponent();
  186. expect(screen.getByRole('textbox', {name: 'Client ID'})).toBeInTheDocument();
  187. expect(screen.getByRole('textbox', {name: 'Client Secret'})).toBeInTheDocument();
  188. });
  189. });
  190. describe('Renders for internal apps', () => {
  191. function renderComponent() {
  192. return render(
  193. <SentryApplicationDetails
  194. router={router}
  195. location={router.location}
  196. routes={router.routes}
  197. routeParams={{}}
  198. route={router.routes[0]}
  199. params={{appSlug: sentryApp.slug}}
  200. />,
  201. {
  202. context: TestStubs.routerContext([{organization: org}]),
  203. }
  204. );
  205. }
  206. beforeEach(() => {
  207. sentryApp = SentryApp({
  208. status: 'internal',
  209. });
  210. token = SentryAppToken();
  211. sentryApp.events = ['issue'];
  212. MockApiClient.addMockResponse({
  213. url: `/sentry-apps/${sentryApp.slug}/`,
  214. body: sentryApp,
  215. });
  216. MockApiClient.addMockResponse({
  217. url: `/sentry-apps/${sentryApp.slug}/api-tokens/`,
  218. body: [token],
  219. });
  220. });
  221. it('no inputs for redirectUrl and verifyInstall', () => {
  222. renderComponent();
  223. expect(
  224. screen.queryByRole('checkbox', {name: 'Verify Installation'})
  225. ).not.toBeInTheDocument();
  226. expect(
  227. screen.queryByRole('textbox', {name: 'Redirect URL'})
  228. ).not.toBeInTheDocument();
  229. });
  230. it('shows logo upload fields', function () {
  231. renderComponent();
  232. expect(screen.getByText('Logo')).toBeInTheDocument();
  233. expect(screen.getByText('Small Icon')).toBeInTheDocument();
  234. });
  235. it('shows tokens', function () {
  236. renderComponent();
  237. expect(screen.getByText('Tokens')).toBeInTheDocument();
  238. expect(screen.getByRole('textbox', {name: 'Token value'})).toHaveValue(
  239. '123456123456123456123456-token'
  240. );
  241. });
  242. it('shows just clientSecret', function () {
  243. renderComponent();
  244. expect(screen.queryByRole('textbox', {name: 'Client ID'})).not.toBeInTheDocument();
  245. expect(screen.getByRole('textbox', {name: 'Client Secret'})).toBeInTheDocument();
  246. });
  247. });
  248. describe('Renders masked values', () => {
  249. function renderComponent() {
  250. return render(
  251. <SentryApplicationDetails
  252. router={router}
  253. location={router.location}
  254. routes={router.routes}
  255. routeParams={{}}
  256. route={router.routes[0]}
  257. params={{appSlug: sentryApp.slug}}
  258. />,
  259. {
  260. context: TestStubs.routerContext([{organization: org}]),
  261. }
  262. );
  263. }
  264. beforeEach(() => {
  265. sentryApp = SentryApp({
  266. status: 'internal',
  267. clientSecret: maskedValue,
  268. });
  269. token = SentryAppToken({token: maskedValue, refreshToken: maskedValue});
  270. sentryApp.events = ['issue'];
  271. MockApiClient.addMockResponse({
  272. url: `/sentry-apps/${sentryApp.slug}/`,
  273. body: sentryApp,
  274. });
  275. MockApiClient.addMockResponse({
  276. url: `/sentry-apps/${sentryApp.slug}/api-tokens/`,
  277. body: [token],
  278. });
  279. });
  280. it('shows masked tokens', function () {
  281. renderComponent();
  282. expect(screen.getByRole('textbox', {name: 'Token value'})).toHaveValue(maskedValue);
  283. });
  284. it('shows masked clientSecret', function () {
  285. renderComponent();
  286. expect(screen.getByRole('textbox', {name: 'Client Secret'})).toHaveValue(
  287. maskedValue
  288. );
  289. });
  290. });
  291. describe('Editing internal app tokens', () => {
  292. function renderComponent() {
  293. return render(
  294. <SentryApplicationDetails
  295. router={router}
  296. location={router.location}
  297. routes={router.routes}
  298. routeParams={{}}
  299. route={router.routes[0]}
  300. params={{appSlug: sentryApp.slug}}
  301. />,
  302. {
  303. context: TestStubs.routerContext([{organization: org}]),
  304. }
  305. );
  306. }
  307. beforeEach(() => {
  308. sentryApp = SentryApp({
  309. status: 'internal',
  310. isAlertable: true,
  311. });
  312. token = SentryAppToken();
  313. sentryApp.events = ['issue'];
  314. MockApiClient.addMockResponse({
  315. url: `/sentry-apps/${sentryApp.slug}/`,
  316. body: sentryApp,
  317. });
  318. MockApiClient.addMockResponse({
  319. url: `/sentry-apps/${sentryApp.slug}/api-tokens/`,
  320. body: [token],
  321. });
  322. });
  323. it('adding token to list', async function () {
  324. MockApiClient.addMockResponse({
  325. url: `/sentry-apps/${sentryApp.slug}/api-tokens/`,
  326. method: 'POST',
  327. body: [
  328. SentryAppToken({
  329. token: '392847329',
  330. dateCreated: '2018-03-02T18:30:26Z',
  331. }),
  332. ],
  333. });
  334. renderComponent();
  335. await userEvent.click(screen.getByRole('button', {name: 'New Token'}));
  336. await waitFor(() => {
  337. expect(screen.getAllByRole('textbox', {name: 'Token value'})).toHaveLength(2);
  338. });
  339. });
  340. it('removing token from list', async function () {
  341. MockApiClient.addMockResponse({
  342. url: `/sentry-apps/${sentryApp.slug}/api-tokens/${token.token}/`,
  343. method: 'DELETE',
  344. body: {},
  345. });
  346. renderComponent();
  347. await userEvent.click(screen.getByRole('button', {name: 'Revoke'}));
  348. expect(await screen.findByText('No tokens created yet.')).toBeInTheDocument();
  349. });
  350. it('removing webhookURL unsets isAlertable and changes webhookDisabled to true', async () => {
  351. renderComponent();
  352. expect(screen.getByRole('checkbox', {name: 'Alert Rule Action'})).toBeChecked();
  353. await userEvent.clear(screen.getByRole('textbox', {name: 'Webhook URL'}));
  354. expect(screen.getByRole('checkbox', {name: 'Alert Rule Action'})).not.toBeChecked();
  355. });
  356. });
  357. describe('Editing an existing public Sentry App', () => {
  358. function renderComponent() {
  359. return render(
  360. <SentryApplicationDetails
  361. router={router}
  362. location={router.location}
  363. routes={router.routes}
  364. routeParams={{}}
  365. route={router.routes[0]}
  366. params={{appSlug: sentryApp.slug}}
  367. />,
  368. {
  369. context: TestStubs.routerContext([{organization: org}]),
  370. }
  371. );
  372. }
  373. beforeEach(() => {
  374. sentryApp = SentryApp();
  375. sentryApp.events = ['issue'];
  376. sentryApp.scopes = ['project:read', 'event:read'];
  377. editAppRequest = MockApiClient.addMockResponse({
  378. url: `/sentry-apps/${sentryApp.slug}/`,
  379. method: 'PUT',
  380. body: [],
  381. });
  382. MockApiClient.addMockResponse({
  383. url: `/sentry-apps/${sentryApp.slug}/`,
  384. body: sentryApp,
  385. });
  386. MockApiClient.addMockResponse({
  387. url: `/sentry-apps/${sentryApp.slug}/api-tokens/`,
  388. body: [],
  389. });
  390. });
  391. it('updates app with correct data', async function () {
  392. renderComponent();
  393. await userEvent.clear(screen.getByRole('textbox', {name: 'Redirect URL'}));
  394. await userEvent.type(
  395. screen.getByRole('textbox', {name: 'Redirect URL'}),
  396. 'https://hello.com/'
  397. );
  398. await userEvent.click(screen.getByRole('textbox', {name: 'Schema'}));
  399. await userEvent.paste('{}');
  400. await userEvent.click(screen.getByRole('checkbox', {name: 'issue'}));
  401. await userEvent.click(screen.getByRole('button', {name: 'Save Changes'}));
  402. expect(editAppRequest).toHaveBeenCalledWith(
  403. `/sentry-apps/${sentryApp.slug}/`,
  404. expect.objectContaining({
  405. data: expect.objectContaining({
  406. redirectUrl: 'https://hello.com/',
  407. events: [],
  408. }),
  409. method: 'PUT',
  410. })
  411. );
  412. });
  413. it('submits with no-access for event subscription when permission is revoked', async () => {
  414. renderComponent();
  415. await userEvent.click(screen.getByRole('checkbox', {name: 'issue'}));
  416. await userEvent.click(screen.getByRole('textbox', {name: 'Schema'}));
  417. await userEvent.paste('{}');
  418. await selectEvent.select(
  419. screen.getByRole('textbox', {name: 'Issue & Event'}),
  420. 'No Access'
  421. );
  422. await userEvent.click(screen.getByRole('button', {name: 'Save Changes'}));
  423. expect(editAppRequest).toHaveBeenCalledWith(
  424. `/sentry-apps/${sentryApp.slug}/`,
  425. expect.objectContaining({
  426. data: expect.objectContaining({
  427. events: [],
  428. }),
  429. method: 'PUT',
  430. })
  431. );
  432. });
  433. });
  434. describe('Editing an existing public Sentry App with a scope error', () => {
  435. function renderComponent() {
  436. render(
  437. <SentryApplicationDetails
  438. router={router}
  439. location={router.location}
  440. routes={router.routes}
  441. routeParams={{}}
  442. route={router.routes[0]}
  443. params={{appSlug: sentryApp.slug}}
  444. />,
  445. {
  446. context: TestStubs.routerContext([{organization: org}]),
  447. }
  448. );
  449. }
  450. beforeEach(() => {
  451. sentryApp = SentryApp();
  452. editAppRequest = MockApiClient.addMockResponse({
  453. url: `/sentry-apps/${sentryApp.slug}/`,
  454. method: 'PUT',
  455. statusCode: 400,
  456. body: {
  457. scopes: [
  458. "Requested permission of member:write exceeds requester's permission. Please contact an administrator to make the requested change.",
  459. "Requested permission of member:admin exceeds requester's permission. Please contact an administrator to make the requested change.",
  460. ],
  461. },
  462. });
  463. MockApiClient.addMockResponse({
  464. url: `/sentry-apps/${sentryApp.slug}/`,
  465. body: sentryApp,
  466. });
  467. MockApiClient.addMockResponse({
  468. url: `/sentry-apps/${sentryApp.slug}/api-tokens/`,
  469. body: [],
  470. });
  471. });
  472. it('renders the error', async () => {
  473. renderComponent();
  474. await userEvent.click(screen.getByRole('button', {name: 'Save Changes'}));
  475. expect(
  476. await screen.findByText(
  477. "Requested permission of member:admin exceeds requester's permission. Please contact an administrator to make the requested change."
  478. )
  479. ).toBeInTheDocument();
  480. });
  481. });
  482. });