index.spec.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  1. import {LocationFixture} from 'sentry-fixture/locationFixture';
  2. import {OrganizationFixture} from 'sentry-fixture/organization';
  3. import {RouterFixture} from 'sentry-fixture/routerFixture';
  4. import {SentryAppFixture} from 'sentry-fixture/sentryApp';
  5. import {initializeOrg} from 'sentry-test/initializeOrg';
  6. import {
  7. render,
  8. renderGlobalModal,
  9. screen,
  10. userEvent,
  11. waitFor,
  12. within,
  13. } from 'sentry-test/reactTestingLibrary';
  14. import OrganizationDeveloperSettings from 'sentry/views/settings/organizationDeveloperSettings/index';
  15. describe('Organization Developer Settings', function () {
  16. const {organization: org} = initializeOrg();
  17. const sentryApp = SentryAppFixture({
  18. scopes: [
  19. 'team:read',
  20. 'project:releases',
  21. 'event:read',
  22. 'event:write',
  23. 'org:read',
  24. 'org:write',
  25. ],
  26. });
  27. beforeEach(() => {
  28. MockApiClient.clearMockResponses();
  29. });
  30. describe('when no Apps exist', () => {
  31. it('displays empty state', async () => {
  32. MockApiClient.addMockResponse({
  33. url: `/organizations/${org.slug}/sentry-apps/`,
  34. body: [],
  35. });
  36. render(<OrganizationDeveloperSettings />);
  37. await waitFor(() => {
  38. expect(
  39. screen.getByText('No internal integrations have been created yet.')
  40. ).toBeInTheDocument();
  41. });
  42. });
  43. });
  44. describe('with unpublished apps', () => {
  45. beforeEach(() => {
  46. const sentryAppWithAvatars = SentryAppFixture({
  47. avatars: [
  48. {
  49. avatarType: 'upload',
  50. avatarUuid: '1234561234561234561234567',
  51. avatarUrl: 'https://example.com/avatar/1234561234561234561234567/',
  52. color: true,
  53. photoType: 'logo',
  54. },
  55. ],
  56. scopes: [
  57. 'team:read',
  58. 'project:releases',
  59. 'event:read',
  60. 'event:write',
  61. 'org:read',
  62. 'org:write',
  63. ],
  64. });
  65. MockApiClient.addMockResponse({
  66. url: `/organizations/${org.slug}/sentry-apps/`,
  67. body: [sentryAppWithAvatars],
  68. });
  69. });
  70. it('internal integrations list is empty', async () => {
  71. render(<OrganizationDeveloperSettings />);
  72. expect(
  73. await screen.findByText('No internal integrations have been created yet.')
  74. ).toBeInTheDocument();
  75. });
  76. it('public integrations list contains 1 item', async () => {
  77. const router = RouterFixture({
  78. location: LocationFixture({query: {type: 'public'}}),
  79. });
  80. render(<OrganizationDeveloperSettings />, {
  81. router,
  82. });
  83. expect(await screen.findByText('Sample App')).toBeInTheDocument();
  84. expect(screen.getByText('unpublished')).toBeInTheDocument();
  85. });
  86. it('allows for deletion', async () => {
  87. MockApiClient.addMockResponse({
  88. url: `/sentry-apps/${sentryApp.slug}/`,
  89. method: 'DELETE',
  90. body: [],
  91. });
  92. const router = RouterFixture({
  93. location: LocationFixture({query: {type: 'public'}}),
  94. });
  95. render(<OrganizationDeveloperSettings />, {
  96. router,
  97. });
  98. const deleteButton = await screen.findByRole('button', {name: 'Delete'});
  99. expect(deleteButton).toHaveAttribute('aria-disabled', 'false');
  100. await userEvent.click(deleteButton);
  101. renderGlobalModal();
  102. const dialog = await screen.findByRole('dialog');
  103. expect(dialog).toBeInTheDocument();
  104. const input = await within(dialog).findByPlaceholderText('sample-app');
  105. await userEvent.type(input, 'sample-app');
  106. const confirmDeleteButton = await screen.findByRole('button', {name: 'Confirm'});
  107. await userEvent.click(confirmDeleteButton);
  108. await screen.findByText('No public integrations have been created yet.');
  109. });
  110. it('can make a request to publish an integration', async () => {
  111. const mock = MockApiClient.addMockResponse({
  112. url: `/sentry-apps/${sentryApp.slug}/publish-request/`,
  113. method: 'POST',
  114. });
  115. const router = RouterFixture({
  116. location: LocationFixture({query: {type: 'public'}}),
  117. });
  118. render(<OrganizationDeveloperSettings />, {
  119. router,
  120. });
  121. const publishButton = await screen.findByRole('button', {name: 'Publish'});
  122. expect(publishButton).toHaveAttribute('aria-disabled', 'false');
  123. await userEvent.click(publishButton);
  124. renderGlobalModal();
  125. const dialog = await screen.findByRole('dialog');
  126. expect(dialog).toBeInTheDocument();
  127. const questionnaire = [
  128. {
  129. answer: 'yep',
  130. question:
  131. 'Provide a description about your integration, how this benefits developers using Sentry along with what’s needed to set up this integration.',
  132. },
  133. {
  134. answer: 'the coolest integration ever',
  135. question:
  136. 'Provide a one-liner describing your integration. Subject to approval, we’ll use this to describe your integration on Sentry Integrations .',
  137. },
  138. {
  139. answer: 'https://example.com',
  140. question: 'Link to your documentation page.',
  141. },
  142. {
  143. answer: 'https://example.com',
  144. question:
  145. 'Link to a video showing installation, setup and user flow for your submission.',
  146. },
  147. {
  148. answer: 'example@example.com',
  149. question: 'Email address for user support.',
  150. },
  151. ];
  152. await userEvent.click(
  153. screen.getByRole('textbox', {
  154. name: 'Select what category best describes your integration. Documentation for reference.',
  155. })
  156. );
  157. expect(screen.getByText('Deployment')).toBeInTheDocument();
  158. await userEvent.click(screen.getByText('Deployment'));
  159. for (const {question, answer} of questionnaire) {
  160. const element = within(dialog).getByRole('textbox', {name: question});
  161. await userEvent.type(element, answer);
  162. }
  163. const requestPublishButton =
  164. await within(dialog).findByLabelText('Request Publication');
  165. expect(requestPublishButton).toBeEnabled();
  166. await userEvent.click(requestPublishButton);
  167. expect(mock).toHaveBeenCalledTimes(1);
  168. const [url, {method, data}] = mock.mock.calls[0];
  169. expect(url).toBe(`/sentry-apps/${sentryApp.slug}/publish-request/`);
  170. expect(method).toBe('POST');
  171. expect(data).toEqual({
  172. questionnaire: expect.arrayContaining([
  173. {
  174. question:
  175. 'Provide a description about your integration, how this benefits developers using Sentry along with what’s needed to set up this integration.',
  176. answer: 'yep',
  177. },
  178. {
  179. question:
  180. 'Provide a one-liner describing your integration. Subject to approval, we’ll use this to describe your integration on Sentry Integrations.',
  181. answer: 'the coolest integration ever',
  182. },
  183. {
  184. question: 'Select what category best describes your integration.',
  185. answer: 'deployment',
  186. },
  187. {
  188. question: 'Link to your documentation page.',
  189. answer: 'https://example.com',
  190. },
  191. {
  192. question: 'Email address for user support.',
  193. answer: 'example@example.com',
  194. },
  195. {
  196. question:
  197. 'Link to a video showing installation, setup and user flow for your submission.',
  198. answer: 'https://example.com',
  199. },
  200. ]),
  201. });
  202. });
  203. });
  204. describe('with published apps', () => {
  205. beforeEach(() => {
  206. const publishedSentryApp = SentryAppFixture({status: 'published'});
  207. MockApiClient.addMockResponse({
  208. url: `/organizations/${org.slug}/sentry-apps/`,
  209. body: [publishedSentryApp],
  210. });
  211. });
  212. it('shows the published status', async () => {
  213. const router = RouterFixture({
  214. location: LocationFixture({query: {type: 'public'}}),
  215. });
  216. render(<OrganizationDeveloperSettings />, {
  217. router,
  218. });
  219. expect(await screen.findByText('published')).toBeInTheDocument();
  220. });
  221. it('trash button is disabled', async () => {
  222. const router = RouterFixture({
  223. location: LocationFixture({query: {type: 'public'}}),
  224. });
  225. render(<OrganizationDeveloperSettings />, {
  226. router,
  227. });
  228. const deleteButton = await screen.findByRole('button', {name: 'Delete'});
  229. expect(deleteButton).toHaveAttribute('aria-disabled', 'true');
  230. });
  231. it('publish button is disabled', async () => {
  232. const router = RouterFixture({
  233. location: LocationFixture({query: {type: 'public'}}),
  234. });
  235. render(<OrganizationDeveloperSettings />, {
  236. router,
  237. });
  238. const publishButton = await screen.findByRole('button', {name: 'Publish'});
  239. expect(publishButton).toHaveAttribute('aria-disabled', 'true');
  240. });
  241. });
  242. describe('with Internal Integrations', () => {
  243. beforeEach(() => {
  244. const internalIntegration = SentryAppFixture({status: 'internal'});
  245. MockApiClient.addMockResponse({
  246. url: `/organizations/${org.slug}/sentry-apps/`,
  247. body: [internalIntegration],
  248. });
  249. });
  250. it('allows deleting', async () => {
  251. render(<OrganizationDeveloperSettings />);
  252. const deleteButton = await screen.findByRole('button', {name: 'Delete'});
  253. expect(deleteButton).toHaveAttribute('aria-disabled', 'false');
  254. });
  255. it('publish button does not exist', () => {
  256. render(<OrganizationDeveloperSettings />);
  257. expect(screen.queryByText('Publish')).not.toBeInTheDocument();
  258. });
  259. });
  260. describe('without Owner permissions', () => {
  261. const newOrg = OrganizationFixture({access: ['org:read']});
  262. beforeEach(() => {
  263. MockApiClient.addMockResponse({
  264. url: `/organizations/${newOrg.slug}/sentry-apps/`,
  265. body: [sentryApp],
  266. });
  267. });
  268. it('trash button is disabled', async () => {
  269. const router = RouterFixture({
  270. location: LocationFixture({query: {type: 'public'}}),
  271. });
  272. render(<OrganizationDeveloperSettings />, {
  273. router,
  274. organization: newOrg,
  275. });
  276. const deleteButton = await screen.findByRole('button', {name: 'Delete'});
  277. expect(deleteButton).toHaveAttribute('aria-disabled', 'true');
  278. });
  279. it('publish button is disabled', async () => {
  280. const router = RouterFixture({
  281. location: LocationFixture({query: {type: 'public'}}),
  282. });
  283. render(<OrganizationDeveloperSettings />, {
  284. organization: newOrg,
  285. router,
  286. });
  287. const publishButton = await screen.findByRole('button', {name: 'Publish'});
  288. expect(publishButton).toHaveAttribute('aria-disabled', 'true');
  289. });
  290. });
  291. });