sentryApplicationDetails.spec.jsx 15 KB

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