organizationTeams.spec.jsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  1. import {initializeOrg} from 'sentry-test/initializeOrg';
  2. import {act, render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary';
  3. import {openCreateTeamModal} from 'sentry/actionCreators/modal';
  4. import TeamStore from 'sentry/stores/teamStore';
  5. import recreateRoute from 'sentry/utils/recreateRoute';
  6. import OrganizationTeams from 'sentry/views/settings/organizationTeams/organizationTeams';
  7. recreateRoute.mockReturnValue('');
  8. jest.mock('sentry/actionCreators/modal', () => ({
  9. openCreateTeamModal: jest.fn(),
  10. }));
  11. describe('OrganizationTeams', function () {
  12. describe('Open Membership', function () {
  13. const {organization, project} = initializeOrg({
  14. organization: {
  15. openMembership: true,
  16. },
  17. });
  18. const createWrapper = props =>
  19. render(
  20. <OrganizationTeams
  21. params={{projectId: project.slug}}
  22. routes={[]}
  23. features={new Set(['open-membership'])}
  24. access={new Set(['project:admin'])}
  25. organization={organization}
  26. {...props}
  27. />
  28. );
  29. it('opens "create team modal" when creating a new team from header', async function () {
  30. createWrapper();
  31. // Click "Create Team" in Panel Header
  32. await userEvent.click(screen.getByLabelText('Create Team'));
  33. // action creator to open "create team modal" is called
  34. expect(openCreateTeamModal).toHaveBeenCalledWith(
  35. expect.objectContaining({
  36. organization: expect.objectContaining({
  37. slug: organization.slug,
  38. }),
  39. })
  40. );
  41. });
  42. it('can join team and have link to details', function () {
  43. const mockTeams = [
  44. TestStubs.Team({
  45. hasAccess: true,
  46. isMember: false,
  47. }),
  48. ];
  49. act(() => void TeamStore.loadInitialData(mockTeams, false, null));
  50. createWrapper({
  51. access: new Set([]),
  52. });
  53. expect(screen.getByLabelText('Join Team')).toBeInTheDocument();
  54. // Should also link to details
  55. expect(screen.getByTestId('team-link')).toBeInTheDocument();
  56. });
  57. it('reloads projects after joining a team', async function () {
  58. const team = TestStubs.Team({
  59. hasAccess: true,
  60. isMember: false,
  61. });
  62. const getOrgMock = MockApiClient.addMockResponse({
  63. url: '/organizations/org-slug/',
  64. body: TestStubs.Organization(),
  65. });
  66. MockApiClient.addMockResponse({
  67. url: `/organizations/org-slug/members/me/teams/${team.slug}/`,
  68. method: 'POST',
  69. body: {...team, isMember: true},
  70. });
  71. const mockTeams = [team];
  72. act(() => void TeamStore.loadInitialData(mockTeams, false, null));
  73. createWrapper({access: new Set([])});
  74. await userEvent.click(screen.getByLabelText('Join Team'));
  75. await waitFor(() => {
  76. expect(getOrgMock).toHaveBeenCalledTimes(1);
  77. });
  78. });
  79. it('cannot leave idp-provisioned team', function () {
  80. const mockTeams = [
  81. TestStubs.Team({flags: {'idp:provisioned': true}, isMember: true}),
  82. ];
  83. act(() => void TeamStore.loadInitialData(mockTeams, false, null));
  84. createWrapper();
  85. expect(screen.getByRole('button', {name: 'Leave Team'})).toBeDisabled();
  86. });
  87. it('cannot join idp-provisioned team', function () {
  88. const mockTeams = [
  89. TestStubs.Team({flags: {'idp:provisioned': true}, isMember: false}),
  90. ];
  91. act(() => void TeamStore.loadInitialData(mockTeams, false, null));
  92. createWrapper({
  93. access: new Set([]),
  94. });
  95. expect(screen.getByRole('button', {name: 'Join Team'})).toBeDisabled();
  96. });
  97. });
  98. describe('Closed Membership', function () {
  99. const {organization, project} = initializeOrg({
  100. organization: {
  101. openMembership: false,
  102. },
  103. });
  104. const createWrapper = props =>
  105. render(
  106. <OrganizationTeams
  107. params={{projectId: project.slug}}
  108. routes={[]}
  109. features={new Set([])}
  110. access={new Set([])}
  111. allTeams={[]}
  112. activeTeams={[]}
  113. organization={organization}
  114. {...props}
  115. />
  116. );
  117. it('can request access to team and does not have link to details', function () {
  118. const mockTeams = [
  119. TestStubs.Team({
  120. hasAccess: false,
  121. isMember: false,
  122. }),
  123. ];
  124. act(() => void TeamStore.loadInitialData(mockTeams, false, null));
  125. createWrapper({access: new Set([])});
  126. expect(screen.getByLabelText('Request Access')).toBeInTheDocument();
  127. // Should also not link to details because of lack of access
  128. expect(screen.queryByTestId('team-link')).not.toBeInTheDocument();
  129. });
  130. it('can leave team when you are a member', function () {
  131. const mockTeams = [
  132. TestStubs.Team({
  133. hasAccess: true,
  134. isMember: true,
  135. }),
  136. ];
  137. act(() => void TeamStore.loadInitialData(mockTeams, false, null));
  138. createWrapper({
  139. access: new Set([]),
  140. });
  141. expect(screen.getByLabelText('Leave Team')).toBeInTheDocument();
  142. });
  143. it('cannot request to join idp-provisioned team', function () {
  144. const mockTeams = [
  145. TestStubs.Team({flags: {'idp:provisioned': true}, isMember: false}),
  146. ];
  147. act(() => void TeamStore.loadInitialData(mockTeams, false, null));
  148. createWrapper({
  149. access: new Set([]),
  150. });
  151. expect(screen.getByRole('button', {name: 'Request Access'})).toBeDisabled();
  152. });
  153. it('cannot leave idp-provisioned team', function () {
  154. const mockTeams = [
  155. TestStubs.Team({flags: {'idp:provisioned': true}, isMember: true}),
  156. ];
  157. act(() => void TeamStore.loadInitialData(mockTeams, false, null));
  158. createWrapper({
  159. access: new Set([]),
  160. });
  161. expect(screen.getByRole('button', {name: 'Leave Team'})).toBeDisabled();
  162. });
  163. });
  164. describe('Team Requests', function () {
  165. const {organization, project} = initializeOrg({
  166. organization: {
  167. openMembership: false,
  168. },
  169. });
  170. const orgId = organization.slug;
  171. const accessRequest = TestStubs.AccessRequest();
  172. const requester = TestStubs.User({
  173. id: '9',
  174. username: 'requester@example.com',
  175. email: 'requester@example.com',
  176. name: 'Requester',
  177. });
  178. const requestList = [accessRequest, TestStubs.AccessRequest({id: '4', requester})];
  179. const createWrapper = props =>
  180. render(
  181. <OrganizationTeams
  182. params={{projectId: project.slug}}
  183. routes={[]}
  184. features={new Set([])}
  185. access={new Set([])}
  186. allTeams={[]}
  187. activeTeams={[]}
  188. organization={organization}
  189. requestList={requestList}
  190. {...props}
  191. />
  192. );
  193. it('renders team request panel', function () {
  194. createWrapper();
  195. expect(screen.getByText('Pending Team Requests')).toBeInTheDocument();
  196. expect(screen.queryAllByTestId('request-message')).toHaveLength(2);
  197. expect(screen.queryAllByTestId('request-message')[0]).toHaveTextContent(
  198. `${accessRequest.member.user.name} requests access to the #${accessRequest.team.slug} team`
  199. );
  200. });
  201. it('can approve', async function () {
  202. const onUpdateRequestListMock = jest.fn();
  203. const approveMock = MockApiClient.addMockResponse({
  204. url: `/organizations/${orgId}/access-requests/${accessRequest.id}/`,
  205. method: 'PUT',
  206. });
  207. createWrapper({
  208. onRemoveAccessRequest: onUpdateRequestListMock,
  209. });
  210. await userEvent.click(screen.getAllByLabelText('Approve')[0]);
  211. await tick();
  212. expect(approveMock).toHaveBeenCalledWith(
  213. expect.anything(),
  214. expect.objectContaining({
  215. data: {
  216. isApproved: true,
  217. },
  218. })
  219. );
  220. expect(onUpdateRequestListMock).toHaveBeenCalledWith(accessRequest.id, true);
  221. });
  222. it('can deny', async function () {
  223. const onUpdateRequestListMock = jest.fn();
  224. const denyMock = MockApiClient.addMockResponse({
  225. url: `/organizations/${orgId}/access-requests/${accessRequest.id}/`,
  226. method: 'PUT',
  227. });
  228. createWrapper({
  229. onRemoveAccessRequest: onUpdateRequestListMock,
  230. });
  231. await userEvent.click(screen.getAllByLabelText('Deny')[0]);
  232. await tick();
  233. expect(denyMock).toHaveBeenCalledWith(
  234. expect.anything(),
  235. expect.objectContaining({
  236. data: {
  237. isApproved: false,
  238. },
  239. })
  240. );
  241. expect(onUpdateRequestListMock).toHaveBeenCalledWith(accessRequest.id, false);
  242. });
  243. });
  244. describe('Team Roles', function () {
  245. const features = new Set(['team-roles']);
  246. const access = new Set();
  247. it('does not render alert without feature flag', function () {
  248. const {organization, project} = initializeOrg({organization: {orgRole: 'admin'}});
  249. render(
  250. <OrganizationTeams
  251. params={{projectId: project.slug}}
  252. routes={[]}
  253. features={new Set()}
  254. access={access}
  255. organization={organization}
  256. />
  257. );
  258. expect(screen.queryByText('a minimum team-level role of')).not.toBeInTheDocument();
  259. });
  260. it('renders alert with elevated org role', function () {
  261. const {organization, project} = initializeOrg({organization: {orgRole: 'admin'}});
  262. render(
  263. <OrganizationTeams
  264. params={{projectId: project.slug}}
  265. routes={[]}
  266. features={features}
  267. access={access}
  268. organization={organization}
  269. />
  270. );
  271. expect(
  272. // Text broken up by styles
  273. screen.getByText(
  274. 'Your organization role as an has granted you a minimum team-level role of'
  275. )
  276. ).toBeInTheDocument();
  277. });
  278. it('does not render alert with lowest org role', function () {
  279. const {organization, project} = initializeOrg({organization: {orgRole: 'member'}});
  280. render(
  281. <OrganizationTeams
  282. params={{projectId: project.slug}}
  283. routes={[]}
  284. features={features}
  285. access={access}
  286. organization={organization}
  287. />
  288. );
  289. expect(screen.queryByText('a minimum team-level role of')).not.toBeInTheDocument();
  290. });
  291. });
  292. });