organizationMembersList.spec.tsx 16 KB

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