sentryApplicationDetails.spec.jsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455
  1. import {mountWithTheme} from 'sentry-test/enzyme';
  2. import {selectByValue} from 'sentry-test/select-new';
  3. import {Client} from 'sentry/api';
  4. import JsonForm from 'sentry/components/forms/jsonForm';
  5. import PermissionsObserver from 'sentry/views/settings/organizationDeveloperSettings/permissionsObserver';
  6. import SentryApplicationDetails from 'sentry/views/settings/organizationDeveloperSettings/sentryApplicationDetails';
  7. describe('Sentry Application Details', function () {
  8. let org;
  9. let orgId;
  10. let sentryApp;
  11. let token;
  12. let wrapper;
  13. let createAppRequest;
  14. let editAppRequest;
  15. const verifyInstallToggle = 'Switch[name="verifyInstall"]';
  16. const redirectUrlInput = 'Input[name="redirectUrl"]';
  17. const maskedValue = '*'.repeat(64);
  18. beforeEach(() => {
  19. Client.clearMockResponses();
  20. org = TestStubs.Organization({features: ['sentry-app-logo-upload']});
  21. orgId = org.slug;
  22. });
  23. describe('Creating a new public Sentry App', () => {
  24. beforeEach(() => {
  25. createAppRequest = Client.addMockResponse({
  26. url: '/sentry-apps/',
  27. method: 'POST',
  28. body: [],
  29. });
  30. wrapper = mountWithTheme(
  31. <SentryApplicationDetails params={{orgId}} route={{path: 'new-public/'}} />,
  32. TestStubs.routerContext([{organization: org}])
  33. );
  34. });
  35. it('has inputs for redirectUrl and verifyInstall', () => {
  36. expect(wrapper.exists(verifyInstallToggle)).toBeTruthy();
  37. expect(wrapper.exists(redirectUrlInput)).toBeTruthy();
  38. });
  39. it('shows empty scopes and no credentials', function () {
  40. // new app starts off with no scopes selected
  41. expect(wrapper.find('PermissionsObserver').prop('scopes')).toEqual([]);
  42. expect(
  43. wrapper.find('PanelHeader').findWhere(h => h.text() === 'Permissions')
  44. ).toBeDefined();
  45. });
  46. it('does not show logo upload fields', function () {
  47. expect(wrapper.find('PanelHeader').at(1).text()).not.toContain('Logo');
  48. expect(wrapper.find('PanelHeader').at(2).text()).not.toContain('Small Icon');
  49. expect(wrapper.exists('AvatarChooser')).toBe(false);
  50. });
  51. it('saves', function () {
  52. wrapper
  53. .find('Input[name="name"]')
  54. .simulate('change', {target: {value: 'Test App'}});
  55. wrapper
  56. .find('Input[name="author"]')
  57. .simulate('change', {target: {value: 'Sentry'}});
  58. wrapper
  59. .find('Input[name="webhookUrl"]')
  60. .simulate('change', {target: {value: 'https://webhook.com'}});
  61. wrapper
  62. .find(redirectUrlInput)
  63. .simulate('change', {target: {value: 'https://webhook.com/setup'}});
  64. wrapper.find('TextArea[name="schema"]').simulate('change', {target: {value: '{}'}});
  65. wrapper.find('Switch[name="isAlertable"]').simulate('click');
  66. selectByValue(wrapper, 'admin', {name: 'Member--permission'});
  67. selectByValue(wrapper, 'admin', {name: 'Event--permission'});
  68. wrapper
  69. .find('Checkbox')
  70. .first()
  71. .simulate('change', {target: {checked: true}});
  72. wrapper.find('form').simulate('submit');
  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. beforeEach(() => {
  103. wrapper = mountWithTheme(
  104. <SentryApplicationDetails params={{orgId}} route={{path: 'new-internal/'}} />,
  105. TestStubs.routerContext([{organization: org}])
  106. );
  107. });
  108. it('does not show logo upload fields', function () {
  109. expect(wrapper.find('PanelHeader').at(1).text()).not.toContain('Logo');
  110. expect(wrapper.find('PanelHeader').at(2).text()).not.toContain('Small Icon');
  111. expect(wrapper.exists('AvatarChooser')).toBe(false);
  112. });
  113. it('no inputs for redirectUrl and verifyInstall', () => {
  114. expect(wrapper.exists(verifyInstallToggle)).toBeFalsy();
  115. expect(wrapper.exists(redirectUrlInput)).toBeFalsy();
  116. });
  117. });
  118. describe('Renders public app', function () {
  119. beforeEach(() => {
  120. sentryApp = TestStubs.SentryApp();
  121. sentryApp.events = ['issue'];
  122. Client.addMockResponse({
  123. url: `/sentry-apps/${sentryApp.slug}/`,
  124. body: sentryApp,
  125. });
  126. Client.addMockResponse({
  127. url: `/sentry-apps/${sentryApp.slug}/api-tokens/`,
  128. body: [],
  129. });
  130. wrapper = mountWithTheme(
  131. <SentryApplicationDetails params={{appSlug: sentryApp.slug, orgId}} />,
  132. TestStubs.routerContext([{organization: org}])
  133. );
  134. });
  135. it('shows logo upload fields', function () {
  136. expect(wrapper.find('PanelHeader').at(1).text()).toContain('Logo');
  137. expect(wrapper.find('PanelHeader').at(2).text()).toContain('Small Icon');
  138. expect(wrapper.find('AvatarChooser')).toHaveLength(2);
  139. });
  140. it('has inputs for redirectUrl and verifyInstall', () => {
  141. expect(wrapper.exists(verifyInstallToggle)).toBeTruthy();
  142. expect(wrapper.exists(redirectUrlInput)).toBeTruthy();
  143. });
  144. it('shows application data', function () {
  145. // data should be filled out
  146. expect(wrapper.find('PermissionsObserver').prop('scopes')).toEqual([
  147. 'project:read',
  148. ]);
  149. });
  150. it('renders clientId and clientSecret for public apps', function () {
  151. expect(wrapper.find('#clientId').exists()).toBe(true);
  152. expect(wrapper.find('#clientSecret').exists()).toBe(true);
  153. });
  154. });
  155. describe('Renders for internal apps', () => {
  156. beforeEach(() => {
  157. sentryApp = TestStubs.SentryApp({
  158. status: 'internal',
  159. });
  160. token = TestStubs.SentryAppToken();
  161. sentryApp.events = ['issue'];
  162. Client.addMockResponse({
  163. url: `/sentry-apps/${sentryApp.slug}/`,
  164. body: sentryApp,
  165. });
  166. Client.addMockResponse({
  167. url: `/sentry-apps/${sentryApp.slug}/api-tokens/`,
  168. body: [token],
  169. });
  170. wrapper = mountWithTheme(
  171. <SentryApplicationDetails params={{appSlug: sentryApp.slug, orgId}} />,
  172. TestStubs.routerContext([{organization: org}])
  173. );
  174. });
  175. it('no inputs for redirectUrl and verifyInstall', () => {
  176. expect(wrapper.exists(verifyInstallToggle)).toBeFalsy();
  177. expect(wrapper.exists(redirectUrlInput)).toBeFalsy();
  178. });
  179. it('shows logo upload fields', function () {
  180. expect(wrapper.find('PanelHeader').at(1).text()).toContain('Logo');
  181. expect(wrapper.find('PanelHeader').at(2).text()).toContain('Small Icon');
  182. expect(wrapper.find('AvatarChooser')).toHaveLength(2);
  183. });
  184. it('shows tokens', function () {
  185. expect(wrapper.find('PanelHeader').at(5).text()).toContain('Tokens');
  186. expect(wrapper.find('TokenItem').exists()).toBe(true);
  187. });
  188. it('shows just clientSecret', function () {
  189. expect(wrapper.find('#clientSecret').exists()).toBe(true);
  190. expect(wrapper.find('#clientId').exists()).toBe(false);
  191. });
  192. });
  193. describe('Renders masked values', () => {
  194. beforeEach(() => {
  195. sentryApp = TestStubs.SentryApp({
  196. status: 'internal',
  197. clientSecret: maskedValue,
  198. });
  199. token = TestStubs.SentryAppToken({token: maskedValue, refreshToken: maskedValue});
  200. sentryApp.events = ['issue'];
  201. Client.addMockResponse({
  202. url: `/sentry-apps/${sentryApp.slug}/`,
  203. body: sentryApp,
  204. });
  205. Client.addMockResponse({
  206. url: `/sentry-apps/${sentryApp.slug}/api-tokens/`,
  207. body: [token],
  208. });
  209. wrapper = mountWithTheme(
  210. <SentryApplicationDetails params={{appSlug: sentryApp.slug, orgId}} />,
  211. TestStubs.routerContext([{organization: org}])
  212. );
  213. });
  214. it('shows masked tokens', function () {
  215. expect(wrapper.find('TextCopyInput input').first().prop('value')).toBe(maskedValue);
  216. });
  217. it('shows masked clientSecret', function () {
  218. expect(wrapper.find('#clientSecret input').prop('value')).toBe(maskedValue);
  219. });
  220. });
  221. describe('Editing internal app tokens', () => {
  222. beforeEach(() => {
  223. sentryApp = TestStubs.SentryApp({
  224. status: 'internal',
  225. isAlertable: true,
  226. });
  227. token = TestStubs.SentryAppToken();
  228. sentryApp.events = ['issue'];
  229. Client.addMockResponse({
  230. url: `/sentry-apps/${sentryApp.slug}/`,
  231. body: sentryApp,
  232. });
  233. Client.addMockResponse({
  234. url: `/sentry-apps/${sentryApp.slug}/api-tokens/`,
  235. body: [token],
  236. });
  237. wrapper = mountWithTheme(
  238. <SentryApplicationDetails params={{appSlug: sentryApp.slug, orgId}} />,
  239. TestStubs.routerContext([{organization: org}])
  240. );
  241. });
  242. it('adding token to list', async function () {
  243. Client.addMockResponse({
  244. url: `/sentry-apps/${sentryApp.slug}/api-tokens/`,
  245. method: 'POST',
  246. body: [
  247. TestStubs.SentryAppToken({
  248. token: '392847329',
  249. dateCreated: '2018-03-02T18:30:26Z',
  250. }),
  251. ],
  252. });
  253. wrapper.find('Button[data-test-id="token-add"]').simulate('click');
  254. await tick();
  255. wrapper.update();
  256. const tokenItems = wrapper.find('TokenItem');
  257. expect(tokenItems).toHaveLength(2);
  258. });
  259. it('removing token from list', async function () {
  260. Client.addMockResponse({
  261. url: `/sentry-apps/${sentryApp.slug}/api-tokens/${token.token}/`,
  262. method: 'DELETE',
  263. body: {},
  264. });
  265. wrapper.find('Button[data-test-id="token-delete"]').simulate('click');
  266. await tick();
  267. wrapper.update();
  268. expect(wrapper.find('EmptyMessage').exists()).toBe(true);
  269. });
  270. it('removing webhookURL unsets isAlertable and changes webhookDisabled to true', () => {
  271. expect(wrapper.find(PermissionsObserver).prop('webhookDisabled')).toBe(false);
  272. expect(wrapper.find('Switch[name="isAlertable"]').prop('isActive')).toBe(true);
  273. wrapper.find('Input[name="webhookUrl"]').simulate('change', {target: {value: ''}});
  274. expect(wrapper.find('Switch[name="isAlertable"]').prop('isActive')).toBe(false);
  275. expect(wrapper.find(PermissionsObserver).prop('webhookDisabled')).toBe(true);
  276. expect(wrapper.find(JsonForm).prop('additionalFieldProps')).toEqual({
  277. webhookDisabled: true,
  278. });
  279. });
  280. });
  281. describe('Editing an existing public Sentry App', () => {
  282. beforeEach(() => {
  283. sentryApp = TestStubs.SentryApp();
  284. sentryApp.events = ['issue'];
  285. editAppRequest = Client.addMockResponse({
  286. url: `/sentry-apps/${sentryApp.slug}/`,
  287. method: 'PUT',
  288. body: [],
  289. });
  290. Client.addMockResponse({
  291. url: `/sentry-apps/${sentryApp.slug}/`,
  292. body: sentryApp,
  293. });
  294. Client.addMockResponse({
  295. url: `/sentry-apps/${sentryApp.slug}/api-tokens/`,
  296. body: [],
  297. });
  298. wrapper = mountWithTheme(
  299. <SentryApplicationDetails params={{appSlug: sentryApp.slug, orgId}} />,
  300. TestStubs.routerContext([{organization: org}])
  301. );
  302. });
  303. it('updates app with correct data', function () {
  304. wrapper
  305. .find(redirectUrlInput)
  306. .simulate('change', {target: {value: 'https://hello.com/'}});
  307. wrapper.find('TextArea[name="schema"]').simulate('change', {target: {value: '{}'}});
  308. wrapper
  309. .find('Checkbox')
  310. .first()
  311. .simulate('change', {target: {checked: false}});
  312. wrapper.find('form').simulate('submit');
  313. expect(editAppRequest).toHaveBeenCalledWith(
  314. `/sentry-apps/${sentryApp.slug}/`,
  315. expect.objectContaining({
  316. data: expect.objectContaining({
  317. redirectUrl: 'https://hello.com/',
  318. events: [],
  319. }),
  320. method: 'PUT',
  321. })
  322. );
  323. });
  324. it('submits with no-access for event subscription when permission is revoked', () => {
  325. wrapper
  326. .find('Checkbox')
  327. .first()
  328. .simulate('change', {target: {checked: true}});
  329. wrapper.find('TextArea[name="schema"]').simulate('change', {target: {value: '{}'}});
  330. selectByValue(wrapper, 'no-access', {name: 'Event--permission'});
  331. wrapper.find('form').simulate('submit');
  332. expect(editAppRequest).toHaveBeenCalledWith(
  333. `/sentry-apps/${sentryApp.slug}/`,
  334. expect.objectContaining({
  335. data: expect.objectContaining({
  336. events: [],
  337. }),
  338. method: 'PUT',
  339. })
  340. );
  341. });
  342. });
  343. describe('Editing an existing public Sentry App with a scope error', () => {
  344. beforeEach(() => {
  345. sentryApp = TestStubs.SentryApp();
  346. editAppRequest = Client.addMockResponse({
  347. url: `/sentry-apps/${sentryApp.slug}/`,
  348. method: 'PUT',
  349. statusCode: 400,
  350. body: {
  351. scopes: [
  352. "Requested permission of member:write exceeds requester's permission. Please contact an administrator to make the requested change.",
  353. "Requested permission of member:admin exceeds requester's permission. Please contact an administrator to make the requested change.",
  354. ],
  355. },
  356. });
  357. Client.addMockResponse({
  358. url: `/sentry-apps/${sentryApp.slug}/`,
  359. body: sentryApp,
  360. });
  361. Client.addMockResponse({
  362. url: `/sentry-apps/${sentryApp.slug}/api-tokens/`,
  363. body: [],
  364. });
  365. wrapper = mountWithTheme(
  366. <SentryApplicationDetails params={{appSlug: sentryApp.slug, orgId}} />,
  367. TestStubs.routerContext([{organization: org}])
  368. );
  369. });
  370. it('renders the error', async () => {
  371. wrapper.find('form').simulate('submit');
  372. await tick();
  373. wrapper.update();
  374. expect(wrapper.find('div FieldErrorReason').text()).toEqual(
  375. "Requested permission of member:admin exceeds requester's permission. Please contact an administrator to make the requested change."
  376. );
  377. });
  378. });
  379. });