organizationMemberDetail.spec.tsx 26 KB

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