organizationMembersList.spec.jsx 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536
  1. import {browserHistory} from 'react-router';
  2. import selectEvent from 'react-select-event';
  3. import {
  4. render,
  5. renderGlobalModal,
  6. screen,
  7. userEvent,
  8. waitFor,
  9. waitForElementToBeRemoved,
  10. } from 'sentry-test/reactTestingLibrary';
  11. import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator';
  12. import {Client} from 'sentry/api';
  13. import ConfigStore from 'sentry/stores/configStore';
  14. import OrganizationsStore from 'sentry/stores/organizationsStore';
  15. import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
  16. import OrganizationMembersList from 'sentry/views/settings/organizationMembers/organizationMembersList';
  17. jest.mock('sentry/utils/analytics/trackAdvancedAnalyticsEvent', () => jest.fn());
  18. jest.mock('sentry/api');
  19. jest.mock('sentry/actionCreators/indicator');
  20. const roles = [
  21. {
  22. id: 'admin',
  23. name: 'Admin',
  24. desc: 'This is the admin role',
  25. allowed: true,
  26. },
  27. {
  28. id: 'member',
  29. name: 'Member',
  30. desc: 'This is the member role',
  31. allowed: true,
  32. },
  33. ];
  34. describe('OrganizationMembersList', function () {
  35. const members = TestStubs.Members();
  36. const currentUser = members[1];
  37. const defaultProps = {
  38. orgId: 'org-slug',
  39. orgName: 'Organization Name',
  40. status: '',
  41. router: {routes: []},
  42. requireLink: false,
  43. memberCanLeave: false,
  44. canAddMembers: false,
  45. canRemoveMembers: false,
  46. currentUser,
  47. onSendInvite: () => {},
  48. onRemove: () => {},
  49. onLeave: () => {},
  50. location: {query: {}},
  51. };
  52. const organization = TestStubs.Organization({
  53. access: ['member:admin', 'org:admin', 'member:write'],
  54. status: {
  55. id: 'active',
  56. },
  57. });
  58. jest.spyOn(ConfigStore, 'get').mockImplementation(() => currentUser);
  59. afterAll(function () {
  60. ConfigStore.get.mockRestore();
  61. });
  62. beforeEach(function () {
  63. Client.clearMockResponses();
  64. Client.addMockResponse({
  65. url: '/organizations/org-id/members/me/',
  66. method: 'GET',
  67. body: {roles},
  68. });
  69. Client.addMockResponse({
  70. url: '/organizations/org-id/members/',
  71. method: 'GET',
  72. body: TestStubs.Members(),
  73. });
  74. Client.addMockResponse({
  75. url: '/organizations/org-id/access-requests/',
  76. method: 'GET',
  77. body: [
  78. {
  79. id: 'pending-id',
  80. member: {
  81. id: 'pending-member-id',
  82. email: '',
  83. name: '',
  84. role: '',
  85. roleName: '',
  86. user: {
  87. id: '',
  88. name: 'sentry@test.com',
  89. },
  90. },
  91. team: TestStubs.Team(),
  92. },
  93. ],
  94. });
  95. Client.addMockResponse({
  96. url: '/organizations/org-id/auth-provider/',
  97. method: 'GET',
  98. body: {
  99. ...TestStubs.AuthProvider(),
  100. require_link: true,
  101. },
  102. });
  103. Client.addMockResponse({
  104. url: '/organizations/org-id/teams/',
  105. method: 'GET',
  106. body: TestStubs.Team(),
  107. });
  108. Client.addMockResponse({
  109. url: '/organizations/org-id/invite-requests/',
  110. method: 'GET',
  111. body: [],
  112. });
  113. browserHistory.push.mockReset();
  114. OrganizationsStore.load([organization]);
  115. });
  116. it('can remove a member', async function () {
  117. const deleteMock = MockApiClient.addMockResponse({
  118. url: `/organizations/org-id/members/${members[0].id}/`,
  119. method: 'DELETE',
  120. });
  121. render(<OrganizationMembersList {...defaultProps} params={{orgId: 'org-id'}} />, {
  122. context: TestStubs.routerContext([{organization}]),
  123. });
  124. userEvent.click(screen.getAllByRole('button', {name: 'Remove'})[0]);
  125. renderGlobalModal();
  126. userEvent.click(screen.getByTestId('confirm-button'));
  127. await waitFor(() => expect(addSuccessMessage).toHaveBeenCalled());
  128. expect(deleteMock).toHaveBeenCalled();
  129. expect(browserHistory.push).not.toHaveBeenCalled();
  130. expect(OrganizationsStore.getAll()).toEqual([organization]);
  131. });
  132. it('displays error message when failing to remove member', async function () {
  133. const deleteMock = MockApiClient.addMockResponse({
  134. url: `/organizations/org-id/members/${members[0].id}/`,
  135. method: 'DELETE',
  136. statusCode: 500,
  137. });
  138. render(<OrganizationMembersList {...defaultProps} params={{orgId: 'org-id'}} />, {
  139. context: TestStubs.routerContext([{organization}]),
  140. });
  141. userEvent.click(screen.getAllByRole('button', {name: 'Remove'})[0]);
  142. renderGlobalModal();
  143. userEvent.click(screen.getByTestId('confirm-button'));
  144. await waitFor(() => expect(addErrorMessage).toHaveBeenCalled());
  145. expect(deleteMock).toHaveBeenCalled();
  146. expect(browserHistory.push).not.toHaveBeenCalled();
  147. expect(OrganizationsStore.getAll()).toEqual([organization]);
  148. });
  149. it('can leave org', async function () {
  150. const deleteMock = Client.addMockResponse({
  151. url: `/organizations/org-id/members/${members[1].id}/`,
  152. method: 'DELETE',
  153. });
  154. render(<OrganizationMembersList {...defaultProps} params={{orgId: 'org-id'}} />, {
  155. context: TestStubs.routerContext([{organization}]),
  156. });
  157. userEvent.click(screen.getAllByRole('button', {name: 'Leave'})[0]);
  158. renderGlobalModal();
  159. userEvent.click(screen.getByTestId('confirm-button'));
  160. await waitFor(() => expect(addSuccessMessage).toHaveBeenCalled());
  161. expect(deleteMock).toHaveBeenCalled();
  162. expect(browserHistory.push).toHaveBeenCalledTimes(1);
  163. expect(browserHistory.push).toHaveBeenCalledWith('/organizations/new/');
  164. });
  165. it('can redirect to remaining org after leaving', async function () {
  166. const deleteMock = Client.addMockResponse({
  167. url: `/organizations/org-id/members/${members[1].id}/`,
  168. method: 'DELETE',
  169. });
  170. const secondOrg = TestStubs.Organization({
  171. slug: 'org-two',
  172. status: {
  173. id: 'active',
  174. },
  175. });
  176. OrganizationsStore.addOrReplace(secondOrg);
  177. render(<OrganizationMembersList {...defaultProps} params={{orgId: 'org-id'}} />, {
  178. context: TestStubs.routerContext([{organization}]),
  179. });
  180. userEvent.click(screen.getAllByRole('button', {name: 'Leave'})[0]);
  181. renderGlobalModal();
  182. userEvent.click(screen.getByTestId('confirm-button'));
  183. await waitFor(() => expect(addSuccessMessage).toHaveBeenCalled());
  184. expect(deleteMock).toHaveBeenCalled();
  185. expect(browserHistory.push).toHaveBeenCalledTimes(1);
  186. expect(browserHistory.push).toHaveBeenCalledWith(`/${secondOrg.slug}/`);
  187. expect(OrganizationsStore.getAll()).toEqual([secondOrg]);
  188. });
  189. it('displays error message when failing to leave org', async function () {
  190. const deleteMock = Client.addMockResponse({
  191. url: `/organizations/org-id/members/${members[1].id}/`,
  192. method: 'DELETE',
  193. statusCode: 500,
  194. });
  195. render(<OrganizationMembersList {...defaultProps} params={{orgId: 'org-id'}} />, {
  196. context: TestStubs.routerContext([{organization}]),
  197. });
  198. userEvent.click(screen.getAllByRole('button', {name: 'Leave'})[0]);
  199. renderGlobalModal();
  200. userEvent.click(screen.getByTestId('confirm-button'));
  201. await waitFor(() => expect(addErrorMessage).toHaveBeenCalled());
  202. expect(deleteMock).toHaveBeenCalled();
  203. expect(browserHistory.push).not.toHaveBeenCalled();
  204. expect(OrganizationsStore.getAll()).toEqual([organization]);
  205. });
  206. it('can re-send SSO link to member', function () {
  207. const inviteMock = MockApiClient.addMockResponse({
  208. url: `/organizations/org-id/members/${members[0].id}/`,
  209. method: 'PUT',
  210. body: {
  211. id: '1234',
  212. },
  213. });
  214. render(<OrganizationMembersList {...defaultProps} params={{orgId: 'org-id'}} />, {
  215. context: TestStubs.routerContext([{organization}]),
  216. });
  217. expect(inviteMock).not.toHaveBeenCalled();
  218. userEvent.click(screen.getByRole('button', {name: 'Resend SSO link'}));
  219. expect(inviteMock).toHaveBeenCalled();
  220. });
  221. it('can re-send invite to member', function () {
  222. const inviteMock = MockApiClient.addMockResponse({
  223. url: `/organizations/org-id/members/${members[1].id}/`,
  224. method: 'PUT',
  225. body: {
  226. id: '1234',
  227. },
  228. });
  229. render(<OrganizationMembersList {...defaultProps} params={{orgId: 'org-id'}} />, {
  230. context: TestStubs.routerContext([{organization}]),
  231. });
  232. expect(inviteMock).not.toHaveBeenCalled();
  233. userEvent.click(screen.getByRole('button', {name: 'Resend invite'}));
  234. expect(inviteMock).toHaveBeenCalled();
  235. });
  236. it('can search organization members', function () {
  237. const searchMock = MockApiClient.addMockResponse({
  238. url: '/organizations/org-id/members/',
  239. body: [],
  240. });
  241. const routerContext = TestStubs.routerContext();
  242. render(<OrganizationMembersList {...defaultProps} params={{orgId: 'org-id'}} />, {
  243. context: routerContext,
  244. });
  245. userEvent.type(screen.getByPlaceholderText('Search Members'), 'member');
  246. expect(searchMock).toHaveBeenLastCalledWith(
  247. '/organizations/org-id/members/',
  248. expect.objectContaining({
  249. method: 'GET',
  250. query: {
  251. query: 'member',
  252. },
  253. })
  254. );
  255. userEvent.keyboard('{enter}');
  256. expect(routerContext.context.router.push).toHaveBeenCalledTimes(1);
  257. });
  258. it('can filter members', function () {
  259. const searchMock = MockApiClient.addMockResponse({
  260. url: '/organizations/org-id/members/',
  261. body: [],
  262. });
  263. const routerContext = TestStubs.routerContext();
  264. render(<OrganizationMembersList {...defaultProps} params={{orgId: 'org-id'}} />, {
  265. context: routerContext,
  266. });
  267. userEvent.click(screen.getByRole('button', {name: 'Filter'}));
  268. userEvent.click(screen.getByRole('checkbox', {name: 'Member'}));
  269. expect(searchMock).toHaveBeenLastCalledWith(
  270. '/organizations/org-id/members/',
  271. expect.objectContaining({
  272. method: 'GET',
  273. query: {query: 'role:member'},
  274. })
  275. );
  276. userEvent.click(screen.getByRole('checkbox', {name: 'Member'}));
  277. for (const [filter, label] of [
  278. ['isInvited', 'Invited'],
  279. ['has2fa', '2FA'],
  280. ['ssoLinked', 'SSO Linked'],
  281. ]) {
  282. userEvent.click(screen.getByRole('checkbox', {name: `Enable ${label} filter`}));
  283. expect(searchMock).toHaveBeenLastCalledWith(
  284. '/organizations/org-id/members/',
  285. expect.objectContaining({
  286. method: 'GET',
  287. query: {query: `${filter}:true`},
  288. })
  289. );
  290. userEvent.click(screen.getByRole('checkbox', {name: `Toggle ${label}`}));
  291. expect(searchMock).toHaveBeenLastCalledWith(
  292. '/organizations/org-id/members/',
  293. expect.objectContaining({
  294. method: 'GET',
  295. query: {query: `${filter}:false`},
  296. })
  297. );
  298. userEvent.click(screen.getByRole('checkbox', {name: `Enable ${label} filter`}));
  299. }
  300. });
  301. describe('OrganizationInviteRequests', function () {
  302. const inviteRequest = TestStubs.Member({
  303. id: '123',
  304. user: null,
  305. inviteStatus: 'requested_to_be_invited',
  306. inviter: TestStubs.User(),
  307. role: 'member',
  308. teams: [],
  309. });
  310. const joinRequest = TestStubs.Member({
  311. id: '456',
  312. user: null,
  313. email: 'test@gmail.com',
  314. inviteStatus: 'requested_to_join',
  315. role: 'member',
  316. teams: [],
  317. });
  318. it('disable buttons for no access', function () {
  319. const org = TestStubs.Organization({
  320. status: {
  321. id: 'active',
  322. },
  323. });
  324. MockApiClient.addMockResponse({
  325. url: '/organizations/org-id/invite-requests/',
  326. method: 'GET',
  327. body: [inviteRequest],
  328. });
  329. MockApiClient.addMockResponse({
  330. url: `/organizations/org-id/invite-requests/${inviteRequest.id}/`,
  331. method: 'PUT',
  332. });
  333. render(
  334. <OrganizationMembersList
  335. {...defaultProps}
  336. params={{orgId: 'org-id'}}
  337. organization={org}
  338. />,
  339. {context: TestStubs.routerContext([{organization: org}])}
  340. );
  341. expect(screen.getByText('Pending Members')).toBeInTheDocument();
  342. expect(screen.getByRole('button', {name: 'Approve'})).toBeDisabled();
  343. });
  344. it('can approve invite request and update', async function () {
  345. const org = TestStubs.Organization({
  346. access: ['member:admin', 'org:admin', 'member:write'],
  347. status: {
  348. id: 'active',
  349. },
  350. });
  351. MockApiClient.addMockResponse({
  352. url: '/organizations/org-id/invite-requests/',
  353. method: 'GET',
  354. body: [inviteRequest],
  355. });
  356. MockApiClient.addMockResponse({
  357. url: `/organizations/org-id/invite-requests/${inviteRequest.id}/`,
  358. method: 'PUT',
  359. });
  360. render(
  361. <OrganizationMembersList
  362. {...defaultProps}
  363. params={{orgId: 'org-id'}}
  364. organization={org}
  365. />,
  366. {context: TestStubs.routerContext([{organization: org}])}
  367. );
  368. expect(screen.getByText('Pending Members')).toBeInTheDocument();
  369. userEvent.click(screen.getByRole('button', {name: 'Approve'}));
  370. renderGlobalModal();
  371. userEvent.click(screen.getByTestId('confirm-button'));
  372. await waitForElementToBeRemoved(() => screen.queryByText('Pending Members'));
  373. expect(trackAdvancedAnalyticsEvent).toHaveBeenCalledWith(
  374. 'invite_request.approved',
  375. {
  376. invite_status: inviteRequest.inviteStatus,
  377. member_id: parseInt(inviteRequest.id, 10),
  378. organization: org,
  379. }
  380. );
  381. });
  382. it('can deny invite request and remove', async function () {
  383. const org = TestStubs.Organization({
  384. access: ['member:admin', 'org:admin', 'member:write'],
  385. status: {
  386. id: 'active',
  387. },
  388. });
  389. MockApiClient.addMockResponse({
  390. url: '/organizations/org-id/invite-requests/',
  391. method: 'GET',
  392. body: [joinRequest],
  393. });
  394. MockApiClient.addMockResponse({
  395. url: `/organizations/org-id/invite-requests/${joinRequest.id}/`,
  396. method: 'DELETE',
  397. });
  398. render(
  399. <OrganizationMembersList
  400. {...defaultProps}
  401. params={{orgId: 'org-id'}}
  402. organization={org}
  403. />,
  404. {context: TestStubs.routerContext([{organization: org}])}
  405. );
  406. expect(screen.getByText('Pending Members')).toBeInTheDocument();
  407. userEvent.click(screen.getByRole('button', {name: 'Deny'}));
  408. await waitForElementToBeRemoved(() => screen.queryByText('Pending Members'));
  409. expect(trackAdvancedAnalyticsEvent).toHaveBeenCalledWith('invite_request.denied', {
  410. invite_status: joinRequest.inviteStatus,
  411. member_id: parseInt(joinRequest.id, 10),
  412. organization: org,
  413. });
  414. });
  415. it('can update invite requests', async function () {
  416. const org = TestStubs.Organization({
  417. access: ['member:admin', 'org:admin', 'member:write'],
  418. status: {
  419. id: 'active',
  420. },
  421. });
  422. MockApiClient.addMockResponse({
  423. url: '/organizations/org-id/invite-requests/',
  424. method: 'GET',
  425. body: [inviteRequest],
  426. });
  427. const updateWithApprove = MockApiClient.addMockResponse({
  428. url: `/organizations/org-id/invite-requests/${inviteRequest.id}/`,
  429. method: 'PUT',
  430. });
  431. render(
  432. <OrganizationMembersList
  433. {...defaultProps}
  434. params={{orgId: 'org-id'}}
  435. organization={org}
  436. />,
  437. {context: TestStubs.routerContext([{organization: org}])}
  438. );
  439. await selectEvent.select(screen.getAllByRole('textbox')[1], ['Admin']);
  440. userEvent.click(screen.getByRole('button', {name: 'Approve'}));
  441. renderGlobalModal();
  442. userEvent.click(screen.getByTestId('confirm-button'));
  443. expect(updateWithApprove).toHaveBeenCalledWith(
  444. `/organizations/org-id/invite-requests/${inviteRequest.id}/`,
  445. expect.objectContaining({data: expect.objectContaining({role: 'admin'})})
  446. );
  447. });
  448. });
  449. });