sentryApplicationDetails.spec.tsx 17 KB

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