sentryApplicationDetails.spec.tsx 19 KB

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