index.spec.tsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339
  1. import {
  2. render,
  3. renderGlobalModal,
  4. screen,
  5. userEvent,
  6. waitFor,
  7. within,
  8. } from 'sentry-test/reactTestingLibrary';
  9. import OrganizationDeveloperSettings from 'sentry/views/settings/organizationDeveloperSettings/index';
  10. describe('Organization Developer Settings', function () {
  11. const org = TestStubs.Organization();
  12. const sentryApp = TestStubs.SentryApp({
  13. scopes: [
  14. 'team:read',
  15. 'project:releases',
  16. 'event:read',
  17. 'event:write',
  18. 'org:read',
  19. 'org:write',
  20. ],
  21. });
  22. beforeEach(() => {
  23. MockApiClient.clearMockResponses();
  24. });
  25. const router = TestStubs.router();
  26. describe('when no Apps exist', () => {
  27. it('displays empty state', async () => {
  28. MockApiClient.addMockResponse({
  29. url: `/organizations/${org.slug}/sentry-apps/`,
  30. body: [],
  31. });
  32. const {container} = render(
  33. <OrganizationDeveloperSettings
  34. organization={org}
  35. router={router}
  36. route={router.routes[0]}
  37. params={router.params}
  38. routeParams={router.params}
  39. location={router.location}
  40. routes={router.routes}
  41. />
  42. );
  43. await waitFor(() => {
  44. expect(
  45. screen.getByText('No internal integrations have been created yet.')
  46. ).toBeInTheDocument();
  47. });
  48. expect(container).toSnapshot();
  49. });
  50. });
  51. describe('with unpublished apps', () => {
  52. beforeEach(() => {
  53. MockApiClient.addMockResponse({
  54. url: `/organizations/${org.slug}/sentry-apps/`,
  55. body: [sentryApp],
  56. });
  57. });
  58. it('internal integrations list is empty', () => {
  59. render(
  60. <OrganizationDeveloperSettings
  61. organization={org}
  62. router={router}
  63. route={router.routes[0]}
  64. params={router.params}
  65. routeParams={router.params}
  66. location={router.location}
  67. routes={router.routes}
  68. />,
  69. {organization: org}
  70. );
  71. expect(
  72. screen.getByText('No internal integrations have been created yet.')
  73. ).toBeInTheDocument();
  74. });
  75. it('public integrations list contains 1 item', () => {
  76. render(
  77. <OrganizationDeveloperSettings
  78. organization={org}
  79. router={router}
  80. route={router.routes[0]}
  81. params={router.params}
  82. routeParams={router.params}
  83. routes={router.routes}
  84. location={{...router.location, query: {type: 'public'}}}
  85. />,
  86. {organization: org}
  87. );
  88. expect(screen.getByText('Sample App')).toBeInTheDocument();
  89. expect(screen.getByText('unpublished')).toBeInTheDocument();
  90. });
  91. it('allows for deletion', async () => {
  92. MockApiClient.addMockResponse({
  93. url: `/sentry-apps/${sentryApp.slug}/`,
  94. method: 'DELETE',
  95. body: [],
  96. });
  97. render(
  98. <OrganizationDeveloperSettings
  99. organization={org}
  100. router={router}
  101. route={router.routes[0]}
  102. params={router.params}
  103. routeParams={router.params}
  104. routes={router.routes}
  105. location={{...router.location, query: {type: 'public'}}}
  106. />
  107. );
  108. const deleteButton = await screen.findByRole('button', {name: 'Delete'});
  109. expect(deleteButton).toHaveAttribute('aria-disabled', 'false');
  110. await userEvent.click(deleteButton);
  111. renderGlobalModal();
  112. const dialog = await screen.findByRole('dialog');
  113. expect(dialog).toBeInTheDocument();
  114. const input = await within(dialog).findByPlaceholderText('sample-app');
  115. await userEvent.type(input, 'sample-app');
  116. const confirmDeleteButton = await screen.findByRole('button', {name: 'Confirm'});
  117. await userEvent.click(confirmDeleteButton);
  118. await screen.findByText('No public integrations have been created yet.');
  119. });
  120. it('can make a request to publish an integration', async () => {
  121. const mock = MockApiClient.addMockResponse({
  122. url: `/sentry-apps/${sentryApp.slug}/publish-request/`,
  123. method: 'POST',
  124. });
  125. render(
  126. <OrganizationDeveloperSettings
  127. organization={org}
  128. router={router}
  129. route={router.routes[0]}
  130. params={router.params}
  131. routeParams={router.params}
  132. routes={router.routes}
  133. location={{...router.location, query: {type: 'public'}}}
  134. />
  135. );
  136. const publishButton = await screen.findByRole('button', {name: 'Publish'});
  137. expect(publishButton).toHaveAttribute('aria-disabled', 'false');
  138. await userEvent.click(publishButton);
  139. renderGlobalModal();
  140. const dialog = await screen.findByRole('dialog');
  141. expect(dialog).toBeInTheDocument();
  142. const questionnaire = [
  143. {
  144. answer: 'Answer 0',
  145. question: 'What does your integration do? Please be as detailed as possible.',
  146. },
  147. {answer: 'Answer 1', question: 'What value does it offer customers?'},
  148. {
  149. answer: 'Answer 2',
  150. question: 'Do you operate the web service your integration communicates with?',
  151. },
  152. {
  153. answer: 'Answer 3',
  154. question:
  155. 'Please justify why you are requesting each of the following permissions: Team Read, Release Admin, Event Write, Organization Write.',
  156. },
  157. ];
  158. for (const {question, answer} of questionnaire) {
  159. const element = within(dialog).getByRole('textbox', {name: question});
  160. await userEvent.type(element, answer);
  161. }
  162. const requestPublishButton = await within(dialog).findByLabelText(
  163. 'Request Publication'
  164. );
  165. expect(requestPublishButton).toHaveAttribute('aria-disabled', 'false');
  166. await userEvent.click(requestPublishButton);
  167. expect(mock).toHaveBeenCalledWith(
  168. `/sentry-apps/${sentryApp.slug}/publish-request/`,
  169. expect.objectContaining({
  170. data: {questionnaire},
  171. })
  172. );
  173. });
  174. });
  175. describe('with published apps', () => {
  176. beforeEach(() => {
  177. const publishedSentryApp = TestStubs.SentryApp({status: 'published'});
  178. MockApiClient.addMockResponse({
  179. url: `/organizations/${org.slug}/sentry-apps/`,
  180. body: [publishedSentryApp],
  181. });
  182. });
  183. it('shows the published status', () => {
  184. render(
  185. <OrganizationDeveloperSettings
  186. organization={org}
  187. router={router}
  188. route={router.routes[0]}
  189. params={router.params}
  190. routeParams={router.params}
  191. routes={router.routes}
  192. location={{...router.location, query: {type: 'public'}}}
  193. />
  194. );
  195. expect(screen.getByText('published')).toBeInTheDocument();
  196. });
  197. it('trash button is disabled', async () => {
  198. render(
  199. <OrganizationDeveloperSettings
  200. organization={org}
  201. router={router}
  202. route={router.routes[0]}
  203. params={router.params}
  204. routeParams={router.params}
  205. routes={router.routes}
  206. location={{...router.location, query: {type: 'public'}}}
  207. />
  208. );
  209. const deleteButton = await screen.findByRole('button', {name: 'Delete'});
  210. expect(deleteButton).toHaveAttribute('aria-disabled', 'true');
  211. });
  212. it('publish button is disabled', async () => {
  213. render(
  214. <OrganizationDeveloperSettings
  215. organization={org}
  216. router={router}
  217. route={router.routes[0]}
  218. params={router.params}
  219. routeParams={router.params}
  220. routes={router.routes}
  221. location={{...router.location, query: {type: 'public'}}}
  222. />
  223. );
  224. const publishButton = await screen.findByRole('button', {name: 'Publish'});
  225. expect(publishButton).toHaveAttribute('aria-disabled', 'true');
  226. });
  227. });
  228. describe('with Internal Integrations', () => {
  229. beforeEach(() => {
  230. const internalIntegration = TestStubs.SentryApp({status: 'internal'});
  231. MockApiClient.addMockResponse({
  232. url: `/organizations/${org.slug}/sentry-apps/`,
  233. body: [internalIntegration],
  234. });
  235. });
  236. it('allows deleting', async () => {
  237. render(
  238. <OrganizationDeveloperSettings
  239. organization={org}
  240. router={router}
  241. route={router.routes[0]}
  242. params={router.params}
  243. routeParams={router.params}
  244. location={router.location}
  245. routes={router.routes}
  246. />
  247. );
  248. const deleteButton = await screen.findByRole('button', {name: 'Delete'});
  249. expect(deleteButton).toHaveAttribute('aria-disabled', 'false');
  250. });
  251. it('publish button does not exist', () => {
  252. render(
  253. <OrganizationDeveloperSettings
  254. organization={org}
  255. router={router}
  256. route={router.routes[0]}
  257. params={router.params}
  258. routeParams={router.params}
  259. location={router.location}
  260. routes={router.routes}
  261. />
  262. );
  263. expect(screen.queryByText('Publish')).not.toBeInTheDocument();
  264. });
  265. });
  266. describe('without Owner permissions', () => {
  267. const newOrg = TestStubs.Organization({access: ['org:read']});
  268. beforeEach(() => {
  269. MockApiClient.addMockResponse({
  270. url: `/organizations/${newOrg.slug}/sentry-apps/`,
  271. body: [sentryApp],
  272. });
  273. });
  274. it('trash button is disabled', async () => {
  275. render(
  276. <OrganizationDeveloperSettings
  277. organization={newOrg}
  278. router={router}
  279. route={router.routes[0]}
  280. params={router.params}
  281. routeParams={router.params}
  282. routes={router.routes}
  283. location={{...router.location, query: {type: 'public'}}}
  284. />,
  285. {organization: newOrg}
  286. );
  287. const deleteButton = await screen.findByRole('button', {name: 'Delete'});
  288. expect(deleteButton).toHaveAttribute('aria-disabled', 'true');
  289. });
  290. it('publish button is disabled', async () => {
  291. render(
  292. <OrganizationDeveloperSettings
  293. organization={newOrg}
  294. router={router}
  295. route={router.routes[0]}
  296. params={router.params}
  297. routeParams={router.params}
  298. routes={router.routes}
  299. location={{...router.location, query: {type: 'public'}}}
  300. />,
  301. {organization: newOrg}
  302. );
  303. const publishButton = await screen.findByRole('button', {name: 'Publish'});
  304. expect(publishButton).toHaveAttribute('aria-disabled', 'true');
  305. });
  306. });
  307. });