organizationMemberDetail.spec.tsx 27 KB

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