organizationMemberDetail.spec.jsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445
  1. import {
  2. render,
  3. renderGlobalModal,
  4. screen,
  5. userEvent,
  6. } from 'sentry-test/reactTestingLibrary';
  7. import {updateMember} from 'sentry/actionCreators/members';
  8. import TeamStore from 'sentry/stores/teamStore';
  9. import OrganizationMemberDetail from 'sentry/views/settings/organizationMembers/organizationMemberDetail';
  10. jest.mock('sentry/actionCreators/members', () => ({
  11. updateMember: jest.fn().mockReturnValue(new Promise(() => {})),
  12. }));
  13. describe('OrganizationMemberDetail', function () {
  14. let organization;
  15. let routerContext;
  16. const team = TestStubs.Team();
  17. const teams = [
  18. team,
  19. TestStubs.Team({
  20. id: '2',
  21. slug: 'new-team',
  22. name: 'New Team',
  23. isMember: false,
  24. }),
  25. ];
  26. const member = TestStubs.Member({
  27. roles: TestStubs.OrgRoleList(),
  28. dateCreated: new Date(),
  29. teams: [team.slug],
  30. });
  31. const pendingMember = TestStubs.Member({
  32. id: 2,
  33. roles: TestStubs.OrgRoleList(),
  34. dateCreated: new Date(),
  35. teams: [team.slug],
  36. invite_link: 'http://example.com/i/abc123',
  37. pending: true,
  38. });
  39. const expiredMember = TestStubs.Member({
  40. id: 3,
  41. roles: TestStubs.OrgRoleList(),
  42. dateCreated: new Date(),
  43. teams: [team.slug],
  44. invite_link: 'http://example.com/i/abc123',
  45. pending: true,
  46. expired: true,
  47. });
  48. describe('Can Edit', function () {
  49. beforeEach(function () {
  50. organization = TestStubs.Organization({teams});
  51. routerContext = TestStubs.routerContext([{organization}]);
  52. TeamStore.init();
  53. TeamStore.loadInitialData(teams);
  54. jest.resetAllMocks();
  55. MockApiClient.clearMockResponses();
  56. MockApiClient.addMockResponse({
  57. url: `/organizations/${organization.slug}/members/${member.id}/`,
  58. body: member,
  59. });
  60. MockApiClient.addMockResponse({
  61. url: `/organizations/${organization.slug}/members/${pendingMember.id}/`,
  62. body: pendingMember,
  63. });
  64. MockApiClient.addMockResponse({
  65. url: `/organizations/${organization.slug}/members/${expiredMember.id}/`,
  66. body: expiredMember,
  67. });
  68. MockApiClient.addMockResponse({
  69. url: `/organizations/${organization.slug}/teams/`,
  70. body: teams,
  71. });
  72. });
  73. it('changes role to owner', function () {
  74. render(<OrganizationMemberDetail params={{memberId: member.id}} />, {
  75. context: routerContext,
  76. });
  77. // Should have 4 roles
  78. const radios = screen.getAllByRole('radio');
  79. expect(radios).toHaveLength(4);
  80. // Click last radio
  81. userEvent.click(radios.at(-1));
  82. expect(radios.at(-1)).toBeChecked();
  83. // Save Member
  84. userEvent.click(screen.getByRole('button', {name: 'Save Member'}));
  85. expect(updateMember).toHaveBeenCalledWith(
  86. expect.anything(),
  87. expect.objectContaining({
  88. data: expect.objectContaining({
  89. role: 'owner',
  90. }),
  91. })
  92. );
  93. });
  94. it('leaves a team', function () {
  95. render(<OrganizationMemberDetail params={{memberId: member.id}} />, {
  96. context: routerContext,
  97. });
  98. // Remove our one team
  99. userEvent.click(screen.getByRole('button', {name: 'Remove'}));
  100. // Save Member
  101. userEvent.click(screen.getByRole('button', {name: 'Save Member'}));
  102. expect(updateMember).toHaveBeenCalledWith(
  103. expect.anything(),
  104. expect.objectContaining({
  105. data: expect.objectContaining({
  106. teams: [],
  107. }),
  108. })
  109. );
  110. });
  111. it('joins a team', function () {
  112. render(<OrganizationMemberDetail params={{memberId: member.id}} />, {
  113. context: routerContext,
  114. });
  115. // Should have one team enabled
  116. expect(screen.getByTestId('team-row')).toBeInTheDocument();
  117. // Select new team to join
  118. // Open the dropdown
  119. userEvent.click(screen.getByText('Add Team'));
  120. // Click the first item
  121. userEvent.click(screen.getByText('#new-team'));
  122. // Save Member
  123. userEvent.click(screen.getByRole('button', {name: 'Save Member'}));
  124. expect(updateMember).toHaveBeenCalledWith(
  125. expect.anything(),
  126. expect.objectContaining({
  127. data: expect.objectContaining({
  128. teams: ['team-slug', 'new-team'],
  129. }),
  130. })
  131. );
  132. });
  133. });
  134. describe('Cannot Edit', function () {
  135. beforeEach(function () {
  136. organization = TestStubs.Organization({teams, access: ['org:read']});
  137. routerContext = TestStubs.routerContext([{organization}]);
  138. TeamStore.init();
  139. TeamStore.loadInitialData(teams);
  140. jest.resetAllMocks();
  141. MockApiClient.clearMockResponses();
  142. MockApiClient.addMockResponse({
  143. url: `/organizations/${organization.slug}/members/${member.id}/`,
  144. body: member,
  145. });
  146. MockApiClient.addMockResponse({
  147. url: `/organizations/${organization.slug}/members/${pendingMember.id}/`,
  148. body: pendingMember,
  149. });
  150. MockApiClient.addMockResponse({
  151. url: `/organizations/${organization.slug}/members/${expiredMember.id}/`,
  152. body: expiredMember,
  153. });
  154. MockApiClient.addMockResponse({
  155. url: `/organizations/${organization.slug}/teams/`,
  156. body: teams,
  157. });
  158. });
  159. it('can not change roles, teams, or save', function () {
  160. render(<OrganizationMemberDetail params={{memberId: member.id}} />, {
  161. context: routerContext,
  162. });
  163. // Should have 4 roles
  164. const radios = screen.getAllByRole('radio');
  165. expect(radios.at(0)).toHaveAttribute('readonly');
  166. // Save Member
  167. expect(screen.getByRole('button', {name: 'Save Member'})).toBeDisabled();
  168. });
  169. });
  170. describe('Display status', function () {
  171. beforeEach(function () {
  172. organization = TestStubs.Organization({teams, access: ['org:read']});
  173. routerContext = TestStubs.routerContext([{organization}]);
  174. TeamStore.init();
  175. TeamStore.loadInitialData(teams);
  176. jest.resetAllMocks();
  177. MockApiClient.clearMockResponses();
  178. MockApiClient.addMockResponse({
  179. url: `/organizations/${organization.slug}/members/${member.id}/`,
  180. body: member,
  181. });
  182. MockApiClient.addMockResponse({
  183. url: `/organizations/${organization.slug}/members/${pendingMember.id}/`,
  184. body: pendingMember,
  185. });
  186. MockApiClient.addMockResponse({
  187. url: `/organizations/${organization.slug}/members/${expiredMember.id}/`,
  188. body: expiredMember,
  189. });
  190. MockApiClient.addMockResponse({
  191. url: `/organizations/${organization.slug}/teams/`,
  192. body: teams,
  193. });
  194. });
  195. it('display pending status', function () {
  196. render(<OrganizationMemberDetail params={{memberId: pendingMember.id}} />, {
  197. context: routerContext,
  198. });
  199. expect(screen.getByTestId('member-status')).toHaveTextContent('Invitation Pending');
  200. });
  201. it('display expired status', function () {
  202. render(<OrganizationMemberDetail params={{memberId: expiredMember.id}} />, {
  203. context: routerContext,
  204. });
  205. expect(screen.getByTestId('member-status')).toHaveTextContent('Invitation Expired');
  206. });
  207. });
  208. describe('Show resend button', function () {
  209. beforeEach(function () {
  210. organization = TestStubs.Organization({teams, access: ['org:read']});
  211. routerContext = TestStubs.routerContext([{organization}]);
  212. TeamStore.init();
  213. TeamStore.loadInitialData(teams);
  214. jest.resetAllMocks();
  215. MockApiClient.clearMockResponses();
  216. MockApiClient.addMockResponse({
  217. url: `/organizations/${organization.slug}/members/${member.id}/`,
  218. body: member,
  219. });
  220. MockApiClient.addMockResponse({
  221. url: `/organizations/${organization.slug}/members/${pendingMember.id}/`,
  222. body: pendingMember,
  223. });
  224. MockApiClient.addMockResponse({
  225. url: `/organizations/${organization.slug}/members/${expiredMember.id}/`,
  226. body: expiredMember,
  227. });
  228. MockApiClient.addMockResponse({
  229. url: `/organizations/${organization.slug}/teams/`,
  230. body: teams,
  231. });
  232. });
  233. it('shows for pending', function () {
  234. render(<OrganizationMemberDetail params={{memberId: pendingMember.id}} />, {
  235. context: routerContext,
  236. });
  237. expect(screen.getByRole('button', {name: 'Resend Invite'})).toBeInTheDocument();
  238. });
  239. it('does not show for expired', function () {
  240. render(<OrganizationMemberDetail params={{memberId: expiredMember.id}} />, {
  241. context: routerContext,
  242. });
  243. expect(
  244. screen.queryByRole('button', {name: 'Resend Invite'})
  245. ).not.toBeInTheDocument();
  246. });
  247. });
  248. describe('Reset member 2FA', function () {
  249. const fields = {
  250. roles: TestStubs.OrgRoleList(),
  251. dateCreated: new Date(),
  252. teams: [team.slug],
  253. };
  254. const noAccess = TestStubs.Member({
  255. ...fields,
  256. id: '4',
  257. user: TestStubs.User({has2fa: false}),
  258. });
  259. const no2fa = TestStubs.Member({
  260. ...fields,
  261. id: '5',
  262. user: TestStubs.User({has2fa: false, authenticators: [], canReset2fa: true}),
  263. });
  264. const has2fa = TestStubs.Member({
  265. ...fields,
  266. id: '6',
  267. user: TestStubs.User({
  268. has2fa: true,
  269. authenticators: [
  270. TestStubs.Authenticators().Totp(),
  271. TestStubs.Authenticators().Sms(),
  272. TestStubs.Authenticators().U2f(),
  273. ],
  274. canReset2fa: true,
  275. }),
  276. });
  277. const multipleOrgs = TestStubs.Member({
  278. ...fields,
  279. id: '7',
  280. user: TestStubs.User({
  281. has2fa: true,
  282. authenticators: [TestStubs.Authenticators().Totp()],
  283. canReset2fa: false,
  284. }),
  285. });
  286. beforeEach(function () {
  287. organization = TestStubs.Organization({teams});
  288. routerContext = TestStubs.routerContext([{organization}]);
  289. MockApiClient.clearMockResponses();
  290. MockApiClient.addMockResponse({
  291. url: `/organizations/${organization.slug}/members/${pendingMember.id}/`,
  292. body: pendingMember,
  293. });
  294. MockApiClient.addMockResponse({
  295. url: `/organizations/${organization.slug}/members/${noAccess.id}/`,
  296. body: noAccess,
  297. });
  298. MockApiClient.addMockResponse({
  299. url: `/organizations/${organization.slug}/members/${no2fa.id}/`,
  300. body: no2fa,
  301. });
  302. MockApiClient.addMockResponse({
  303. url: `/organizations/${organization.slug}/members/${has2fa.id}/`,
  304. body: has2fa,
  305. });
  306. MockApiClient.addMockResponse({
  307. url: `/organizations/${organization.slug}/members/${multipleOrgs.id}/`,
  308. body: multipleOrgs,
  309. });
  310. MockApiClient.addMockResponse({
  311. url: `/organizations/${organization.slug}/teams/`,
  312. body: teams,
  313. });
  314. });
  315. const button = () =>
  316. screen.queryByRole('button', {name: 'Reset two-factor authentication'});
  317. const tooltip = () => screen.queryByTestId('reset-2fa-tooltip');
  318. const expectButtonEnabled = () => {
  319. expect(button()).toHaveTextContent('Reset two-factor authentication');
  320. expect(button()).toBeEnabled();
  321. expect(tooltip()).not.toBeInTheDocument();
  322. };
  323. const expectButtonDisabled = async title => {
  324. expect(button()).toHaveTextContent('Reset two-factor authentication');
  325. expect(button()).toBeDisabled();
  326. userEvent.hover(button());
  327. expect(await screen.findByText(title)).toBeInTheDocument();
  328. };
  329. it('does not show for pending member', function () {
  330. render(<OrganizationMemberDetail params={{memberId: pendingMember.id}} />, {
  331. context: routerContext,
  332. });
  333. expect(button()).not.toBeInTheDocument();
  334. });
  335. it('shows tooltip for joined member without permission to edit', async function () {
  336. render(<OrganizationMemberDetail params={{memberId: noAccess.id}} />, {
  337. context: routerContext,
  338. });
  339. await expectButtonDisabled('You do not have permission to perform this action');
  340. });
  341. it('shows tooltip for member without 2fa', async function () {
  342. render(<OrganizationMemberDetail params={{memberId: no2fa.id}} />, {
  343. context: routerContext,
  344. });
  345. await expectButtonDisabled('Not enrolled in two-factor authentication');
  346. });
  347. it('can reset member 2FA', function () {
  348. const deleteMocks = has2fa.user.authenticators.map(auth =>
  349. MockApiClient.addMockResponse({
  350. url: `/users/${has2fa.user.id}/authenticators/${auth.id}/`,
  351. method: 'DELETE',
  352. })
  353. );
  354. render(<OrganizationMemberDetail params={{memberId: has2fa.id}} />, {
  355. context: routerContext,
  356. });
  357. renderGlobalModal();
  358. expectButtonEnabled();
  359. userEvent.click(button());
  360. userEvent.click(screen.getByRole('button', {name: 'Confirm'}));
  361. deleteMocks.forEach(deleteMock => {
  362. expect(deleteMock).toHaveBeenCalled();
  363. });
  364. });
  365. it('shows tooltip for member in multiple orgs', async function () {
  366. render(<OrganizationMemberDetail params={{memberId: multipleOrgs.id}} />, {
  367. context: routerContext,
  368. });
  369. await expectButtonDisabled(
  370. 'Cannot be reset since user is in more than one organization'
  371. );
  372. });
  373. it('shows tooltip for member in 2FA required org', async function () {
  374. organization.require2FA = true;
  375. MockApiClient.addMockResponse({
  376. url: `/organizations/${organization.slug}/members/${has2fa.id}/`,
  377. body: has2fa,
  378. });
  379. render(<OrganizationMemberDetail params={{memberId: has2fa.id}} />, {
  380. context: routerContext,
  381. });
  382. await expectButtonDisabled(
  383. 'Cannot be reset since two-factor is required for this organization'
  384. );
  385. });
  386. });
  387. });