teamMembers.spec.tsx 14 KB

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