diff --git a/cypress/e2e/users.cy.ts b/cypress/e2e/users.cy.ts
index 5778f6a93..7dca7b118 100644
--- a/cypress/e2e/users.cy.ts
+++ b/cypress/e2e/users.cy.ts
@@ -180,7 +180,7 @@ describe('Users', () => {
});
});
- describe('modify authorised routes ', () => {
+ describe('modify authorised routes', () => {
beforeEach(() => {
cy.visit('/admin/users');
cy.findAllByRole('button', { name: 'Row Actions' }).first().click();
@@ -223,4 +223,34 @@ describe('Users', () => {
);
});
});
+
+ describe('delete users', () => {
+ beforeEach(() => {
+ cy.visit('/admin/users');
+ cy.findAllByRole('button', { name: 'Row Actions' }).first().click();
+ cy.findByText('Delete').click();
+ });
+
+ afterEach(() => {
+ cy.clearMocks();
+ });
+
+ it('sends a delete request when an admin deletes a user', () => {
+ cy.findAllByTestId('delete-user-name').should('have.text', 'user1');
+
+ cy.startSnoopingBrowserMockedRequest();
+
+ cy.findByRole('button', { name: 'Continue' }).click();
+
+ cy.findBrowserMockedRequests({
+ method: 'DELETE',
+ url: '/users/:id',
+ }).should((deleteRequests) => {
+ expect(deleteRequests.length).equal(1);
+ const request = deleteRequests[0];
+
+ expect(request.url.toString()).to.contain('user1');
+ });
+ });
+ });
});
diff --git a/src/admin/users/deleteUserDialogue.component.test.tsx b/src/admin/users/deleteUserDialogue.component.test.tsx
new file mode 100644
index 000000000..30d96191c
--- /dev/null
+++ b/src/admin/users/deleteUserDialogue.component.test.tsx
@@ -0,0 +1,70 @@
+import { RenderResult, screen, waitFor } from '@testing-library/react';
+import userEvent, { UserEvent } from '@testing-library/user-event';
+import UsersJson from '../../mocks/users.json';
+import { renderComponentWithProviders } from '../../testUtils';
+import DeleteUserDialogue, {
+ DeleteUserDialogueProps,
+} from './deleteUserDialogue.component';
+
+describe('delete user dialogue', () => {
+ let props: DeleteUserDialogueProps;
+ let user: UserEvent;
+ const onClose = vi.fn();
+
+ const createView = (): RenderResult => {
+ return renderComponentWithProviders();
+ };
+
+ beforeEach(() => {
+ props = {
+ open: true,
+ onClose: onClose,
+ selectedUser: UsersJson[0],
+ };
+ user = userEvent.setup();
+ });
+ afterEach(() => {
+ vi.clearAllMocks();
+ });
+ it('renders correctly', async () => {
+ createView();
+ expect(screen.getByText('Delete User')).toBeInTheDocument();
+ expect(screen.getByTestId('delete-user-name')).toHaveTextContent('user1');
+ });
+
+ it('calls onClose when Close button is clicked', async () => {
+ createView();
+ const closeButton = screen.getByRole('button', { name: 'Close' });
+ await user.click(closeButton);
+
+ await waitFor(() => {
+ expect(onClose).toHaveBeenCalled();
+ });
+ });
+
+ it('displays warning message when user data does not exist in database', async () => {
+ props = {
+ ...props,
+ selectedUser: { ...UsersJson[0], username: 'test' },
+ };
+ createView();
+
+ const continueButton = screen.getByRole('button', { name: 'Continue' });
+ await user.click(continueButton);
+ const helperTexts = await screen.findByText(
+ `username field must exist in the database. You put: 'test'`
+ );
+ expect(helperTexts).toBeInTheDocument();
+ expect(onClose).not.toHaveBeenCalled();
+ });
+
+ it('calls handleDeleteUser when continue button is clicked with a valid user name', async () => {
+ createView();
+ const continueButton = screen.getByRole('button', { name: 'Continue' });
+ await user.click(continueButton);
+
+ await waitFor(() => {
+ expect(onClose).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/src/admin/users/deleteUserDialogue.component.tsx b/src/admin/users/deleteUserDialogue.component.tsx
new file mode 100644
index 000000000..8c61bab9b
--- /dev/null
+++ b/src/admin/users/deleteUserDialogue.component.tsx
@@ -0,0 +1,64 @@
+import {
+ Button,
+ Dialog,
+ DialogActions,
+ DialogContent,
+ DialogTitle,
+ FormHelperText,
+} from '@mui/material';
+import { AxiosError } from 'axios';
+import React from 'react';
+import { useDeleteUser } from '../../api/user';
+import { APIError, User } from '../../app.types';
+
+export interface DeleteUserDialogueProps {
+ open: boolean;
+ onClose: () => void;
+ selectedUser: User;
+}
+
+const DeleteUserDialogue = (props: DeleteUserDialogueProps) => {
+ const { open, onClose, selectedUser } = props;
+
+ const [errorMessage, setErrorMessage] = React.useState(
+ undefined
+ );
+
+ const { mutateAsync: deleteUser } = useDeleteUser();
+
+ const handleClose = React.useCallback(() => {
+ onClose();
+ setErrorMessage(undefined);
+ }, [onClose]);
+
+ const handleDeleteUser = React.useCallback(() => {
+ deleteUser(selectedUser.username)
+ .then(() => {
+ handleClose();
+ })
+ .catch((error: AxiosError) => {
+ const errorDetail = (error.response?.data as APIError).detail;
+ setErrorMessage(errorDetail as string);
+ });
+ }, [deleteUser, handleClose, selectedUser]);
+
+ return (
+
+ );
+};
+
+export default DeleteUserDialogue;
diff --git a/src/admin/users/usersTable.component.test.tsx b/src/admin/users/usersTable.component.test.tsx
index 4f1ff513c..76e7d8558 100644
--- a/src/admin/users/usersTable.component.test.tsx
+++ b/src/admin/users/usersTable.component.test.tsx
@@ -96,6 +96,33 @@ describe('UsersTable Snapshot', () => {
});
});
+ it('opens delete dialog and closes it correctly', async () => {
+ createView();
+ await waitFor(() => {
+ expect(screen.getByText('user1')).toBeInTheDocument();
+ });
+
+ const addButtons = screen.getAllByRole('button', { name: 'Row Actions' });
+ await user.click(addButtons[0]);
+
+ await user.click(screen.getByText('Delete'));
+
+ await waitFor(() => {
+ expect(
+ screen.getByRole('dialog', { name: 'Delete User' })
+ ).toBeInTheDocument();
+ });
+
+ const closeButton = screen.getByRole('button', { name: 'Close' });
+ await user.click(closeButton);
+
+ await waitFor(() => {
+ expect(
+ screen.queryByRole('dialog', { name: 'Delete User' })
+ ).not.toBeInTheDocument();
+ });
+ });
+
it('sets the table filters and clears the table filters', async () => {
createView();
diff --git a/src/admin/users/usersTable.component.tsx b/src/admin/users/usersTable.component.tsx
index 108d8d725..0761868da 100644
--- a/src/admin/users/usersTable.component.tsx
+++ b/src/admin/users/usersTable.component.tsx
@@ -1,5 +1,6 @@
import AddIcon from '@mui/icons-material/Add';
import ClearIcon from '@mui/icons-material/Clear';
+import DeleteIcon from '@mui/icons-material/Delete';
import EditIcon from '@mui/icons-material/Edit';
import PasswordIcon from '@mui/icons-material/Password';
import {
@@ -20,6 +21,7 @@ import { MRT_Localization_EN } from 'material-react-table/locales/en';
import React from 'react';
import { useUsers } from '../../api/user';
import { User } from '../../app.types';
+import DeleteUserDialogue from './deleteUserDialogue.component';
import UserDialogue from './userDialogue.component';
export const AUTHORISED_ROUTE_LIST = [
@@ -37,7 +39,7 @@ function UsersTable() {
const { data: userData, isLoading: userDataLoading } = useUsers();
const [requestType, setRequestType] = React.useState<
- 'patchPassword' | 'patchAuthorisedRoutes' | 'post'
+ 'patchPassword' | 'patchAuthorisedRoutes' | 'post' | 'delete' | false
>('post');
const [selectedUser, setSelectedUser] = React.useState(
@@ -201,10 +203,38 @@ function UsersTable() {
,
]
: []),
+ ,
];
},
});
- return ;
+ return (
+ <>
+
+ {selectedUser && (
+ {
+ setRequestType(false);
+ }}
+ selectedUser={selectedUser}
+ />
+ )}
+ >
+ );
}
export default UsersTable;
diff --git a/src/api/user.test.tsx b/src/api/user.test.tsx
index f8377f76b..666497855 100644
--- a/src/api/user.test.tsx
+++ b/src/api/user.test.tsx
@@ -2,7 +2,7 @@ import { renderHook, waitFor } from '@testing-library/react';
import type { UsersDict } from '../app.types';
import usersJson from '../mocks/users.json';
import { hooksWrapperWithProviders } from '../testUtils';
-import { useAddUser, useEditUser, useUsers } from './user';
+import { useAddUser, useDeleteUser, useEditUser, useUsers } from './user';
describe('useUsers', () => {
it('sends request to fetch users and returns successful response', async () => {
@@ -68,3 +68,24 @@ describe('useEditUser', () => {
'sends axios request to edit a user and throws an appropriate error on failure'
);
});
+
+describe('useDeleteUser', () => {
+ it('delete request to delete user and returns successful response', async () => {
+ const { result } = renderHook(() => useDeleteUser(), {
+ wrapper: hooksWrapperWithProviders(),
+ });
+ expect(result.current.isIdle).toBe(true);
+
+ result.current.mutate('user1');
+
+ await waitFor(() => {
+ expect(result.current.isSuccess).toBeTruthy();
+ });
+
+ expect(result.current.data).toEqual('');
+ });
+
+ it.todo(
+ 'sends axios request to delete user session and throws an appropriate error on failure'
+ );
+});
diff --git a/src/api/user.tsx b/src/api/user.tsx
index bb99632aa..febc3771d 100644
--- a/src/api/user.tsx
+++ b/src/api/user.tsx
@@ -59,3 +59,21 @@ export const useEditUser = (): UseMutationResult<
},
});
};
+
+const deleteUser = async (userId: string): Promise => {
+ return ogApi.delete(`/users/${userId}`).then((response) => response.data);
+};
+
+export const useDeleteUser = (): UseMutationResult<
+ void,
+ AxiosError,
+ string
+> => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: (userId: string) => deleteUser(userId),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['Users'] });
+ },
+ });
+};
diff --git a/src/mocks/handlers.ts b/src/mocks/handlers.ts
index 560fd3eec..80d326552 100644
--- a/src/mocks/handlers.ts
+++ b/src/mocks/handlers.ts
@@ -419,6 +419,21 @@ export const handlers = [
return HttpResponse.json(body._id, { status: 201 });
}),
+ http.delete('/users/:id', async ({ params }) => {
+ const { id } = params;
+ const validId = usersJson.map((user) => user.username);
+ if (validId.includes(id as string)) {
+ return new HttpResponse(null, { status: 204 });
+ } else {
+ return HttpResponse.json(
+ {
+ detail: `username field must exist in the database. You put: '${id}'`,
+ },
+ { status: 400 }
+ );
+ }
+ }),
+
http.post('/users/filters', async () => {
return HttpResponse.json('1', { status: 201 });
}),