teamMembers.spec.tsx 14 KB

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