teamMembers.spec.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334
  1. import {MemberFixture} from 'sentry-fixture/member';
  2. import {MembersFixture} from 'sentry-fixture/members';
  3. import {OrganizationFixture} from 'sentry-fixture/organization';
  4. import {RouterFixture} from 'sentry-fixture/routerFixture';
  5. import {TeamFixture} from 'sentry-fixture/team';
  6. import {initializeOrg} from 'sentry-test/initializeOrg';
  7. import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
  8. import {
  9. openInviteMembersModal,
  10. openTeamAccessRequestModal,
  11. } from 'sentry/actionCreators/modal';
  12. import TeamMembers from 'sentry/views/settings/organizationTeams/teamMembers';
  13. jest.mock('sentry/actionCreators/modal', () => ({
  14. openInviteMembersModal: jest.fn(),
  15. openTeamAccessRequestModal: jest.fn(),
  16. }));
  17. describe('TeamMembers', function () {
  18. let createMock: jest.Mock;
  19. const organization = OrganizationFixture();
  20. const team = TeamFixture();
  21. const managerTeam = TeamFixture();
  22. const members = MembersFixture();
  23. const member = MemberFixture({
  24. id: '9',
  25. email: 'sentry9@test.com',
  26. name: 'Sentry 9 Name',
  27. });
  28. const router = RouterFixture({
  29. params: {
  30. orgId: organization.slug,
  31. teamId: team.slug,
  32. },
  33. });
  34. beforeEach(function () {
  35. MockApiClient.clearMockResponses();
  36. MockApiClient.addMockResponse({
  37. url: `/organizations/${organization.slug}/members/`,
  38. method: 'GET',
  39. body: [member],
  40. });
  41. MockApiClient.addMockResponse({
  42. url: `/teams/${organization.slug}/${team.slug}/members/`,
  43. method: 'GET',
  44. body: members,
  45. });
  46. MockApiClient.addMockResponse({
  47. url: `/teams/${organization.slug}/${team.slug}/`,
  48. method: 'GET',
  49. body: team,
  50. });
  51. MockApiClient.addMockResponse({
  52. url: `/teams/${organization.slug}/${managerTeam.slug}/`,
  53. method: 'GET',
  54. body: managerTeam,
  55. });
  56. createMock = MockApiClient.addMockResponse({
  57. url: `/organizations/${organization.slug}/members/${member.id}/teams/${team.slug}/`,
  58. method: 'POST',
  59. });
  60. });
  61. it('can add member to team with open membership', async function () {
  62. const org = OrganizationFixture({access: [], openMembership: true});
  63. render(<TeamMembers team={team} />, {router, organization: org});
  64. await userEvent.click(
  65. (await screen.findAllByRole('button', {name: 'Add Member'}))[0]!
  66. );
  67. await userEvent.click(screen.getAllByTestId('letter_avatar-avatar')[0]!);
  68. expect(createMock).toHaveBeenCalled();
  69. });
  70. it('can add multiple members with one click on dropdown', async function () {
  71. const org = OrganizationFixture({access: [], openMembership: true});
  72. render(<TeamMembers team={team} />, {router, organization: org});
  73. await userEvent.click(
  74. (await screen.findAllByRole('button', {name: 'Add Member'}))[0]!
  75. );
  76. await userEvent.click(screen.getAllByTestId('letter_avatar-avatar')[0]!);
  77. expect(createMock).toHaveBeenCalled();
  78. expect(screen.getAllByTestId('add-member-menu')[0]).toBeVisible();
  79. });
  80. it('can add member to team with team:admin permission', async function () {
  81. const org = OrganizationFixture({access: ['team:admin'], openMembership: false});
  82. render(<TeamMembers team={team} />, {router, organization: org});
  83. await userEvent.click(
  84. (await screen.findAllByRole('button', {name: 'Add Member'}))[0]!
  85. );
  86. await userEvent.click(screen.getAllByTestId('letter_avatar-avatar')[0]!);
  87. expect(createMock).toHaveBeenCalled();
  88. });
  89. it('can add member to team with org:write permission', async function () {
  90. const org = OrganizationFixture({access: ['org:write'], openMembership: false});
  91. render(<TeamMembers team={team} />, {router, organization: org});
  92. await userEvent.click(
  93. (await screen.findAllByRole('button', {name: 'Add Member'}))[0]!
  94. );
  95. await userEvent.click(screen.getAllByTestId('letter_avatar-avatar')[0]!);
  96. expect(createMock).toHaveBeenCalled();
  97. });
  98. it('can request access to add member to team without permission', async function () {
  99. const org = OrganizationFixture({access: [], openMembership: false});
  100. render(<TeamMembers team={team} />, {router, organization: org});
  101. await userEvent.click(
  102. (await screen.findAllByRole('button', {name: 'Add Member'}))[0]!
  103. );
  104. await userEvent.click(screen.getAllByTestId('letter_avatar-avatar')[0]!);
  105. expect(openTeamAccessRequestModal).toHaveBeenCalled();
  106. });
  107. it('can invite member from team dropdown with access', async function () {
  108. const {organization: org} = initializeOrg({
  109. organization: OrganizationFixture({
  110. access: ['team:admin'],
  111. openMembership: false,
  112. }),
  113. });
  114. render(<TeamMembers team={team} />, {router, organization: org});
  115. await userEvent.click(
  116. (await screen.findAllByRole('button', {name: 'Add Member'}))[0]!
  117. );
  118. await userEvent.click(screen.getByTestId('invite-member'));
  119. expect(openInviteMembersModal).toHaveBeenCalled();
  120. });
  121. it('can invite member from team dropdown with access and `Open Membership` enabled', async function () {
  122. const {organization: org} = initializeOrg({
  123. organization: OrganizationFixture({
  124. access: ['team:admin'],
  125. openMembership: true,
  126. }),
  127. });
  128. render(<TeamMembers team={team} />, {router, organization: org});
  129. await userEvent.click(
  130. (await screen.findAllByRole('button', {name: 'Add Member'}))[0]!
  131. );
  132. await userEvent.click(screen.getByTestId('invite-member'));
  133. expect(openInviteMembersModal).toHaveBeenCalled();
  134. });
  135. it('can invite member from team dropdown without access and `Open Membership` enabled', async function () {
  136. const {organization: org} = initializeOrg({
  137. organization: OrganizationFixture({access: [], openMembership: true}),
  138. });
  139. render(<TeamMembers team={team} />, {router, organization: org});
  140. await userEvent.click(
  141. (await screen.findAllByRole('button', {name: 'Add Member'}))[0]!
  142. );
  143. await userEvent.click(screen.getByTestId('invite-member'));
  144. expect(openInviteMembersModal).toHaveBeenCalled();
  145. });
  146. it('can invite member from team dropdown without access and `Open Membership` disabled', async function () {
  147. const {organization: org} = initializeOrg({
  148. organization: OrganizationFixture({access: [], openMembership: false}),
  149. });
  150. render(<TeamMembers team={team} />, {router, organization: org});
  151. await userEvent.click(
  152. (await screen.findAllByRole('button', {name: 'Add Member'}))[0]!
  153. );
  154. await userEvent.click(screen.getByTestId('invite-member'));
  155. expect(openInviteMembersModal).toHaveBeenCalled();
  156. });
  157. it('can remove member from team', async function () {
  158. const deleteMock = MockApiClient.addMockResponse({
  159. url: `/organizations/${organization.slug}/members/${members[0]!.id}/teams/${team.slug}/`,
  160. method: 'DELETE',
  161. });
  162. render(<TeamMembers team={team} />, {router, organization});
  163. await screen.findAllByRole('button', {name: 'Add Member'});
  164. expect(deleteMock).not.toHaveBeenCalled();
  165. await userEvent.click(screen.getAllByRole('button', {name: 'Remove'})[0]!);
  166. expect(deleteMock).toHaveBeenCalled();
  167. });
  168. it('can only remove self from team', async function () {
  169. const me = MemberFixture({
  170. id: '123',
  171. email: 'foo@example.com',
  172. });
  173. MockApiClient.addMockResponse({
  174. url: `/teams/${organization.slug}/${team.slug}/members/`,
  175. method: 'GET',
  176. body: [...members, me],
  177. });
  178. const deleteMock = MockApiClient.addMockResponse({
  179. url: `/organizations/${organization.slug}/members/${me.id}/teams/${team.slug}/`,
  180. method: 'DELETE',
  181. });
  182. const organizationMember = OrganizationFixture({access: []});
  183. render(<TeamMembers team={team} />, {router, organization: organizationMember});
  184. await screen.findAllByRole('button', {name: 'Add Member'});
  185. expect(deleteMock).not.toHaveBeenCalled();
  186. expect(screen.getAllByTestId('letter_avatar-avatar')).toHaveLength(
  187. members.length + 1
  188. );
  189. // Can only remove self
  190. expect(screen.getByRole('button', {name: 'Leave'})).toBeInTheDocument();
  191. await userEvent.click(screen.getByRole('button', {name: 'Leave'}));
  192. expect(deleteMock).toHaveBeenCalled();
  193. });
  194. it('renders team-level roles without flag', async function () {
  195. const owner = MemberFixture({
  196. id: '123',
  197. email: 'foo@example.com',
  198. orgRole: 'owner',
  199. role: 'owner',
  200. });
  201. MockApiClient.addMockResponse({
  202. url: `/teams/${organization.slug}/${team.slug}/members/`,
  203. method: 'GET',
  204. body: [...members, owner],
  205. });
  206. render(<TeamMembers team={team} />, {router, organization});
  207. const admins = await screen.findAllByText('Team Admin');
  208. expect(admins).toHaveLength(3);
  209. const contributors = screen.queryAllByText('Contributor');
  210. expect(contributors).toHaveLength(2);
  211. });
  212. it('renders team-level roles with flag', async function () {
  213. const manager = MemberFixture({
  214. id: '123',
  215. email: 'foo@example.com',
  216. orgRole: 'manager',
  217. role: 'manager',
  218. });
  219. MockApiClient.addMockResponse({
  220. url: `/teams/${organization.slug}/${team.slug}/members/`,
  221. method: 'GET',
  222. body: [...members, manager],
  223. });
  224. const orgWithTeamRoles = OrganizationFixture({features: ['team-roles']});
  225. render(<TeamMembers team={team} />, {router, organization: orgWithTeamRoles});
  226. const admins = await screen.findAllByText('Team Admin');
  227. expect(admins).toHaveLength(3);
  228. const contributors = screen.queryAllByText('Contributor');
  229. expect(contributors).toHaveLength(2);
  230. });
  231. it('cannot add or remove members if team is idp:provisioned', async function () {
  232. const team2 = TeamFixture({
  233. flags: {
  234. 'idp:provisioned': true,
  235. },
  236. });
  237. const me = MemberFixture({
  238. id: '123',
  239. email: 'foo@example.com',
  240. role: 'owner',
  241. flags: {
  242. 'idp:provisioned': true,
  243. 'idp:role-restricted': false,
  244. 'member-limit:restricted': false,
  245. 'partnership:restricted': false,
  246. 'sso:invalid': false,
  247. 'sso:linked': false,
  248. },
  249. });
  250. const idpMembers = members.map(teamMember => ({
  251. ...teamMember,
  252. flags: {...teamMember.flags, 'idp:provisioned': true},
  253. }));
  254. MockApiClient.clearMockResponses();
  255. MockApiClient.addMockResponse({
  256. url: `/organizations/${organization.slug}/members/`,
  257. method: 'GET',
  258. body: [...idpMembers, me],
  259. });
  260. MockApiClient.addMockResponse({
  261. url: `/teams/${organization.slug}/${team2.slug}/members/`,
  262. method: 'GET',
  263. body: idpMembers,
  264. });
  265. MockApiClient.addMockResponse({
  266. url: `/teams/${organization.slug}/${team2.slug}/`,
  267. method: 'GET',
  268. body: team2,
  269. });
  270. render(<TeamMembers team={team2} />, {router, organization});
  271. expect(
  272. (await screen.findAllByRole('button', {name: 'Add Member'})).at(1)
  273. ).toBeDisabled();
  274. expect((await screen.findAllByRole('button', {name: 'Remove'})).at(0)).toBeDisabled();
  275. });
  276. });