sentryApplicationDetails.spec.tsx 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590
  1. import selectEvent from 'react-select-event';
  2. import {OrganizationFixture} from 'sentry-fixture/organization';
  3. import {RouterContextFixture} from 'sentry-fixture/routerContextFixture';
  4. import {RouterFixture} from 'sentry-fixture/routerFixture';
  5. import {SentryAppFixture} from 'sentry-fixture/sentryApp';
  6. import {SentryAppTokenFixture} 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 = '************oken';
  16. const router = RouterFixture();
  17. beforeEach(() => {
  18. MockApiClient.clearMockResponses();
  19. org = OrganizationFixture({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 = 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', 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 = SentryAppFixture({
  210. status: 'internal',
  211. });
  212. token = SentryAppTokenFixture();
  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('has tokens', function () {
  238. renderComponent();
  239. expect(screen.getByText('Tokens')).toBeInTheDocument();
  240. expect(screen.getByLabelText('Token preview')).toHaveTextContent('oken');
  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: RouterContextFixture([{organization: org}]),
  261. }
  262. );
  263. }
  264. beforeEach(() => {
  265. sentryApp = SentryAppFixture({
  266. status: 'internal',
  267. clientSecret: maskedValue,
  268. });
  269. token = SentryAppTokenFixture({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.getByLabelText('Token preview')).toHaveTextContent(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: RouterContextFixture([{organization: org}]),
  304. }
  305. );
  306. }
  307. beforeEach(() => {
  308. sentryApp = SentryAppFixture({
  309. status: 'internal',
  310. isAlertable: true,
  311. });
  312. token = SentryAppTokenFixture();
  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. SentryAppTokenFixture({
  329. token: '392847329',
  330. dateCreated: '2018-03-02T18:30:26Z',
  331. id: '234',
  332. }),
  333. ],
  334. });
  335. renderComponent();
  336. expect(screen.queryByLabelText('Generated token')).not.toBeInTheDocument();
  337. expect(screen.getAllByLabelText('Token preview')).toHaveLength(1);
  338. await userEvent.click(screen.getByRole('button', {name: 'New Token'}));
  339. await waitFor(() => {
  340. expect(screen.getAllByLabelText('Token preview')).toHaveLength(1);
  341. });
  342. await waitFor(() => {
  343. expect(screen.getAllByLabelText('Generated token')).toHaveLength(1);
  344. });
  345. });
  346. it('removing token from list', async function () {
  347. MockApiClient.addMockResponse({
  348. url: `/sentry-apps/${sentryApp.slug}/api-tokens/${token.id}/`,
  349. method: 'DELETE',
  350. body: {},
  351. });
  352. renderComponent();
  353. await userEvent.click(screen.getByRole('button', {name: 'Remove'}));
  354. expect(await screen.findByText('No tokens created yet.')).toBeInTheDocument();
  355. });
  356. it('removing webhookURL unsets isAlertable and changes webhookDisabled to true', async () => {
  357. renderComponent();
  358. expect(screen.getByRole('checkbox', {name: 'Alert Rule Action'})).toBeChecked();
  359. await userEvent.clear(screen.getByRole('textbox', {name: 'Webhook URL'}));
  360. expect(screen.getByRole('checkbox', {name: 'Alert Rule Action'})).not.toBeChecked();
  361. });
  362. });
  363. describe('Editing an existing public Sentry App', () => {
  364. function renderComponent() {
  365. return render(
  366. <SentryApplicationDetails
  367. router={router}
  368. location={router.location}
  369. routes={router.routes}
  370. routeParams={{}}
  371. route={router.routes[0]}
  372. params={{appSlug: sentryApp.slug}}
  373. />,
  374. {
  375. context: RouterContextFixture([{organization: org}]),
  376. }
  377. );
  378. }
  379. beforeEach(() => {
  380. sentryApp = SentryAppFixture();
  381. sentryApp.events = ['issue'];
  382. sentryApp.scopes = ['project:read', 'event:read'];
  383. editAppRequest = MockApiClient.addMockResponse({
  384. url: `/sentry-apps/${sentryApp.slug}/`,
  385. method: 'PUT',
  386. body: [],
  387. });
  388. MockApiClient.addMockResponse({
  389. url: `/sentry-apps/${sentryApp.slug}/`,
  390. body: sentryApp,
  391. });
  392. MockApiClient.addMockResponse({
  393. url: `/sentry-apps/${sentryApp.slug}/api-tokens/`,
  394. body: [],
  395. });
  396. });
  397. it('updates app with correct data', async function () {
  398. renderComponent();
  399. await userEvent.clear(screen.getByRole('textbox', {name: 'Redirect URL'}));
  400. await userEvent.type(
  401. screen.getByRole('textbox', {name: 'Redirect URL'}),
  402. 'https://hello.com/'
  403. );
  404. await userEvent.click(screen.getByRole('textbox', {name: 'Schema'}));
  405. await userEvent.paste('{}');
  406. await userEvent.click(screen.getByRole('checkbox', {name: 'issue'}));
  407. await userEvent.click(screen.getByRole('button', {name: 'Save Changes'}));
  408. expect(editAppRequest).toHaveBeenCalledWith(
  409. `/sentry-apps/${sentryApp.slug}/`,
  410. expect.objectContaining({
  411. data: expect.objectContaining({
  412. redirectUrl: 'https://hello.com/',
  413. events: [],
  414. }),
  415. method: 'PUT',
  416. })
  417. );
  418. });
  419. it('submits with no-access for event subscription when permission is revoked', async () => {
  420. renderComponent();
  421. await userEvent.click(screen.getByRole('checkbox', {name: 'issue'}));
  422. await userEvent.click(screen.getByRole('textbox', {name: 'Schema'}));
  423. await userEvent.paste('{}');
  424. await selectEvent.select(
  425. screen.getByRole('textbox', {name: 'Issue & Event'}),
  426. 'No Access'
  427. );
  428. await userEvent.click(screen.getByRole('button', {name: 'Save Changes'}));
  429. expect(editAppRequest).toHaveBeenCalledWith(
  430. `/sentry-apps/${sentryApp.slug}/`,
  431. expect.objectContaining({
  432. data: expect.objectContaining({
  433. events: [],
  434. }),
  435. method: 'PUT',
  436. })
  437. );
  438. });
  439. });
  440. describe('Editing an existing public Sentry App with a scope error', () => {
  441. function renderComponent() {
  442. render(
  443. <SentryApplicationDetails
  444. router={router}
  445. location={router.location}
  446. routes={router.routes}
  447. routeParams={{}}
  448. route={router.routes[0]}
  449. params={{appSlug: sentryApp.slug}}
  450. />,
  451. {
  452. context: RouterContextFixture([{organization: org}]),
  453. }
  454. );
  455. }
  456. beforeEach(() => {
  457. sentryApp = SentryAppFixture();
  458. editAppRequest = MockApiClient.addMockResponse({
  459. url: `/sentry-apps/${sentryApp.slug}/`,
  460. method: 'PUT',
  461. statusCode: 400,
  462. body: {
  463. scopes: [
  464. "Requested permission of member:write exceeds requester's permission. Please contact an administrator to make the requested change.",
  465. "Requested permission of member:admin exceeds requester's permission. Please contact an administrator to make the requested change.",
  466. ],
  467. },
  468. });
  469. MockApiClient.addMockResponse({
  470. url: `/sentry-apps/${sentryApp.slug}/`,
  471. body: sentryApp,
  472. });
  473. MockApiClient.addMockResponse({
  474. url: `/sentry-apps/${sentryApp.slug}/api-tokens/`,
  475. body: [],
  476. });
  477. });
  478. it('renders the error', async () => {
  479. renderComponent();
  480. await userEvent.click(screen.getByRole('button', {name: 'Save Changes'}));
  481. expect(
  482. await screen.findByText(
  483. "Requested permission of member:admin exceeds requester's permission. Please contact an administrator to make the requested change."
  484. )
  485. ).toBeInTheDocument();
  486. });
  487. });
  488. });