organizationMemberDetail.spec.tsx 26 KB

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