sentryApplicationDetails.spec.jsx 15 KB

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