teamMembers.spec.jsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483
  1. import {initializeOrg} from 'sentry-test/initializeOrg';
  2. import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary';
  3. import {
  4. openInviteMembersModal,
  5. openTeamAccessRequestModal,
  6. } from 'sentry/actionCreators/modal';
  7. import {Client} from 'sentry/api';
  8. import TeamMembers from 'sentry/views/settings/organizationTeams/teamMembers';
  9. jest.mock('sentry/actionCreators/modal', () => ({
  10. openInviteMembersModal: jest.fn(),
  11. openTeamAccessRequestModal: jest.fn(),
  12. }));
  13. describe('TeamMembers', function () {
  14. let createMock;
  15. const organization = TestStubs.Organization();
  16. const team = TestStubs.Team();
  17. const managerTeam = TestStubs.Team({orgRole: 'manager'});
  18. const members = TestStubs.Members();
  19. const member = TestStubs.Member({
  20. id: '9',
  21. email: 'sentry9@test.com',
  22. name: 'Sentry 9 Name',
  23. });
  24. beforeEach(function () {
  25. Client.clearMockResponses();
  26. Client.addMockResponse({
  27. url: `/organizations/${organization.slug}/members/`,
  28. method: 'GET',
  29. body: [member],
  30. });
  31. Client.addMockResponse({
  32. url: `/teams/${organization.slug}/${team.slug}/members/`,
  33. method: 'GET',
  34. body: members,
  35. });
  36. Client.addMockResponse({
  37. url: `/teams/${organization.slug}/${team.slug}/`,
  38. method: 'GET',
  39. body: team,
  40. });
  41. Client.addMockResponse({
  42. url: `/teams/${organization.slug}/${managerTeam.slug}/`,
  43. method: 'GET',
  44. body: managerTeam,
  45. });
  46. createMock = Client.addMockResponse({
  47. url: `/organizations/${organization.slug}/members/${member.id}/teams/${team.slug}/`,
  48. method: 'POST',
  49. });
  50. });
  51. it('can add member to team with open membership', async function () {
  52. const org = TestStubs.Organization({access: [], openMembership: true});
  53. render(
  54. <TeamMembers
  55. params={{orgId: org.slug, teamId: team.slug}}
  56. organization={org}
  57. team={team}
  58. />
  59. );
  60. await userEvent.click(
  61. (
  62. await screen.findAllByRole('button', {name: 'Add Member'})
  63. )[0]
  64. );
  65. await userEvent.click(screen.getAllByTestId('letter_avatar-avatar')[0]);
  66. expect(createMock).toHaveBeenCalled();
  67. });
  68. it('can add multiple members with one click on dropdown', async function () {
  69. const org = TestStubs.Organization({access: [], openMembership: true});
  70. render(
  71. <TeamMembers
  72. params={{orgId: org.slug, teamId: team.slug}}
  73. organization={org}
  74. team={team}
  75. />
  76. );
  77. await userEvent.click(
  78. (
  79. await screen.findAllByRole('button', {name: 'Add Member'})
  80. )[0]
  81. );
  82. await userEvent.click(screen.getAllByTestId('letter_avatar-avatar')[0]);
  83. expect(createMock).toHaveBeenCalled();
  84. expect(screen.getAllByTestId('add-member-menu')[0]).toBeVisible();
  85. });
  86. it('can add member to team with team:admin permission', async function () {
  87. const org = TestStubs.Organization({access: ['team:admin'], openMembership: false});
  88. render(
  89. <TeamMembers
  90. params={{orgId: org.slug, teamId: team.slug}}
  91. organization={org}
  92. team={team}
  93. />
  94. );
  95. await userEvent.click(
  96. (
  97. await screen.findAllByRole('button', {name: 'Add Member'})
  98. )[0]
  99. );
  100. await userEvent.click(screen.getAllByTestId('letter_avatar-avatar')[0]);
  101. expect(createMock).toHaveBeenCalled();
  102. });
  103. it('can add member to team with org:write permission', async function () {
  104. const org = TestStubs.Organization({access: ['org:write'], openMembership: false});
  105. render(
  106. <TeamMembers
  107. params={{orgId: org.slug, teamId: team.slug}}
  108. organization={org}
  109. team={team}
  110. />
  111. );
  112. await userEvent.click(
  113. (
  114. await screen.findAllByRole('button', {name: 'Add Member'})
  115. )[0]
  116. );
  117. await userEvent.click(screen.getAllByTestId('letter_avatar-avatar')[0]);
  118. expect(createMock).toHaveBeenCalled();
  119. });
  120. it('can request access to add member to team without permission', async function () {
  121. const org = TestStubs.Organization({access: [], openMembership: false});
  122. render(
  123. <TeamMembers
  124. params={{orgId: org.slug, teamId: team.slug}}
  125. organization={org}
  126. team={team}
  127. />
  128. );
  129. await userEvent.click(
  130. (
  131. await screen.findAllByRole('button', {name: 'Add Member'})
  132. )[0]
  133. );
  134. await userEvent.click(screen.getAllByTestId('letter_avatar-avatar')[0]);
  135. expect(openTeamAccessRequestModal).toHaveBeenCalled();
  136. });
  137. it('can invite member from team dropdown with access', async function () {
  138. const {organization: org, routerContext} = initializeOrg({
  139. organization: TestStubs.Organization({
  140. access: ['team:admin'],
  141. openMembership: false,
  142. }),
  143. });
  144. render(
  145. <TeamMembers
  146. params={{orgId: org.slug, teamId: team.slug}}
  147. organization={org}
  148. team={team}
  149. />,
  150. {context: routerContext}
  151. );
  152. await userEvent.click(
  153. (
  154. await screen.findAllByRole('button', {name: 'Add Member'})
  155. )[0]
  156. );
  157. await userEvent.click(screen.getByTestId('invite-member'));
  158. expect(openInviteMembersModal).toHaveBeenCalled();
  159. });
  160. it('can invite member from team dropdown with access and `Open Membership` enabled', async function () {
  161. const {organization: org, routerContext} = initializeOrg({
  162. organization: TestStubs.Organization({
  163. access: ['team:admin'],
  164. openMembership: true,
  165. }),
  166. });
  167. render(
  168. <TeamMembers
  169. params={{orgId: org.slug, teamId: team.slug}}
  170. organization={org}
  171. team={team}
  172. />,
  173. {context: routerContext}
  174. );
  175. await userEvent.click(
  176. (
  177. await screen.findAllByRole('button', {name: 'Add Member'})
  178. )[0]
  179. );
  180. await userEvent.click(screen.getByTestId('invite-member'));
  181. expect(openInviteMembersModal).toHaveBeenCalled();
  182. });
  183. it('can invite member from team dropdown without access and `Open Membership` enabled', async function () {
  184. const {organization: org, routerContext} = initializeOrg({
  185. organization: TestStubs.Organization({access: [], openMembership: true}),
  186. });
  187. render(
  188. <TeamMembers
  189. params={{orgId: org.slug, teamId: team.slug}}
  190. organization={org}
  191. team={team}
  192. />,
  193. {context: routerContext}
  194. );
  195. await userEvent.click(
  196. (
  197. await screen.findAllByRole('button', {name: 'Add Member'})
  198. )[0]
  199. );
  200. await userEvent.click(screen.getByTestId('invite-member'));
  201. expect(openInviteMembersModal).toHaveBeenCalled();
  202. });
  203. it('can invite member from team dropdown without access and `Open Membership` disabled', async function () {
  204. const {organization: org, routerContext} = initializeOrg({
  205. organization: TestStubs.Organization({access: [], openMembership: false}),
  206. });
  207. render(
  208. <TeamMembers
  209. params={{orgId: org.slug, teamId: team.slug}}
  210. organization={org}
  211. team={team}
  212. />,
  213. {context: routerContext}
  214. );
  215. await userEvent.click(
  216. (
  217. await screen.findAllByRole('button', {name: 'Add Member'})
  218. )[0]
  219. );
  220. await userEvent.click(screen.getByTestId('invite-member'));
  221. expect(openInviteMembersModal).toHaveBeenCalled();
  222. });
  223. it('can remove member from team', async function () {
  224. const deleteMock = Client.addMockResponse({
  225. url: `/organizations/${organization.slug}/members/${members[0].id}/teams/${team.slug}/`,
  226. method: 'DELETE',
  227. });
  228. render(
  229. <TeamMembers
  230. params={{orgId: organization.slug, teamId: team.slug}}
  231. organization={organization}
  232. team={team}
  233. />
  234. );
  235. await screen.findAllByRole('button', {name: 'Add Member'});
  236. expect(deleteMock).not.toHaveBeenCalled();
  237. await userEvent.click(screen.getAllByRole('button', {name: 'Remove'})[0]);
  238. expect(deleteMock).toHaveBeenCalled();
  239. });
  240. it('can only remove self from team', async function () {
  241. const me = TestStubs.Member({
  242. id: '123',
  243. email: 'foo@example.com',
  244. });
  245. Client.addMockResponse({
  246. url: `/teams/${organization.slug}/${team.slug}/members/`,
  247. method: 'GET',
  248. body: [...members, me],
  249. });
  250. const deleteMock = Client.addMockResponse({
  251. url: `/organizations/${organization.slug}/members/${me.id}/teams/${team.slug}/`,
  252. method: 'DELETE',
  253. });
  254. const organizationMember = TestStubs.Organization({access: []});
  255. render(
  256. <TeamMembers
  257. params={{orgId: organization.slug, teamId: team.slug}}
  258. organization={organizationMember}
  259. team={team}
  260. />
  261. );
  262. await screen.findAllByRole('button', {name: 'Add Member'});
  263. expect(deleteMock).not.toHaveBeenCalled();
  264. expect(screen.getAllByTestId('letter_avatar-avatar')).toHaveLength(
  265. members.length + 1
  266. );
  267. // Can only remove self
  268. expect(screen.getByRole('button', {name: 'Leave'})).toBeInTheDocument();
  269. await userEvent.click(screen.getByRole('button', {name: 'Leave'}));
  270. expect(deleteMock).toHaveBeenCalled();
  271. });
  272. it('does not renders team-level roles', async function () {
  273. const me = TestStubs.Member({
  274. id: '123',
  275. email: 'foo@example.com',
  276. role: 'owner',
  277. });
  278. Client.addMockResponse({
  279. url: `/teams/${organization.slug}/${team.slug}/members/`,
  280. method: 'GET',
  281. body: [...members, me],
  282. });
  283. await render(
  284. <TeamMembers
  285. params={{orgId: organization.slug, teamId: team.slug}}
  286. organization={organization}
  287. team={team}
  288. />
  289. );
  290. const admins = screen.queryByText('Team Admin');
  291. expect(admins).not.toBeInTheDocument();
  292. const contributors = screen.queryByText('Team Contributor');
  293. expect(contributors).not.toBeInTheDocument();
  294. });
  295. it('renders team-level roles with flag', async function () {
  296. const manager = TestStubs.Member({
  297. id: '123',
  298. email: 'foo@example.com',
  299. orgRole: 'manager',
  300. });
  301. Client.addMockResponse({
  302. url: `/teams/${organization.slug}/${team.slug}/members/`,
  303. method: 'GET',
  304. body: [...members, manager],
  305. });
  306. const orgWithTeamRoles = TestStubs.Organization({features: ['team-roles']});
  307. await render(
  308. <TeamMembers
  309. params={{orgId: orgWithTeamRoles.slug, teamId: team.slug}}
  310. organization={orgWithTeamRoles}
  311. team={team}
  312. />
  313. );
  314. const admins = screen.queryAllByText('Team Admin');
  315. expect(admins).toHaveLength(3);
  316. const contributors = screen.queryAllByText('Contributor');
  317. expect(contributors).toHaveLength(2);
  318. });
  319. it('adding member to manager team makes them team admin', async function () {
  320. Client.addMockResponse({
  321. url: `/teams/${organization.slug}/${managerTeam.slug}/members/`,
  322. method: 'GET',
  323. body: [],
  324. });
  325. const orgWithTeamRoles = TestStubs.Organization({features: ['team-roles']});
  326. render(
  327. <TeamMembers
  328. params={{orgId: orgWithTeamRoles.slug, teamId: managerTeam.slug}}
  329. organization={orgWithTeamRoles}
  330. team={managerTeam}
  331. />
  332. );
  333. await userEvent.click(
  334. (
  335. await screen.findAllByRole('button', {name: 'Add Member'})
  336. )[0]
  337. );
  338. await userEvent.click(screen.getAllByTestId('letter_avatar-avatar')[0]);
  339. const admin = screen.queryByText('Team Admin');
  340. expect(admin).toBeInTheDocument();
  341. });
  342. it('cannot add or remove members if team is idp:provisioned', function () {
  343. const team2 = TestStubs.Team({
  344. flags: {
  345. 'idp:provisioned': true,
  346. },
  347. });
  348. const me = TestStubs.Member({
  349. id: '123',
  350. email: 'foo@example.com',
  351. role: 'owner',
  352. flags: {
  353. 'idp:provisioned': true,
  354. },
  355. });
  356. Client.clearMockResponses();
  357. Client.addMockResponse({
  358. url: `/organizations/${organization.slug}/members/`,
  359. method: 'GET',
  360. body: [...members, me],
  361. });
  362. Client.addMockResponse({
  363. url: `/teams/${organization.slug}/${team2.slug}/members/`,
  364. method: 'GET',
  365. body: members,
  366. });
  367. Client.addMockResponse({
  368. url: `/teams/${organization.slug}/${team2.slug}/`,
  369. method: 'GET',
  370. body: team2,
  371. });
  372. render(
  373. <TeamMembers
  374. params={{orgId: organization.slug, teamId: team2.slug}}
  375. organization={organization}
  376. team={team2}
  377. />
  378. );
  379. waitFor(() => {
  380. expect(screen.findByRole('button', {name: 'Add Member'})).toBeDisabled();
  381. expect(screen.findByRole('button', {name: 'Remove'})).toBeDisabled();
  382. });
  383. });
  384. it('cannot add or remove members or leave if team has org role and no access', function () {
  385. const team2 = TestStubs.Team({orgRole: 'manager'});
  386. const me = TestStubs.Member({
  387. id: '123',
  388. email: 'foo@example.com',
  389. role: 'member',
  390. });
  391. Client.clearMockResponses();
  392. Client.addMockResponse({
  393. url: `/organizations/${organization.slug}/members/`,
  394. method: 'GET',
  395. body: [...members, me],
  396. });
  397. Client.addMockResponse({
  398. url: `/teams/${organization.slug}/${team2.slug}/members/`,
  399. method: 'GET',
  400. body: members,
  401. });
  402. Client.addMockResponse({
  403. url: `/teams/${organization.slug}/${team2.slug}/`,
  404. method: 'GET',
  405. body: team2,
  406. });
  407. render(
  408. <TeamMembers
  409. params={{orgId: organization.slug, teamId: team2.slug}}
  410. organization={organization}
  411. team={team2}
  412. />
  413. );
  414. waitFor(() => {
  415. expect(screen.findByRole('button', {name: 'Add Member'})).toBeDisabled();
  416. expect(screen.findByRole('button', {name: 'Remove'})).toBeDisabled();
  417. expect(screen.findByRole('button', {name: 'Leave'})).toBeDisabled();
  418. });
  419. });
  420. });