organizationMemberDetail.spec.tsx 26 KB

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