organizationMemberDetail.spec.tsx 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889
  1. import selectEvent from 'react-select-event';
  2. import {initializeOrg} from 'sentry-test/initializeOrg';
  3. import {
  4. cleanup,
  5. render,
  6. renderGlobalModal,
  7. screen,
  8. userEvent,
  9. within,
  10. } from 'sentry-test/reactTestingLibrary';
  11. import {updateMember} from 'sentry/actionCreators/members';
  12. import TeamStore from 'sentry/stores/teamStore';
  13. import OrganizationMemberDetail from 'sentry/views/settings/organizationMembers/organizationMemberDetail';
  14. jest.mock('sentry/actionCreators/members', () => ({
  15. updateMember: jest.fn().mockReturnValue(new Promise(() => {})),
  16. }));
  17. describe('OrganizationMemberDetail', function () {
  18. const team = TestStubs.Team();
  19. const idpTeam = TestStubs.Team({
  20. id: '3',
  21. slug: 'idp-member-team',
  22. name: 'Idp Member Team',
  23. isMember: true,
  24. flags: {
  25. 'idp:provisioned': true,
  26. },
  27. });
  28. const managerTeam = TestStubs.Team({id: '5', orgRole: 'manager', slug: 'manager-team'});
  29. const otherManagerTeam = TestStubs.Team({
  30. id: '4',
  31. slug: 'org-role-team',
  32. name: 'Org Role Team',
  33. isMember: true,
  34. orgRole: 'manager',
  35. });
  36. const teams = [
  37. team,
  38. TestStubs.Team({
  39. id: '2',
  40. slug: 'new-team',
  41. name: 'New Team',
  42. isMember: false,
  43. }),
  44. idpTeam,
  45. managerTeam,
  46. otherManagerTeam,
  47. ];
  48. const teamAssignment = {
  49. teams: [team.slug],
  50. teamRoles: [
  51. {
  52. teamSlug: team.slug,
  53. role: null,
  54. },
  55. ],
  56. };
  57. const member = TestStubs.Member({
  58. roles: TestStubs.OrgRoleList(),
  59. dateCreated: new Date(),
  60. ...teamAssignment,
  61. });
  62. const pendingMember = TestStubs.Member({
  63. id: 2,
  64. roles: TestStubs.OrgRoleList(),
  65. dateCreated: new Date(),
  66. ...teamAssignment,
  67. invite_link: 'http://example.com/i/abc123',
  68. pending: true,
  69. });
  70. const expiredMember = TestStubs.Member({
  71. id: 3,
  72. roles: TestStubs.OrgRoleList(),
  73. dateCreated: new Date(),
  74. ...teamAssignment,
  75. invite_link: 'http://example.com/i/abc123',
  76. pending: true,
  77. expired: true,
  78. });
  79. const idpTeamMember = TestStubs.Member({
  80. id: 4,
  81. roles: TestStubs.OrgRoleList(),
  82. dateCreated: new Date(),
  83. teams: [idpTeam.slug],
  84. teamRoles: [
  85. {
  86. teamSlug: idpTeam.slug,
  87. role: null,
  88. },
  89. ],
  90. });
  91. const managerTeamMember = TestStubs.Member({
  92. id: 5,
  93. roles: TestStubs.OrgRoleList(),
  94. dateCreated: new Date(),
  95. teams: [otherManagerTeam.slug],
  96. teamRoles: [
  97. {
  98. teamSlug: otherManagerTeam.slug,
  99. role: null,
  100. },
  101. ],
  102. });
  103. const managerMember = TestStubs.Member({
  104. id: 6,
  105. roles: TestStubs.OrgRoleList(),
  106. role: 'manager',
  107. });
  108. beforeEach(() => {
  109. MockApiClient.clearMockResponses();
  110. TeamStore.loadInitialData(teams);
  111. });
  112. describe('Can Edit', function () {
  113. const organization = TestStubs.Organization({teams, features: ['team-roles']});
  114. beforeEach(function () {
  115. TeamStore.init();
  116. TeamStore.loadInitialData(teams);
  117. jest.resetAllMocks();
  118. MockApiClient.clearMockResponses();
  119. MockApiClient.addMockResponse({
  120. url: `/organizations/${organization.slug}/members/${member.id}/`,
  121. body: member,
  122. });
  123. MockApiClient.addMockResponse({
  124. url: `/organizations/${organization.slug}/members/${pendingMember.id}/`,
  125. body: pendingMember,
  126. });
  127. MockApiClient.addMockResponse({
  128. url: `/organizations/${organization.slug}/members/${expiredMember.id}/`,
  129. body: expiredMember,
  130. });
  131. MockApiClient.addMockResponse({
  132. url: `/organizations/${organization.slug}/members/${idpTeamMember.id}/`,
  133. body: idpTeamMember,
  134. });
  135. MockApiClient.addMockResponse({
  136. url: `/organizations/${organization.slug}/members/${managerTeamMember.id}/`,
  137. body: managerTeamMember,
  138. });
  139. MockApiClient.addMockResponse({
  140. url: `/organizations/${organization.slug}/members/${managerMember.id}/`,
  141. body: managerMember,
  142. });
  143. MockApiClient.addMockResponse({
  144. url: `/organizations/${organization.slug}/teams/`,
  145. body: teams,
  146. });
  147. });
  148. it('changes org role to owner', async function () {
  149. const {routerContext, routerProps} = initializeOrg({organization});
  150. render(
  151. <OrganizationMemberDetail {...routerProps} params={{memberId: member.id}} />,
  152. {
  153. context: routerContext,
  154. }
  155. );
  156. // Should have 4 roles
  157. const radios = screen.getAllByRole('radio');
  158. expect(radios).toHaveLength(4);
  159. // Click last radio
  160. await userEvent.click(radios.at(-1) as Element);
  161. expect(radios.at(-1)).toBeChecked();
  162. // Save Member
  163. await userEvent.click(screen.getByRole('button', {name: 'Save Member'}));
  164. expect(updateMember).toHaveBeenCalledWith(
  165. expect.anything(),
  166. expect.objectContaining({
  167. data: expect.objectContaining({
  168. orgRole: 'owner',
  169. }),
  170. })
  171. );
  172. });
  173. it('leaves a team', async function () {
  174. const {routerContext, routerProps} = initializeOrg({organization});
  175. render(
  176. <OrganizationMemberDetail {...routerProps} params={{memberId: member.id}} />,
  177. {
  178. context: routerContext,
  179. }
  180. );
  181. // Remove our one team
  182. await userEvent.click(screen.getByRole('button', {name: 'Remove'}));
  183. // Save Member
  184. await userEvent.click(screen.getByRole('button', {name: 'Save Member'}));
  185. expect(updateMember).toHaveBeenCalledWith(
  186. expect.anything(),
  187. expect.objectContaining({
  188. data: expect.objectContaining({
  189. teamRoles: [],
  190. }),
  191. })
  192. );
  193. });
  194. it('cannot leave idp-provisioned team', function () {
  195. const {routerContext, routerProps} = initializeOrg({organization});
  196. render(
  197. <OrganizationMemberDetail
  198. {...routerProps}
  199. params={{memberId: idpTeamMember.id}}
  200. />,
  201. {
  202. context: routerContext,
  203. }
  204. );
  205. expect(screen.getByRole('button', {name: 'Remove'})).toBeDisabled();
  206. });
  207. it('cannot leave org role team if missing org:admin', function () {
  208. const {routerContext, routerProps} = initializeOrg({
  209. organization: TestStubs.Organization({
  210. teams,
  211. features: ['team-roles'],
  212. access: [],
  213. }),
  214. });
  215. render(
  216. <OrganizationMemberDetail
  217. {...routerProps}
  218. params={{memberId: managerTeamMember.id}}
  219. />,
  220. {
  221. context: routerContext,
  222. }
  223. );
  224. expect(screen.getByText('Manager Team')).toBeInTheDocument();
  225. expect(screen.getByRole('button', {name: 'Remove'})).toBeDisabled();
  226. });
  227. it('cannot join org role team if missing org:admin', async function () {
  228. const {routerContext, routerProps} = initializeOrg({
  229. organization: TestStubs.Organization({
  230. teams,
  231. features: ['team-roles'],
  232. access: ['org:write'],
  233. }),
  234. });
  235. render(
  236. <OrganizationMemberDetail
  237. {...routerProps}
  238. params={{memberId: managerMember.id}}
  239. />,
  240. {
  241. context: routerContext,
  242. }
  243. );
  244. await userEvent.click(screen.getByText('Add Team'));
  245. await userEvent.hover(screen.getByText('#org-role-team'));
  246. expect(
  247. await screen.findByText(
  248. 'Membership to a team with an organization role is managed by org owners.'
  249. )
  250. ).toBeInTheDocument();
  251. });
  252. it('joins a team and assign a team-role', async function () {
  253. const {routerContext, routerProps} = initializeOrg({organization});
  254. render(
  255. <OrganizationMemberDetail {...routerProps} params={{memberId: member.id}} />,
  256. {
  257. context: routerContext,
  258. }
  259. );
  260. // Should have one team enabled
  261. expect(screen.getByTestId('team-row-for-member')).toBeInTheDocument();
  262. // Select new team to join
  263. // Open the dropdown
  264. await userEvent.click(screen.getByText('Add Team'));
  265. // Click the first item
  266. await userEvent.click(screen.getByText('#new-team'));
  267. // Assign as admin to new team
  268. const teamRoleSelect = screen.getAllByText('Contributor')[0];
  269. await selectEvent.select(teamRoleSelect, ['Team Admin']);
  270. // Save Member
  271. await userEvent.click(screen.getByRole('button', {name: 'Save Member'}));
  272. expect(updateMember).toHaveBeenCalledWith(
  273. expect.anything(),
  274. expect.objectContaining({
  275. data: expect.objectContaining({
  276. teamRoles: [
  277. {teamSlug: 'team-slug', role: null},
  278. {teamSlug: 'new-team', role: 'admin'},
  279. ],
  280. }),
  281. })
  282. );
  283. });
  284. it('cannot join idp-provisioned team', async function () {
  285. const {routerContext, routerProps} = initializeOrg({organization});
  286. render(
  287. <OrganizationMemberDetail {...routerProps} params={{memberId: member.id}} />,
  288. {
  289. context: routerContext,
  290. }
  291. );
  292. await userEvent.click(screen.getByText('Add Team'));
  293. await userEvent.hover(screen.getByText('#idp-member-team'));
  294. expect(
  295. await screen.findByText(
  296. "Membership to this team is managed through your organization's identity provider."
  297. )
  298. ).toBeInTheDocument();
  299. });
  300. });
  301. describe('Cannot Edit', function () {
  302. const organization = TestStubs.Organization({teams, access: ['org:read']});
  303. beforeEach(function () {
  304. TeamStore.init();
  305. TeamStore.loadInitialData(teams);
  306. jest.resetAllMocks();
  307. MockApiClient.clearMockResponses();
  308. MockApiClient.addMockResponse({
  309. url: `/organizations/${organization.slug}/members/${member.id}/`,
  310. body: member,
  311. });
  312. MockApiClient.addMockResponse({
  313. url: `/organizations/${organization.slug}/members/${pendingMember.id}/`,
  314. body: pendingMember,
  315. });
  316. MockApiClient.addMockResponse({
  317. url: `/organizations/${organization.slug}/members/${expiredMember.id}/`,
  318. body: expiredMember,
  319. });
  320. MockApiClient.addMockResponse({
  321. url: `/organizations/${organization.slug}/teams/`,
  322. body: teams,
  323. });
  324. });
  325. it('can not change roles, teams, or save', function () {
  326. const {routerContext, routerProps} = initializeOrg({organization});
  327. render(
  328. <OrganizationMemberDetail {...routerProps} params={{memberId: member.id}} />,
  329. {
  330. context: routerContext,
  331. }
  332. );
  333. // Should have 4 roles
  334. const radios = screen.getAllByRole('radio');
  335. expect(radios.at(0)).toHaveAttribute('readonly');
  336. // Save Member
  337. expect(screen.getByRole('button', {name: 'Save Member'})).toBeDisabled();
  338. });
  339. });
  340. describe('Display status', function () {
  341. const organization = TestStubs.Organization({teams, access: ['org:read']});
  342. beforeEach(function () {
  343. TeamStore.init();
  344. TeamStore.loadInitialData(teams);
  345. jest.resetAllMocks();
  346. MockApiClient.clearMockResponses();
  347. MockApiClient.addMockResponse({
  348. url: `/organizations/${organization.slug}/members/${member.id}/`,
  349. body: member,
  350. });
  351. MockApiClient.addMockResponse({
  352. url: `/organizations/${organization.slug}/members/${pendingMember.id}/`,
  353. body: pendingMember,
  354. });
  355. MockApiClient.addMockResponse({
  356. url: `/organizations/${organization.slug}/members/${expiredMember.id}/`,
  357. body: expiredMember,
  358. });
  359. MockApiClient.addMockResponse({
  360. url: `/organizations/${organization.slug}/teams/`,
  361. body: teams,
  362. });
  363. });
  364. it('display pending status', function () {
  365. const {routerContext, routerProps} = initializeOrg({organization});
  366. render(
  367. <OrganizationMemberDetail
  368. {...routerProps}
  369. params={{memberId: pendingMember.id}}
  370. />,
  371. {
  372. context: routerContext,
  373. }
  374. );
  375. expect(screen.getByTestId('member-status')).toHaveTextContent('Invitation Pending');
  376. });
  377. it('display expired status', function () {
  378. const {routerContext, routerProps} = initializeOrg({organization});
  379. render(
  380. <OrganizationMemberDetail
  381. {...routerProps}
  382. params={{memberId: expiredMember.id}}
  383. />,
  384. {
  385. context: routerContext,
  386. }
  387. );
  388. expect(screen.getByTestId('member-status')).toHaveTextContent('Invitation Expired');
  389. });
  390. });
  391. describe('Show resend button', function () {
  392. const organization = TestStubs.Organization({teams, access: ['org:read']});
  393. beforeEach(function () {
  394. TeamStore.init();
  395. TeamStore.loadInitialData(teams);
  396. jest.resetAllMocks();
  397. MockApiClient.clearMockResponses();
  398. MockApiClient.addMockResponse({
  399. url: `/organizations/${organization.slug}/members/${member.id}/`,
  400. body: member,
  401. });
  402. MockApiClient.addMockResponse({
  403. url: `/organizations/${organization.slug}/members/${pendingMember.id}/`,
  404. body: pendingMember,
  405. });
  406. MockApiClient.addMockResponse({
  407. url: `/organizations/${organization.slug}/members/${expiredMember.id}/`,
  408. body: expiredMember,
  409. });
  410. MockApiClient.addMockResponse({
  411. url: `/organizations/${organization.slug}/teams/`,
  412. body: teams,
  413. });
  414. });
  415. it('shows for pending', function () {
  416. const {routerContext, routerProps} = initializeOrg({organization});
  417. render(
  418. <OrganizationMemberDetail
  419. {...routerProps}
  420. params={{memberId: pendingMember.id}}
  421. />,
  422. {
  423. context: routerContext,
  424. }
  425. );
  426. expect(screen.getByRole('button', {name: 'Resend Invite'})).toBeInTheDocument();
  427. });
  428. it('does not show for expired', function () {
  429. const {routerContext, routerProps} = initializeOrg({organization});
  430. render(
  431. <OrganizationMemberDetail
  432. {...routerProps}
  433. params={{memberId: expiredMember.id}}
  434. />,
  435. {
  436. context: routerContext,
  437. }
  438. );
  439. expect(
  440. screen.queryByRole('button', {name: 'Resend Invite'})
  441. ).not.toBeInTheDocument();
  442. });
  443. });
  444. describe('Reset member 2FA', function () {
  445. const fields = {
  446. roles: TestStubs.OrgRoleList(),
  447. dateCreated: new Date(),
  448. ...teamAssignment,
  449. };
  450. const noAccess = TestStubs.Member({
  451. ...fields,
  452. id: '4',
  453. user: TestStubs.User({has2fa: false, authenticators: undefined}),
  454. });
  455. const no2fa = TestStubs.Member({
  456. ...fields,
  457. id: '5',
  458. user: TestStubs.User({has2fa: false, authenticators: [], canReset2fa: true}),
  459. });
  460. const has2fa = TestStubs.Member({
  461. ...fields,
  462. id: '6',
  463. user: TestStubs.User({
  464. has2fa: true,
  465. authenticators: [
  466. TestStubs.Authenticators().Totp(),
  467. TestStubs.Authenticators().Sms(),
  468. TestStubs.Authenticators().U2f(),
  469. ],
  470. canReset2fa: true,
  471. }),
  472. });
  473. const multipleOrgs = TestStubs.Member({
  474. ...fields,
  475. id: '7',
  476. user: TestStubs.User({
  477. has2fa: true,
  478. authenticators: [TestStubs.Authenticators().Totp()],
  479. canReset2fa: false,
  480. }),
  481. });
  482. const organization = TestStubs.Organization({teams});
  483. beforeEach(function () {
  484. MockApiClient.clearMockResponses();
  485. MockApiClient.addMockResponse({
  486. url: `/organizations/${organization.slug}/members/${pendingMember.id}/`,
  487. body: pendingMember,
  488. });
  489. MockApiClient.addMockResponse({
  490. url: `/organizations/${organization.slug}/members/${noAccess.id}/`,
  491. body: noAccess,
  492. });
  493. MockApiClient.addMockResponse({
  494. url: `/organizations/${organization.slug}/members/${no2fa.id}/`,
  495. body: no2fa,
  496. });
  497. MockApiClient.addMockResponse({
  498. url: `/organizations/${organization.slug}/members/${has2fa.id}/`,
  499. body: has2fa,
  500. });
  501. MockApiClient.addMockResponse({
  502. url: `/organizations/${organization.slug}/members/${multipleOrgs.id}/`,
  503. body: multipleOrgs,
  504. });
  505. MockApiClient.addMockResponse({
  506. url: `/organizations/${organization.slug}/teams/`,
  507. body: teams,
  508. });
  509. });
  510. const button = () =>
  511. screen.queryByRole('button', {name: 'Reset two-factor authentication'});
  512. const tooltip = () => screen.queryByTestId('reset-2fa-tooltip');
  513. const expectButtonEnabled = () => {
  514. expect(button()).toHaveTextContent('Reset two-factor authentication');
  515. expect(button()).toBeEnabled();
  516. expect(tooltip()).not.toBeInTheDocument();
  517. };
  518. const expectButtonDisabled = async title => {
  519. expect(button()).toHaveTextContent('Reset two-factor authentication');
  520. expect(button()).toBeDisabled();
  521. await userEvent.hover(button() as Element);
  522. expect(await screen.findByText(title)).toBeInTheDocument();
  523. };
  524. it('does not show for pending member', function () {
  525. const {routerContext, routerProps} = initializeOrg({organization});
  526. render(
  527. <OrganizationMemberDetail
  528. {...routerProps}
  529. params={{memberId: pendingMember.id}}
  530. />,
  531. {
  532. context: routerContext,
  533. }
  534. );
  535. expect(button()).not.toBeInTheDocument();
  536. });
  537. it('shows tooltip for joined member without permission to edit', async function () {
  538. const {routerContext, routerProps} = initializeOrg({organization});
  539. render(
  540. <OrganizationMemberDetail {...routerProps} params={{memberId: noAccess.id}} />,
  541. {
  542. context: routerContext,
  543. }
  544. );
  545. await expectButtonDisabled('You do not have permission to perform this action');
  546. });
  547. it('shows tooltip for member without 2fa', async function () {
  548. const {routerContext, routerProps} = initializeOrg({organization});
  549. render(
  550. <OrganizationMemberDetail {...routerProps} params={{memberId: no2fa.id}} />,
  551. {
  552. context: routerContext,
  553. }
  554. );
  555. await expectButtonDisabled('Not enrolled in two-factor authentication');
  556. });
  557. it('can reset member 2FA', async function () {
  558. const {routerContext, routerProps} = initializeOrg({organization});
  559. const deleteMocks = has2fa.user.authenticators.map(auth =>
  560. MockApiClient.addMockResponse({
  561. url: `/users/${has2fa.user.id}/authenticators/${auth.id}/`,
  562. method: 'DELETE',
  563. })
  564. );
  565. render(
  566. <OrganizationMemberDetail {...routerProps} params={{memberId: has2fa.id}} />,
  567. {
  568. context: routerContext,
  569. }
  570. );
  571. renderGlobalModal();
  572. expectButtonEnabled();
  573. await userEvent.click(button() as Element);
  574. await userEvent.click(screen.getByRole('button', {name: 'Confirm'}));
  575. deleteMocks.forEach(deleteMock => {
  576. expect(deleteMock).toHaveBeenCalled();
  577. });
  578. });
  579. it('shows tooltip for member in multiple orgs', async function () {
  580. const {routerContext, routerProps} = initializeOrg({organization});
  581. render(
  582. <OrganizationMemberDetail
  583. {...routerProps}
  584. params={{memberId: multipleOrgs.id}}
  585. />,
  586. {
  587. context: routerContext,
  588. }
  589. );
  590. await expectButtonDisabled(
  591. 'Cannot be reset since user is in more than one organization'
  592. );
  593. });
  594. it('shows tooltip for member in 2FA required org', async function () {
  595. organization.require2FA = true;
  596. const {routerContext, routerProps} = initializeOrg({organization});
  597. MockApiClient.addMockResponse({
  598. url: `/organizations/${organization.slug}/members/${has2fa.id}/`,
  599. body: has2fa,
  600. });
  601. render(
  602. <OrganizationMemberDetail {...routerProps} params={{memberId: has2fa.id}} />,
  603. {
  604. context: routerContext,
  605. }
  606. );
  607. await expectButtonDisabled(
  608. 'Cannot be reset since two-factor is required for this organization'
  609. );
  610. });
  611. });
  612. describe('Org Roles affect Team Roles', () => {
  613. // Org Admin will be deprecated
  614. const admin = TestStubs.Member({
  615. id: '4',
  616. role: 'admin',
  617. roleName: 'Admin',
  618. orgRole: 'admin',
  619. ...teamAssignment,
  620. });
  621. const manager = TestStubs.Member({
  622. id: '5',
  623. role: 'manager',
  624. roleName: 'Manager',
  625. orgRole: 'manager',
  626. ...teamAssignment,
  627. });
  628. const owner = TestStubs.Member({
  629. id: '6',
  630. role: 'owner',
  631. roleName: 'Owner',
  632. orgRole: 'owner',
  633. ...teamAssignment,
  634. });
  635. const organization = TestStubs.Organization({teams, features: ['team-roles']});
  636. beforeEach(() => {
  637. MockApiClient.clearMockResponses();
  638. MockApiClient.addMockResponse({
  639. url: `/organizations/${organization.slug}/members/${member.id}/`,
  640. body: member,
  641. });
  642. MockApiClient.addMockResponse({
  643. url: `/organizations/${organization.slug}/members/${admin.id}/`,
  644. body: admin,
  645. });
  646. MockApiClient.addMockResponse({
  647. url: `/organizations/${organization.slug}/members/${manager.id}/`,
  648. body: manager,
  649. });
  650. MockApiClient.addMockResponse({
  651. url: `/organizations/${organization.slug}/members/${owner.id}/`,
  652. body: owner,
  653. });
  654. });
  655. it('does not overwrite team-roles for org members', async () => {
  656. const {routerContext, routerProps} = initializeOrg({organization});
  657. render(
  658. <OrganizationMemberDetail {...routerProps} params={{memberId: member.id}} />,
  659. {
  660. context: routerContext,
  661. }
  662. );
  663. // Role info box is hidden
  664. expect(screen.queryByTestId('alert-role-overwrite')).not.toBeInTheDocument();
  665. // Dropdown has correct value set
  666. const teamRow = screen.getByTestId('team-row-for-member');
  667. const teamRoleSelect = within(teamRow).getByText('Contributor');
  668. // Dropdown options are not visible
  669. expect(screen.queryAllByText('...').length).toBe(0);
  670. // Dropdown can be opened
  671. selectEvent.openMenu(teamRoleSelect);
  672. expect(screen.queryAllByText('...').length).toBe(2);
  673. // Dropdown value can be changed
  674. await selectEvent.select(teamRoleSelect, ['Team Admin']);
  675. expect(teamRoleSelect).toHaveTextContent('Team Admin');
  676. });
  677. it('overwrite team-roles for org admin/manager/owner', () => {
  678. const {routerContext, routerProps} = initializeOrg({organization});
  679. function testForOrgRole(testMember) {
  680. cleanup();
  681. render(
  682. <OrganizationMemberDetail
  683. {...routerProps}
  684. params={{memberId: testMember.id}}
  685. />,
  686. {
  687. context: routerContext,
  688. }
  689. );
  690. // Role info box is showed
  691. expect(screen.queryByTestId('alert-role-overwrite')).toBeInTheDocument();
  692. // Dropdown has correct value set
  693. const teamRow = screen.getByTestId('team-row-for-member');
  694. const teamRoleSelect = within(teamRow).getByText('Team Admin');
  695. // Dropdown options are not visible
  696. expect(screen.queryAllByText('...').length).toBe(0);
  697. // Dropdown cannot be opened
  698. selectEvent.openMenu(teamRoleSelect);
  699. expect(screen.queryAllByText('...').length).toBe(0);
  700. }
  701. for (const role of [admin, manager, owner]) {
  702. testForOrgRole(role);
  703. }
  704. });
  705. it('overwrites when changing from member to manager', async () => {
  706. const {routerContext, routerProps} = initializeOrg({organization});
  707. render(
  708. <OrganizationMemberDetail {...routerProps} params={{memberId: member.id}} />,
  709. {
  710. context: routerContext,
  711. }
  712. );
  713. // Role info box is hidden
  714. expect(screen.queryByTestId('alert-role-overwrite')).not.toBeInTheDocument();
  715. // Dropdown has correct value set
  716. const teamRow = screen.getByTestId('team-row-for-member');
  717. const teamRoleSelect = within(teamRow).getByText('Contributor');
  718. // Change member to owner
  719. const orgRoleRadio = screen.getAllByRole('radio');
  720. expect(orgRoleRadio).toHaveLength(4);
  721. await userEvent.click(orgRoleRadio.at(-1) as Element);
  722. expect(orgRoleRadio.at(-1)).toBeChecked();
  723. // Role info box is shown
  724. expect(screen.queryByTestId('alert-role-overwrite')).toBeInTheDocument();
  725. // Dropdown has correct value set
  726. within(teamRow).getByText('Team Admin');
  727. // Dropdown options are not visible
  728. expect(screen.queryAllByText('...').length).toBe(0);
  729. // Dropdown cannot be opened
  730. selectEvent.openMenu(teamRoleSelect);
  731. expect(screen.queryAllByText('...').length).toBe(0);
  732. });
  733. it('overwrites when member joins a manager team', async () => {
  734. const {routerContext, routerProps} = initializeOrg({});
  735. render(
  736. <OrganizationMemberDetail {...routerProps} params={{memberId: member.id}} />,
  737. {
  738. context: routerContext,
  739. }
  740. );
  741. // Role info box is hidden
  742. expect(screen.queryByTestId('alert-role-overwrite')).not.toBeInTheDocument();
  743. // Dropdown has correct value set
  744. const teamRow = screen.getByTestId('team-row-for-member');
  745. const teamRoleSelect = within(teamRow).getByText('Contributor');
  746. // Join manager team
  747. await userEvent.click(screen.getByText('Add Team'));
  748. // Click the first item
  749. await userEvent.click(screen.getByText('#manager-team'));
  750. // Role info box is shown
  751. expect(screen.queryByTestId('alert-role-overwrite')).toBeInTheDocument();
  752. // Dropdowns have correct value set
  753. const teamRows = screen.getAllByTestId('team-row-for-member');
  754. within(teamRows[0]).getByText('Team Admin');
  755. within(teamRows[1]).getByText('Team Admin');
  756. // Dropdown options are not visible
  757. expect(screen.queryAllByText('...').length).toBe(0);
  758. // Dropdown cannot be opened
  759. selectEvent.openMenu(teamRoleSelect);
  760. expect(screen.queryAllByText('...').length).toBe(0);
  761. });
  762. });
  763. });