diff --git a/src/features/profile/components/UserProfile/RenameUserForm.tsx b/src/features/profile/components/UserProfile/RenameUserForm.tsx index 3ead335b..f6a73cbf 100644 --- a/src/features/profile/components/UserProfile/RenameUserForm.tsx +++ b/src/features/profile/components/UserProfile/RenameUserForm.tsx @@ -6,6 +6,7 @@ import { FormMessage, SquareTextInput, } from '@/components/ui'; +import { renameUser } from '@/features/profile/lib/actions'; import { renameUserFormSchema, RenameUserInputs } from '@/features/profile/lib/schemas'; import { cn } from '@/lib/tailwind/utils'; import { IUser } from '@/types/definition'; @@ -23,7 +24,7 @@ interface IRenameUserFormProps { /** * The current name of the user to rename. */ - currentUserName: IUser['name']; + currentName: IUser['name']; /** * The state to handle if the form is open or not. */ @@ -35,14 +36,14 @@ interface IRenameUserFormProps { onClose: () => void; } -export const RenameUserForm = ({ currentUserName, isOpen, onClose }: IRenameUserFormProps) => { +export const RenameUserForm = ({ userId, currentName, isOpen, onClose }: IRenameUserFormProps) => { // input ref const inputRef = useRef(null); const form = useForm>({ resolver: zodResolver(renameUserFormSchema), defaultValues: { - userName: currentUserName, + name: currentName, }, }); @@ -51,10 +52,15 @@ export const RenameUserForm = ({ currentUserName, isOpen, onClose }: IRenameUser * @param values - The form values */ const processSubmit: SubmitHandler = async (values: RenameUserInputs) => { - const { userName } = values; - if (userName === currentUserName) return; - alert('Successfully renamed the group'); - onClose(); + const { name } = values; + if (name === currentName) return; + const result = await renameUser(userId, { name }); + if (!result.ok) { + alert('Something went wrong. Please try again.'); + } else { + alert('Successfully renamed the user'); + onClose(); + } }; /** @@ -72,7 +78,7 @@ export const RenameUserForm = ({ currentUserName, isOpen, onClose }: IRenameUser * Clear the input value. */ const handleClearInput = () => { - form.setValue('userName', ''); + form.setValue('name', ''); }; /** @@ -102,7 +108,7 @@ export const RenameUserForm = ({ currentUserName, isOpen, onClose }: IRenameUser
( @@ -112,10 +118,7 @@ export const RenameUserForm = ({ currentUserName, isOpen, onClose }: IRenameUser onKeyDown={handleKeyDown} handleClearInput={handleClearInput} handleOutsideClick={handleOutsideClick} - className={cn( - form.formState.errors.userName && 'border-danger', - 'text-xl leading-6', - )} + className={cn(form.formState.errors.name && 'border-danger', 'text-xl leading-6')} /> diff --git a/src/features/profile/components/UserProfile/index.tsx b/src/features/profile/components/UserProfile/index.tsx index c522bfb4..c657a962 100644 --- a/src/features/profile/components/UserProfile/index.tsx +++ b/src/features/profile/components/UserProfile/index.tsx @@ -36,7 +36,7 @@ export const UserProfile = ({ userId, name }: IUserProfileProps) => { {isRenameUserFormOpen ? ( setIsRenameUserFormOpen(false)} /> diff --git a/src/features/profile/lib/actions.test.ts b/src/features/profile/lib/actions.test.ts new file mode 100644 index 00000000..d1b34d62 --- /dev/null +++ b/src/features/profile/lib/actions.test.ts @@ -0,0 +1,67 @@ +import { putUpdateUser } from '@/lib/api/user/client'; + +import { Err, Ok } from 'result-ts-type'; + +import { renameUser } from './actions'; + +jest.mock('@/lib/api/user/client', () => ({ + putUpdateUser: jest.fn(), +})); + +// Clear mocks after each test +afterEach(() => { + jest.clearAllMocks(); +}); + +describe('Profile page actions', () => { + describe('renameUser', () => { + // Arrange common mock data + const mockUserId = 'a3kdifut-a520-c2cb-1be7-d90710691861'; + const mockInputs = { + name: 'New Name', + }; + + it('should rename a user successfully', async () => { + // Arrange + (putUpdateUser as jest.Mock).mockResolvedValue(Ok(undefined)); + + // Act + const result = await renameUser(mockUserId, mockInputs); + + // Assert + expect(result.ok).toBeTruthy(); + expect(result.unwrap()).toEqual(undefined); + expect(putUpdateUser).toHaveBeenCalled(); + }); + + it('should return an error if validation fails', async () => { + // Arrange + const mockInvalidInputs = { + name: '', + }; + const mockErrorMessage = 'Validation failed'; + + // Act + const result = await renameUser(mockUserId, mockInvalidInputs); + + // Assert + expect(result.err).toBeTruthy(); + expect(result.unwrapError()).toEqual(mockErrorMessage); + expect(putUpdateUser).not.toHaveBeenCalled(); + }); + + it('should return an error if API request fails', async () => { + // Arrange + // Mock the API request to simulate failure + const mockErrorMessage = 'API request failed'; + (putUpdateUser as jest.Mock).mockResolvedValue(Err(mockErrorMessage)); + + // Act + const result = await renameUser(mockUserId, mockInputs); + + // Assert + expect(result.err).toBeTruthy(); + expect(result.unwrapError()).toEqual(mockErrorMessage); + }); + }); +}); diff --git a/src/features/profile/lib/actions.ts b/src/features/profile/lib/actions.ts new file mode 100644 index 00000000..164ed4a6 --- /dev/null +++ b/src/features/profile/lib/actions.ts @@ -0,0 +1,24 @@ +import { putUpdateUser } from '@/lib/api/user/client'; +import { IUser } from '@/types/definition'; + +import { Err, Ok, Result } from 'result-ts-type'; + +import { renameUserFormSchema, RenameUserInputs } from './schemas'; + +/** + * Validate the inputs and call the API client to rename a user + * @param userId - The ID of the user to rename + * @param inputs - The raw inputs to be validated + * @returns undefined on success, or an error message if the validation or request fails + */ +export const renameUser = async ( + userId: IUser['id'], + inputs: RenameUserInputs, +): Promise> => { + const validatedData = renameUserFormSchema.safeParse(inputs); + if (!validatedData.success) return Err('Validation failed'); + const result = await putUpdateUser(userId, { name: validatedData.data.name }); + + if (result.ok) return Ok(undefined); + return Err(result.error); +}; diff --git a/src/features/profile/lib/schemas.ts b/src/features/profile/lib/schemas.ts index 35474ed6..8d919172 100644 --- a/src/features/profile/lib/schemas.ts +++ b/src/features/profile/lib/schemas.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; export const renameUserFormSchema = z.object({ - userName: z.string().min(1, { message: 'Name is required' }), + name: z.string().min(1, { message: 'Name is required' }), }); export type RenameUserInputs = z.infer; diff --git a/src/lib/api/user/client/index.ts b/src/lib/api/user/client/index.ts index e69de29b..69b23b3d 100644 --- a/src/lib/api/user/client/index.ts +++ b/src/lib/api/user/client/index.ts @@ -0,0 +1 @@ +export { putUpdateUser } from './userApiClient.client'; diff --git a/src/lib/api/user/client/userApiClient.client.test.ts b/src/lib/api/user/client/userApiClient.client.test.ts new file mode 100644 index 00000000..a263290c --- /dev/null +++ b/src/lib/api/user/client/userApiClient.client.test.ts @@ -0,0 +1,68 @@ +import { request } from '@/lib/api/common/client'; +import { putUpdateUser } from '@/lib/api/user/client'; + +jest.mock('@/lib/api/common/client', () => ({ + request: jest.fn(), +})); + +/** + * Create mock data for request function + * @param mockData + * @returns mocked request function + */ +const setUpMockSuccessRequest = (mockData: T) => { + return (request as jest.MockedFunction).mockResolvedValue(mockData); +}; + +/** + * Create mock error data for request function + * @param mockData + * @returns mocked request function + */ +const setUpMockErrorRequest = (mockData: T) => { + return (request as jest.MockedFunction).mockRejectedValue(mockData); +}; + +describe('API Function Tests', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('putUpdateUser', () => { + // Arrange mock data + const mockUserId = 'a3kdifut-a520-c2cb-1be7-d90710691861'; + const mockRequestBody = { name: 'New name' }; + + it('successfully update user data', async () => { + // mock response,request and expected value + const mockRequest = setUpMockSuccessRequest({}); + + // Act + const result = await putUpdateUser(mockUserId, mockRequestBody); + + // Assert + expect(result.ok).toBeTruthy(); + expect(result.unwrap()).toEqual(undefined); + expect(mockRequest).toHaveBeenCalledWith({ + url: expect.stringContaining(`/users/${mockUserId}`), + method: 'PUT', + options: { + body: JSON.stringify(mockRequestBody), + }, + }); + }); + + it('throws an error on API failure', async () => { + // Arrange mock error and request + const mockError = new Error('API error'); + setUpMockErrorRequest(mockError); + + // Act + const result = await putUpdateUser(mockUserId, mockRequestBody); + + // Assert + expect(result.err).toBeTruthy(); + expect(result.unwrapError()).toEqual(mockError.message); + }); + }); +}); diff --git a/src/lib/api/user/client/userApiClient.client.ts b/src/lib/api/user/client/userApiClient.client.ts new file mode 100644 index 00000000..f1873f17 --- /dev/null +++ b/src/lib/api/user/client/userApiClient.client.ts @@ -0,0 +1,41 @@ +'use client'; + +import { request } from '@/lib/api/common/client'; +import { IUser } from '@/types/definition'; + +import { Err, Ok, Result } from 'result-ts-type'; + +const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || ''; + +/** + * Interface representing the request body for the function to update user's data. + */ +interface IPutUpdateUserRequestBody { + /** + * The new name of the user. + */ + name: IUser['name']; +} + +/** + * Function to update user's data + * @param requestBody - The payload for the function. + */ +export const putUpdateUser = async ( + userId: IUser['id'], + requestBody: IPutUpdateUserRequestBody, +): Promise> => { + try { + await request({ + url: `${API_BASE_URL}/users/${userId}`, + method: 'PUT', + options: { body: JSON.stringify(requestBody) }, + }); + return Ok(undefined); + } catch (err) { + if (err instanceof Error) { + return Err(err.message); + } + return Err('API response is invalid'); + } +};