organizationMemberRow.spec.tsx 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348
  1. import {MemberFixture} from 'sentry-fixture/member';
  2. import {OrganizationFixture} from 'sentry-fixture/organization';
  3. import {TeamFixture} from 'sentry-fixture/team';
  4. import {UserFixture} from 'sentry-fixture/user';
  5. import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary';
  6. import {OrgRoleFixture} from 'sentry/types/role';
  7. import OrganizationMemberRow from 'sentry/views/settings/organizationMembers/organizationMemberRow';
  8. describe('OrganizationMemberRow', function () {
  9. const member = MemberFixture({
  10. id: '1',
  11. email: '',
  12. name: '',
  13. orgRole: 'member',
  14. roleName: 'Member',
  15. pending: false,
  16. flags: {
  17. 'sso:linked': false,
  18. 'idp:provisioned': false,
  19. 'idp:role-restricted': false,
  20. 'member-limit:restricted': false,
  21. 'partnership:restricted': false,
  22. 'sso:invalid': false,
  23. },
  24. user: UserFixture({
  25. id: '',
  26. has2fa: false,
  27. name: 'sentry@test.com',
  28. }),
  29. groupOrgRoles: [],
  30. });
  31. const managerTeam = TeamFixture({
  32. orgRole: 'manager',
  33. });
  34. const memberOnManagerTeam = MemberFixture({
  35. id: '2',
  36. orgRole: 'member',
  37. teams: [managerTeam.slug],
  38. groupOrgRoles: [
  39. {
  40. teamSlug: managerTeam.slug,
  41. role: OrgRoleFixture({name: 'Manager'}),
  42. },
  43. ],
  44. });
  45. const currentUser = UserFixture({
  46. id: '2',
  47. email: 'currentUser@email.com',
  48. });
  49. const defaultProps: React.ComponentProps<typeof OrganizationMemberRow> = {
  50. organization: OrganizationFixture(),
  51. status: '',
  52. requireLink: false,
  53. memberCanLeave: false,
  54. canAddMembers: false,
  55. canRemoveMembers: false,
  56. member,
  57. currentUser,
  58. onSendInvite: () => {},
  59. onRemove: () => {},
  60. onLeave: () => {},
  61. };
  62. function resendButton() {
  63. return screen.queryByRole('button', {name: 'Resend invite'});
  64. }
  65. function resendSsoButton() {
  66. return screen.queryByRole('button', {name: 'Resend SSO link'});
  67. }
  68. function leaveButton() {
  69. return screen.queryByRole('button', {name: 'Leave'});
  70. }
  71. function removeButton() {
  72. return screen.queryByRole('button', {name: 'Remove'});
  73. }
  74. describe('two factor', function () {
  75. it('does not have 2fa warning if user has 2fa', function () {
  76. render(
  77. <OrganizationMemberRow
  78. {...defaultProps}
  79. member={MemberFixture({
  80. ...member,
  81. user: UserFixture({...member.user, has2fa: true}),
  82. })}
  83. />
  84. );
  85. expect(screen.getByText('2FA Enabled')).toBeInTheDocument();
  86. expect(screen.queryByText('2FA Not Enabled')).not.toBeInTheDocument();
  87. });
  88. it('has 2fa warning if user does not have 2fa enabled', function () {
  89. render(
  90. <OrganizationMemberRow
  91. {...defaultProps}
  92. member={{
  93. ...member,
  94. user: UserFixture({...member.user, has2fa: false}),
  95. }}
  96. />
  97. );
  98. expect(screen.getByText('2FA Not Enabled')).toBeInTheDocument();
  99. expect(screen.queryByText('2FA Enabled')).not.toBeInTheDocument();
  100. });
  101. });
  102. describe('Pending user', function () {
  103. const props = {
  104. ...defaultProps,
  105. member: {...member, pending: true},
  106. };
  107. it('has "Invited" status, no "Resend Invite"', function () {
  108. render(<OrganizationMemberRow {...props} />);
  109. expect(screen.getByTestId('member-role')).toHaveTextContent('Invited Member');
  110. expect(resendButton()).toBeDisabled();
  111. });
  112. it('has "Resend Invite" button only if `canAddMembers` is true', function () {
  113. render(<OrganizationMemberRow {...props} canAddMembers />);
  114. expect(screen.getByTestId('member-role')).toHaveTextContent('Invited Member');
  115. expect(resendButton()).toBeEnabled();
  116. });
  117. it('has the right inviting states', function () {
  118. render(<OrganizationMemberRow {...props} canAddMembers />);
  119. expect(resendButton()).toBeInTheDocument();
  120. });
  121. it('has loading state', function () {
  122. render(<OrganizationMemberRow {...props} canAddMembers status="loading" />);
  123. // Should have loader
  124. expect(screen.getByTestId('loading-indicator')).toBeInTheDocument();
  125. // No Resend Invite button
  126. expect(resendButton()).not.toBeInTheDocument();
  127. });
  128. it('has success status', function () {
  129. render(<OrganizationMemberRow {...props} canAddMembers status="success" />);
  130. // Should not have loader
  131. expect(screen.queryByTestId('loading-indicator')).not.toBeInTheDocument();
  132. // No Resend Invite button
  133. expect(resendButton()).not.toBeInTheDocument();
  134. expect(screen.getByTestId('member-status')).toHaveTextContent('Sent!');
  135. });
  136. });
  137. describe('Expired user', function () {
  138. it('has "Expired" status', function () {
  139. render(
  140. <OrganizationMemberRow
  141. {...defaultProps}
  142. canAddMembers
  143. member={{...member, pending: true, expired: true}}
  144. />
  145. );
  146. expect(screen.getByTestId('member-role')).toHaveTextContent('Expired Invite');
  147. expect(resendButton()).toBeEnabled();
  148. });
  149. });
  150. describe('Requires SSO Link', function () {
  151. const props = {
  152. ...defaultProps,
  153. flags: {'sso:link': false},
  154. requireLink: true,
  155. };
  156. it('shows "Invited" status if user has not registered and not linked', function () {
  157. render(
  158. <OrganizationMemberRow
  159. {...props}
  160. canAddMembers
  161. member={{...member, pending: true}}
  162. />
  163. );
  164. expect(screen.getByTestId('member-role')).toHaveTextContent('Invited Member');
  165. expect(resendButton()).toBeEnabled();
  166. });
  167. it('shows "missing SSO link" message if user is registered and needs link', function () {
  168. render(<OrganizationMemberRow {...props} />);
  169. expect(screen.getByTestId('member-role')).toHaveTextContent('Member');
  170. expect(resendSsoButton()).toBeDisabled();
  171. });
  172. it('has "Resend SSO link" button only if `canAddMembers` is true and no link', function () {
  173. render(<OrganizationMemberRow {...props} canAddMembers />);
  174. expect(resendSsoButton()).toBeEnabled();
  175. });
  176. it('has 2fa warning if user is linked does not have 2fa enabled', function () {
  177. render(
  178. <OrganizationMemberRow
  179. {...defaultProps}
  180. member={{
  181. ...member,
  182. flags: {
  183. 'sso:linked': true,
  184. 'idp:provisioned': false,
  185. 'idp:role-restricted': false,
  186. 'member-limit:restricted': false,
  187. 'partnership:restricted': false,
  188. 'sso:invalid': false,
  189. },
  190. user: UserFixture({...member.user, has2fa: false}),
  191. }}
  192. />
  193. );
  194. expect(screen.getByText('2FA Not Enabled')).toBeInTheDocument();
  195. expect(screen.queryByText('2FA Enabled')).not.toBeInTheDocument();
  196. });
  197. });
  198. describe('Is Current User', function () {
  199. const props = {
  200. ...defaultProps,
  201. member: {...member, email: 'currentUser@email.com'},
  202. };
  203. it('has button to leave organization and no button to remove', function () {
  204. render(<OrganizationMemberRow {...props} memberCanLeave />);
  205. expect(leaveButton()).toBeInTheDocument();
  206. expect(removeButton()).not.toBeInTheDocument();
  207. });
  208. it('has disabled button to leave organization and no button to remove when member can not leave', function () {
  209. render(<OrganizationMemberRow {...props} memberCanLeave={false} />);
  210. expect(leaveButton()).toBeDisabled();
  211. expect(removeButton()).not.toBeInTheDocument();
  212. });
  213. });
  214. describe('IDP flags permissions', function () {
  215. member.flags['idp:provisioned'] = true;
  216. it('current user cannot leave if idp:provisioned', function () {
  217. const props = {
  218. ...defaultProps,
  219. member: {...member, email: 'currentUser@email.com'},
  220. };
  221. render(
  222. <OrganizationMemberRow
  223. {...props}
  224. memberCanLeave={!member.flags['idp:provisioned']}
  225. />
  226. );
  227. expect(leaveButton()).toBeDisabled();
  228. });
  229. it('cannot remove member if member is idp:provisioned', function () {
  230. render(<OrganizationMemberRow {...defaultProps} />);
  231. expect(removeButton()).toBeDisabled();
  232. });
  233. });
  234. describe('Not Current User', function () {
  235. const props = {
  236. ...defaultProps,
  237. };
  238. it('does not have Leave button', function () {
  239. render(<OrganizationMemberRow {...props} memberCanLeave />);
  240. expect(leaveButton()).not.toBeInTheDocument();
  241. });
  242. it('has Remove disabled button when `canRemoveMembers` is false', function () {
  243. member.flags['idp:provisioned'] = false;
  244. render(<OrganizationMemberRow {...props} />);
  245. expect(removeButton()).toBeDisabled();
  246. });
  247. it('has Remove button when `canRemoveMembers` is true', function () {
  248. member.flags['idp:provisioned'] = false;
  249. render(<OrganizationMemberRow {...props} canRemoveMembers />);
  250. expect(removeButton()).toBeEnabled();
  251. });
  252. });
  253. describe('render org role', function () {
  254. it('renders org role without tooltip if no org roles from team membership', function () {
  255. render(
  256. <OrganizationMemberRow
  257. {...defaultProps}
  258. member={{...member, user: UserFixture({...member.user})}}
  259. />
  260. );
  261. expect(screen.getByText('Member')).toBeInTheDocument();
  262. const questionTooltip = screen.queryByTestId('more-information');
  263. expect(questionTooltip).not.toBeInTheDocument();
  264. });
  265. });
  266. it('renders org role info tooltip if member has org roles from team membership', async function () {
  267. render(
  268. <OrganizationMemberRow
  269. {...defaultProps}
  270. member={{
  271. ...memberOnManagerTeam,
  272. user: UserFixture({...memberOnManagerTeam.user}),
  273. }}
  274. />
  275. );
  276. const questionTooltip = screen.getByTestId('more-information');
  277. expect(questionTooltip).toBeInTheDocument();
  278. await userEvent.hover(questionTooltip);
  279. await waitFor(() => {
  280. expect(screen.getByText(`#${managerTeam.slug}`)).toBeInTheDocument();
  281. expect(screen.getByText(': Manager')).toBeInTheDocument();
  282. });
  283. });
  284. });