sentryApplicationDetails.spec.jsx 15 KB

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