organizationTeams.spec.tsx 11 KB

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