organizationMemberDetail.spec.tsx 26 KB

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