teamMembers.spec.tsx 14 KB

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