organizationMemberDetail.spec.tsx 26 KB

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