Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Implement update user API client and apply it to rename user form #352

Merged
merged 14 commits into from
Mar 12, 2024
Merged
29 changes: 16 additions & 13 deletions src/features/profile/components/UserProfile/RenameUserForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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.
*/
Expand All @@ -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<HTMLInputElement>(null);

const form = useForm<z.infer<typeof renameUserFormSchema>>({
resolver: zodResolver(renameUserFormSchema),
defaultValues: {
userName: currentUserName,
name: currentName,
},
});

Expand All @@ -51,10 +52,15 @@ export const RenameUserForm = ({ currentUserName, isOpen, onClose }: IRenameUser
* @param values - The form values
*/
const processSubmit: SubmitHandler<RenameUserInputs> = 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();
}
};

/**
Expand All @@ -72,7 +78,7 @@ export const RenameUserForm = ({ currentUserName, isOpen, onClose }: IRenameUser
* Clear the input value.
*/
const handleClearInput = () => {
form.setValue('userName', '');
form.setValue('name', '');
};

/**
Expand Down Expand Up @@ -102,7 +108,7 @@ export const RenameUserForm = ({ currentUserName, isOpen, onClose }: IRenameUser
<form onSubmit={form.handleSubmit(processSubmit)}>
<FormField
control={form.control}
name="userName"
name="name"
render={({ field }) => (
<FormItem>
<FormControl>
Expand All @@ -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')}
/>
</FormControl>
<FormMessage />
Expand Down
2 changes: 1 addition & 1 deletion src/features/profile/components/UserProfile/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export const UserProfile = ({ userId, name }: IUserProfileProps) => {
{isRenameUserFormOpen ? (
<RenameUserForm
userId={userId}
currentUserName={name}
currentName={name}
isOpen={isRenameUserFormOpen}
onClose={() => setIsRenameUserFormOpen(false)}
/>
Expand Down
67 changes: 67 additions & 0 deletions src/features/profile/lib/actions.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
24 changes: 24 additions & 0 deletions src/features/profile/lib/actions.ts
Original file line number Diff line number Diff line change
@@ -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<Result<undefined, string>> => {
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);
};
2 changes: 1 addition & 1 deletion src/features/profile/lib/schemas.ts
Original file line number Diff line number Diff line change
@@ -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<typeof renameUserFormSchema>;
1 change: 1 addition & 0 deletions src/lib/api/user/client/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { putUpdateUser } from './userApiClient.client';
68 changes: 68 additions & 0 deletions src/lib/api/user/client/userApiClient.client.test.ts
Original file line number Diff line number Diff line change
@@ -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 = <T>(mockData: T) => {
return (request as jest.MockedFunction<typeof request>).mockResolvedValue(mockData);
};

/**
* Create mock error data for request function
* @param mockData
* @returns mocked request function
*/
const setUpMockErrorRequest = <T>(mockData: T) => {
return (request as jest.MockedFunction<typeof request>).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);
});
});
});
41 changes: 41 additions & 0 deletions src/lib/api/user/client/userApiClient.client.ts
Original file line number Diff line number Diff line change
@@ -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.
*/
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you rewrite the TSDoc, please? This is not the official way.
#247

Instead, please add a description to the line right above each property.

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<Result<undefined, string>> => {
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');
}
};
Loading