teamMembers.spec.tsx 14 KB

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