organizationTeams.spec.jsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  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. requester: {},
  173. });
  174. const requester = TestStubs.User({
  175. id: '9',
  176. username: 'requester@example.com',
  177. email: 'requester@example.com',
  178. name: 'Requester',
  179. });
  180. const requestList = [accessRequest, TestStubs.AccessRequest({id: '4', requester})];
  181. const createWrapper = props =>
  182. render(
  183. <OrganizationTeams
  184. params={{projectId: project.slug}}
  185. routes={[]}
  186. features={new Set([])}
  187. access={new Set([])}
  188. allTeams={[]}
  189. activeTeams={[]}
  190. organization={organization}
  191. requestList={requestList}
  192. {...props}
  193. />
  194. );
  195. it('renders team request panel', function () {
  196. createWrapper();
  197. expect(screen.getByText('Pending Team Requests')).toBeInTheDocument();
  198. expect(screen.queryAllByTestId('request-message')).toHaveLength(2);
  199. expect(screen.queryAllByTestId('request-message')[0]).toHaveTextContent(
  200. `${accessRequest.member.user.name} requests access to the #${accessRequest.team.slug} team`
  201. );
  202. });
  203. it('can approve', async function () {
  204. const onUpdateRequestListMock = jest.fn();
  205. const approveMock = MockApiClient.addMockResponse({
  206. url: `/organizations/${orgId}/access-requests/${accessRequest.id}/`,
  207. method: 'PUT',
  208. });
  209. createWrapper({
  210. onRemoveAccessRequest: onUpdateRequestListMock,
  211. });
  212. await userEvent.click(screen.getAllByLabelText('Approve')[0]);
  213. await tick();
  214. expect(approveMock).toHaveBeenCalledWith(
  215. expect.anything(),
  216. expect.objectContaining({
  217. data: {
  218. isApproved: true,
  219. },
  220. })
  221. );
  222. expect(onUpdateRequestListMock).toHaveBeenCalledWith(accessRequest.id, true);
  223. });
  224. it('can deny', async function () {
  225. const onUpdateRequestListMock = jest.fn();
  226. const denyMock = MockApiClient.addMockResponse({
  227. url: `/organizations/${orgId}/access-requests/${accessRequest.id}/`,
  228. method: 'PUT',
  229. });
  230. createWrapper({
  231. onRemoveAccessRequest: onUpdateRequestListMock,
  232. });
  233. await userEvent.click(screen.getAllByLabelText('Deny')[0]);
  234. await tick();
  235. expect(denyMock).toHaveBeenCalledWith(
  236. expect.anything(),
  237. expect.objectContaining({
  238. data: {
  239. isApproved: false,
  240. },
  241. })
  242. );
  243. expect(onUpdateRequestListMock).toHaveBeenCalledWith(accessRequest.id, false);
  244. });
  245. });
  246. describe('Team Roles', function () {
  247. const features = new Set(['team-roles']);
  248. const access = new Set();
  249. it('does not render alert without feature flag', function () {
  250. const {organization, project} = initializeOrg({organization: {orgRole: 'admin'}});
  251. render(
  252. <OrganizationTeams
  253. params={{projectId: project.slug}}
  254. routes={[]}
  255. features={new Set()}
  256. access={access}
  257. organization={organization}
  258. />
  259. );
  260. expect(screen.queryByText('a minimum team-level role of')).not.toBeInTheDocument();
  261. });
  262. it('renders alert with elevated org role', function () {
  263. const {organization, project} = initializeOrg({organization: {orgRole: 'admin'}});
  264. render(
  265. <OrganizationTeams
  266. params={{projectId: project.slug}}
  267. routes={[]}
  268. features={features}
  269. access={access}
  270. organization={organization}
  271. />
  272. );
  273. expect(
  274. // Text broken up by styles
  275. screen.getByText(
  276. 'Your organization role as an has granted you a minimum team-level role of'
  277. )
  278. ).toBeInTheDocument();
  279. });
  280. it('does not render alert with lowest org role', function () {
  281. const {organization, project} = initializeOrg({organization: {orgRole: 'member'}});
  282. render(
  283. <OrganizationTeams
  284. params={{projectId: project.slug}}
  285. routes={[]}
  286. features={features}
  287. access={access}
  288. organization={organization}
  289. />
  290. );
  291. expect(screen.queryByText('a minimum team-level role of')).not.toBeInTheDocument();
  292. });
  293. });
  294. });