projectTeams.spec.tsx 9.3 KB

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