teamMembers.spec.tsx 14 KB

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