sentryApplicationDetails.spec.tsx 18 KB

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