sentryApplicationDetails.spec.tsx 17 KB

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