teamMembers.spec.tsx 14 KB

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