diff --git a/services/web/frontend/js/features/group-management/components/members-table/unlink-user-modal.tsx b/services/web/frontend/js/features/group-management/components/members-table/unlink-user-modal.tsx index 13462e8947..73b0e39a67 100644 --- a/services/web/frontend/js/features/group-management/components/members-table/unlink-user-modal.tsx +++ b/services/web/frontend/js/features/group-management/components/members-table/unlink-user-modal.tsx @@ -32,10 +32,11 @@ export default function UnlinkUserModal({ if (!user.enrollment?.sso) { return } + const enrollment = Object.assign({}, user.enrollment, { + sso: user.enrollment.sso.filter(sso => sso.groupId !== groupId), + }) const updatedUser = Object.assign({}, user, { - enrollment: { - sso: user.enrollment.sso.filter(sso => sso.groupId !== groupId), - }, + enrollment, }) updateMemberView(user._id, updatedUser) }, [groupId, updateMemberView, user]) diff --git a/services/web/test/frontend/features/group-management/components/members-table/members-list.spec.tsx b/services/web/test/frontend/features/group-management/components/members-table/members-list.spec.tsx index 206d8ed204..abcc50ab67 100644 --- a/services/web/test/frontend/features/group-management/components/members-table/members-list.spec.tsx +++ b/services/web/test/frontend/features/group-management/components/members-table/members-list.spec.tsx @@ -1,5 +1,6 @@ import MembersList from '@/features/group-management/components/members-table/members-list' import { GroupMembersProvider } from '@/features/group-management/context/group-members-context' +import { User } from '../../../../../../types/group-management/user' const groupId = 'somegroup' @@ -110,4 +111,169 @@ describe('MembersList', function () { .should('have.length', 0) }) }) + + describe('SSO unlinking', function () { + const USER_PENDING_INVITE: User = { + _id: 'abc123def456', + first_name: 'John', + last_name: 'Doe', + email: 'john.doe@test.com', + last_active_at: new Date('2023-01-15'), + invite: true, + } + const USER_NOT_LINKED: User = { + _id: 'bcd234efa567', + first_name: 'Bobby', + last_name: 'Lapointe', + email: 'bobby.lapointe@test.com', + last_active_at: new Date('2023-01-02'), + invite: false, + } + const USER_LINKED: User = { + _id: 'defabc231453', + first_name: 'Claire', + last_name: 'Jennings', + email: 'claire.jennings@test.com', + last_active_at: new Date('2023-01-03'), + invite: false, + enrollment: { + sso: [ + { + groupId, + linkedAt: new Date('2023-01-03'), + primary: true, + }, + ], + }, + } + const USER_LINKED_AND_MANAGED: User = { + _id: 'defabc231453', + first_name: 'Jean-Luc', + last_name: 'Picard', + email: 'picard@test.com', + last_active_at: new Date('2023-01-03'), + invite: false, + enrollment: { + managedBy: groupId, + enrolledAt: new Date('2023-01-03'), + sso: [ + { + groupId, + linkedAt: new Date('2023-01-03'), + primary: true, + }, + ], + }, + } + const users = [ + USER_PENDING_INVITE, + USER_NOT_LINKED, + USER_LINKED, + USER_LINKED_AND_MANAGED, + ] + + beforeEach(function () { + cy.window().then(win => { + win.metaAttributesCache.set('ol-groupId', groupId) + win.metaAttributesCache.set('ol-users', users) + win.metaAttributesCache.set('ol-groupSSOActive', true) + }) + + cy.intercept('POST', `manage/groups/${groupId}/unlink-user/*`, { + statusCode: 200, + }) + }) + + describe('unlinking user', function () { + beforeEach(function () { + mountManagedUsersList() + cy.get('ul.managed-users-list table > tbody').within(() => { + cy.get('tr:nth-child(3)').within(() => { + cy.get('.sr-only').contains('SSO active') + cy.get('.action-btn').click() + cy.findByTestId('unlink-user-action').click() + }) + }) + }) + + it('should show successs notification and update the user row after unlinking', function () { + cy.get('.modal').within(() => { + cy.get('.btn-danger').click() + }) + cy.get('.notification').contains( + `SSO reauthentication request has been sent to ${USER_LINKED.email}` + ) + cy.get('ul.managed-users-list table > tbody').within(() => { + cy.get('tr:nth-child(3)').within(() => { + cy.get('.sr-only').contains('SSO not active') + }) + }) + }) + }) + + describe('managed users enabled', function () { + beforeEach(function () { + cy.window().then(win => { + win.metaAttributesCache.set('ol-managedUsersActive', true) + }) + mountManagedUsersList() + }) + + describe('when user is not managed', function () { + beforeEach(function () { + cy.get('ul.managed-users-list table > tbody').within(() => { + cy.get('tr:nth-child(3)').within(() => { + cy.get('.sr-only').contains('SSO active') + cy.get('.sr-only').contains('Not managed') + cy.get('.action-btn').click() + cy.findByTestId('unlink-user-action').click() + }) + }) + }) + + it('should show successs notification and update the user row after unlinking', function () { + cy.get('.modal').within(() => { + cy.get('.btn-danger').click() + }) + cy.get('.notification').contains( + `SSO reauthentication request has been sent to ${USER_LINKED.email}` + ) + cy.get('ul.managed-users-list table > tbody').within(() => { + cy.get('tr:nth-child(3)').within(() => { + cy.get('.sr-only').contains('SSO not active') + cy.get('.sr-only').contains('Not managed') + }) + }) + }) + }) + + describe('when user is managed', function () { + beforeEach(function () { + cy.get('ul.managed-users-list table > tbody').within(() => { + cy.get('tr:nth-child(4)').within(() => { + cy.get('.sr-only').contains('SSO active') + cy.get('.sr-only').contains('Managed') + cy.get('.action-btn').click() + cy.findByTestId('unlink-user-action').click() + }) + }) + }) + + it('should show successs notification and update the user row after unlinking', function () { + cy.get('.modal').within(() => { + cy.get('.btn-danger').click() + }) + cy.get('.notification').contains( + `SSO reauthentication request has been sent to ${USER_LINKED_AND_MANAGED.email}` + ) + cy.get('ul.managed-users-list table > tbody').within(() => { + cy.get('tr:nth-child(4)').within(() => { + cy.get('.sr-only').contains('SSO not active') + cy.get('.sr-only').contains('Managed') + }) + }) + }) + }) + }) + }) }) diff --git a/services/web/test/frontend/features/group-management/components/members-table/unlink-user-modal.test.tsx b/services/web/test/frontend/features/group-management/components/members-table/unlink-user-modal.test.tsx index d02fedcd35..4eea219475 100644 --- a/services/web/test/frontend/features/group-management/components/members-table/unlink-user-modal.test.tsx +++ b/services/web/test/frontend/features/group-management/components/members-table/unlink-user-modal.test.tsx @@ -1,9 +1,10 @@ -import { render, screen } from '@testing-library/react' +import { fireEvent, render, screen, waitFor } from '@testing-library/react' import { ReactElement } from 'react' import sinon from 'sinon' import fetchMock from 'fetch-mock' import UnlinkUserModal from '@/features/group-management/components/members-table/unlink-user-modal' import { GroupMembersProvider } from '@/features/group-management/context/group-members-context' +import { expect } from 'chai' export function renderWithContext(component: ReactElement, props = {}) { const GroupMembersProviderWrapper = ({ @@ -17,16 +18,21 @@ export function renderWithContext(component: ReactElement, props = {}) { describe('', function () { let defaultProps: any + const groupId = 'group123' + const userId = 'user123' beforeEach(function () { + window.metaAttributesCache = new Map() defaultProps = { onClose: sinon.stub(), - user: {}, + user: { _id: userId }, setGroupUserAlert: sinon.stub(), } + window.metaAttributesCache.set('ol-groupId', groupId) }) afterEach(function () { + window.metaAttributesCache = new Map() fetchMock.reset() }) @@ -35,5 +41,36 @@ describe('', function () { await screen.findByRole('heading', { name: 'Unlink user', }) + screen.getByText('You’re about to remove the SSO login option for', { + exact: false, + }) + }) + + it('closes the modal on success', async function () { + fetchMock.post(`/manage/groups/${groupId}/unlink-user/${userId}`, 200) + + renderWithContext() + await screen.findByRole('heading', { + name: 'Unlink user', + }) + + const confirmButton = screen.getByRole('button', { name: 'Unlink user' }) + fireEvent.click(confirmButton) + + await waitFor(() => expect(defaultProps.onClose).to.have.been.called) + }) + + it('handles errors', async function () { + fetchMock.post(`/manage/groups/${groupId}/unlink-user/${userId}`, 500) + + renderWithContext() + await screen.findByRole('heading', { + name: 'Unlink user', + }) + + const confirmButton = screen.getByRole('button', { name: 'Unlink user' }) + fireEvent.click(confirmButton) + + await waitFor(() => screen.findByText('Sorry, something went wrong')) }) })