projectTeams.spec.jsx 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
  1. import {initializeOrg} from 'sentry-test/initializeOrg';
  2. import {
  3. render,
  4. renderGlobalModal,
  5. screen,
  6. userEvent,
  7. waitFor,
  8. } from 'sentry-test/reactTestingLibrary';
  9. import TeamStore from 'sentry/stores/teamStore';
  10. import ProjectTeams from 'sentry/views/settings/project/projectTeams';
  11. describe('ProjectTeams', function () {
  12. let org;
  13. let project;
  14. let routerContext;
  15. const team1WithAdmin = TestStubs.Team({
  16. access: ['team:read', 'team:write', 'team:admin'],
  17. });
  18. const team2WithAdmin = TestStubs.Team({
  19. id: '2',
  20. slug: 'team-slug-2',
  21. name: 'Team Name 2',
  22. hasAccess: true,
  23. access: ['team:read', 'team:write', 'team:admin'],
  24. });
  25. const team3NoAdmin = TestStubs.Team({
  26. id: '3',
  27. slug: 'team-slug-3',
  28. name: 'Team Name 3',
  29. hasAccess: true,
  30. access: ['team:read'],
  31. });
  32. beforeEach(function () {
  33. const initialData = initializeOrg();
  34. org = initialData.organization;
  35. project = {
  36. ...initialData.project,
  37. access: ['project:admin', 'project:write', 'project:admin'],
  38. };
  39. routerContext = initialData.routerContext;
  40. TeamStore.loadInitialData([team1WithAdmin, team2WithAdmin]);
  41. MockApiClient.addMockResponse({
  42. url: `/projects/${org.slug}/${project.slug}/`,
  43. method: 'GET',
  44. body: project,
  45. });
  46. MockApiClient.addMockResponse({
  47. url: `/projects/${org.slug}/${project.slug}/teams/`,
  48. method: 'GET',
  49. body: [team1WithAdmin],
  50. });
  51. MockApiClient.addMockResponse({
  52. url: `/organizations/${org.slug}/teams/`,
  53. method: 'GET',
  54. body: [team1WithAdmin, team2WithAdmin],
  55. });
  56. });
  57. afterEach(function () {
  58. MockApiClient.clearMockResponses();
  59. });
  60. it('renders', function () {
  61. const {container} = render(
  62. <ProjectTeams
  63. params={{projectId: project.slug}}
  64. organization={org}
  65. project={project}
  66. />
  67. );
  68. expect(container).toSnapshot();
  69. });
  70. it('can remove a team from project', async function () {
  71. MockApiClient.addMockResponse({
  72. url: `/projects/${org.slug}/${project.slug}/teams/`,
  73. method: 'GET',
  74. body: [team1WithAdmin, team2WithAdmin],
  75. });
  76. const endpoint1 = `/projects/${org.slug}/${project.slug}/teams/${team1WithAdmin.slug}/`;
  77. const mock1 = MockApiClient.addMockResponse({
  78. url: endpoint1,
  79. method: 'DELETE',
  80. statusCode: 200,
  81. });
  82. const endpoint2 = `/projects/${org.slug}/${project.slug}/teams/${team2WithAdmin.slug}/`;
  83. const mock2 = MockApiClient.addMockResponse({
  84. url: endpoint2,
  85. method: 'DELETE',
  86. statusCode: 200,
  87. });
  88. render(
  89. <ProjectTeams
  90. params={{projectId: project.slug}}
  91. organization={org}
  92. project={project}
  93. />
  94. );
  95. expect(mock1).not.toHaveBeenCalled();
  96. await userEvent.click(screen.getAllByRole('button', {name: 'Remove'})[0]);
  97. expect(mock1).toHaveBeenCalledWith(
  98. endpoint1,
  99. expect.objectContaining({
  100. method: 'DELETE',
  101. })
  102. );
  103. expect(screen.queryByText('#team-slug')).not.toBeInTheDocument();
  104. // Remove second team
  105. await userEvent.click(screen.getAllByRole('button', {name: 'Remove'})[0]);
  106. // Modal opens because this is the last team in project
  107. renderGlobalModal();
  108. expect(screen.getByRole('dialog')).toBeInTheDocument();
  109. await userEvent.click(screen.getByTestId('confirm-button'));
  110. expect(mock2).toHaveBeenCalledWith(
  111. endpoint2,
  112. expect.objectContaining({
  113. method: 'DELETE',
  114. })
  115. );
  116. });
  117. it('cannot remove a team without admin scopes', async function () {
  118. MockApiClient.addMockResponse({
  119. url: `/projects/${org.slug}/${project.slug}/teams/`,
  120. method: 'GET',
  121. body: [team1WithAdmin, team2WithAdmin, team3NoAdmin],
  122. });
  123. const endpoint1 = `/projects/${org.slug}/${project.slug}/teams/${team1WithAdmin.slug}/`;
  124. const mock1 = MockApiClient.addMockResponse({
  125. url: endpoint1,
  126. method: 'DELETE',
  127. statusCode: 200,
  128. });
  129. const endpoint3 = `/projects/${org.slug}/${project.slug}/teams/${team3NoAdmin.slug}/`;
  130. const mock3 = MockApiClient.addMockResponse({
  131. url: endpoint3,
  132. method: 'DELETE',
  133. statusCode: 200,
  134. });
  135. render(
  136. <ProjectTeams
  137. params={{projectId: project.slug}}
  138. organization={org}
  139. project={project}
  140. />
  141. );
  142. // Remove first team
  143. await userEvent.click(screen.getAllByRole('button', {name: 'Remove'})[0]);
  144. expect(mock1).toHaveBeenCalledWith(
  145. endpoint1,
  146. expect.objectContaining({
  147. method: 'DELETE',
  148. })
  149. );
  150. expect(screen.queryByText('#team-slug')).not.toBeInTheDocument();
  151. // Remove third team, but button should be disabled
  152. await userEvent.click(screen.getAllByRole('button', {name: 'Remove'})[1]);
  153. expect(mock3).not.toHaveBeenCalled();
  154. });
  155. it('removes team from project when project team is not in org list', async function () {
  156. MockApiClient.addMockResponse({
  157. url: `/projects/${org.slug}/${project.slug}/teams/`,
  158. method: 'GET',
  159. body: [team1WithAdmin, team2WithAdmin],
  160. });
  161. const endpoint1 = `/projects/${org.slug}/${project.slug}/teams/${team1WithAdmin.slug}/`;
  162. const mock1 = MockApiClient.addMockResponse({
  163. url: endpoint1,
  164. method: 'DELETE',
  165. });
  166. const endpoint2 = `/projects/${org.slug}/${project.slug}/teams/${team2WithAdmin.slug}/`;
  167. const mock2 = MockApiClient.addMockResponse({
  168. url: endpoint2,
  169. method: 'DELETE',
  170. });
  171. MockApiClient.addMockResponse({
  172. url: `/organizations/${org.slug}/teams/`,
  173. method: 'GET',
  174. body: [team3NoAdmin],
  175. });
  176. render(
  177. <ProjectTeams
  178. params={{projectId: project.slug}}
  179. organization={org}
  180. project={project}
  181. />
  182. );
  183. expect(mock1).not.toHaveBeenCalled();
  184. // Remove first team
  185. await userEvent.click(screen.getAllByRole('button', {name: 'Remove'})[0]);
  186. expect(mock1).toHaveBeenCalledWith(
  187. endpoint1,
  188. expect.objectContaining({
  189. method: 'DELETE',
  190. })
  191. );
  192. expect(screen.queryByText('#team-slug')).not.toBeInTheDocument();
  193. // Remove second team
  194. await userEvent.click(screen.getAllByRole('button', {name: 'Remove'})[0]);
  195. // Modal opens because this is the last team in project
  196. renderGlobalModal();
  197. expect(screen.getByRole('dialog')).toBeInTheDocument();
  198. // Click confirm
  199. await userEvent.click(screen.getByTestId('confirm-button'));
  200. expect(mock2).toHaveBeenCalledWith(
  201. endpoint2,
  202. expect.objectContaining({
  203. method: 'DELETE',
  204. })
  205. );
  206. });
  207. it('can associate a team with project', async function () {
  208. const endpoint = `/projects/${org.slug}/${project.slug}/teams/${team2WithAdmin.slug}/`;
  209. const mock = MockApiClient.addMockResponse({
  210. url: endpoint,
  211. method: 'POST',
  212. statusCode: 200,
  213. });
  214. render(
  215. <ProjectTeams
  216. params={{projectId: project.slug}}
  217. organization={org}
  218. project={project}
  219. />
  220. );
  221. expect(mock).not.toHaveBeenCalled();
  222. // Add a team
  223. await userEvent.click(screen.getAllByRole('button', {name: 'Add Team'})[1]);
  224. await userEvent.click(screen.getByText('#team-slug-2'));
  225. expect(mock).toHaveBeenCalledWith(
  226. endpoint,
  227. expect.objectContaining({
  228. method: 'POST',
  229. })
  230. );
  231. });
  232. it('creates a new team adds it to current project using the "create team modal" in dropdown', async function () {
  233. MockApiClient.addMockResponse({
  234. url: '/internal/health/',
  235. body: {},
  236. });
  237. MockApiClient.addMockResponse({
  238. url: '/assistant/',
  239. body: {},
  240. });
  241. MockApiClient.addMockResponse({
  242. url: '/organizations/',
  243. body: [org],
  244. });
  245. const addTeamToProject = MockApiClient.addMockResponse({
  246. url: `/projects/${org.slug}/${project.slug}/teams/new-team/`,
  247. method: 'POST',
  248. });
  249. const createTeam = MockApiClient.addMockResponse({
  250. url: `/organizations/${org.slug}/teams/`,
  251. method: 'POST',
  252. body: {slug: 'new-team'},
  253. });
  254. render(
  255. <ProjectTeams
  256. params={{projectId: project.slug}}
  257. project={project}
  258. organization={org}
  259. />,
  260. {context: routerContext}
  261. );
  262. // Add new team
  263. await userEvent.click(screen.getAllByRole('button', {name: 'Add Team'})[1]);
  264. // XXX(epurkhiser): Create Team should really be a button
  265. await userEvent.click(screen.getByRole('link', {name: 'Create Team'}));
  266. renderGlobalModal();
  267. await screen.findByRole('dialog');
  268. await userEvent.type(screen.getByRole('textbox', {name: 'Team Name'}), 'new-team');
  269. await userEvent.click(screen.getByRole('button', {name: 'Create Team'}));
  270. await waitFor(() => expect(createTeam).toHaveBeenCalledTimes(1));
  271. expect(createTeam).toHaveBeenCalledWith(
  272. '/organizations/org-slug/teams/',
  273. expect.objectContaining({
  274. data: {slug: 'new-team'},
  275. })
  276. );
  277. expect(addTeamToProject).toHaveBeenCalledTimes(1);
  278. expect(addTeamToProject).toHaveBeenCalledWith(
  279. '/projects/org-slug/project-slug/teams/new-team/',
  280. expect.anything()
  281. );
  282. });
  283. });