diff --git a/src/api/service/user-service/index.ts b/src/api/service/user-service/index.ts index 0113156e..7a1c9440 100644 --- a/src/api/service/user-service/index.ts +++ b/src/api/service/user-service/index.ts @@ -34,9 +34,9 @@ export const userServiceRemote = () => ({ // 4. 알림 설정 변경 updateMyNotification: async (queryParams: UpdateMyNotificationQueryParams) => { - return apiV1.patch( - `/users/notification?isNotificationEnabled=${queryParams.isNotificationEnabled}`, - ); + return apiV1.patch(`/users/notification`, null, { + params: { ...queryParams }, + }); }, // 5. 유저 프로필 조회 @@ -50,7 +50,7 @@ export const userServiceRemote = () => ({ // 7. 닉네임 중복 검사 getNicknameAvailability: async (queryParams: GetNicknameAvailabilityQueryParams) => { return apiV1.get(`/users/nickname/availability`, { - params: { nickname: queryParams.nickName }, + params: { ...queryParams }, }); }, diff --git a/src/components/pages/user/profile/profile-edit-modal/index.test.tsx b/src/components/pages/user/profile/profile-edit-modal/index.test.tsx index 7553a6ec..bf763d6d 100644 --- a/src/components/pages/user/profile/profile-edit-modal/index.test.tsx +++ b/src/components/pages/user/profile/profile-edit-modal/index.test.tsx @@ -1,10 +1,14 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { act, render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +import { delay, http, HttpResponse } from 'msw'; +import { API } from '@/api'; import { ModalProvider } from '@/components/ui'; import { server } from '@/mock/server'; +import { createMockSuccessResponse } from '@/mock/service/common/common-mock'; import { mockUserItems } from '@/mock/service/user/user-mock'; +import { User } from '@/types/service/user'; import { ProfileEditModal } from '.'; @@ -254,4 +258,152 @@ describe('ProfileEditModal 테스트', () => { }); }); }); + + describe('폼 제출 테스트', () => { + describe('닉네임 중복 검사 테스트', () => { + test('닉네임 수정 시 닉네임 중복 검사가 실행된다.', async () => { + const user = userEvent.setup(); + + const getNicknameAvailabilitySpy = jest.spyOn(API.userService, 'getNicknameAvailability'); + + await renderWithProviders(); + + const nickNameInput = screen.getByDisplayValue(mockUser.nickName); + await user.clear(nickNameInput); + await user.type(nickNameInput, '새로운 닉네임'); + + const submitButton = screen.getByText('수정하기'); + await user.click(submitButton); + + await waitFor(() => { + expect(getNicknameAvailabilitySpy).toHaveBeenCalledWith({ nickName: '새로운 닉네임' }); + }); + + getNicknameAvailabilitySpy.mockRestore(); + }); + + test('중복된 닉네임일 경우 에러 메시지가 표시된다.', async () => { + const user = userEvent.setup(); + await renderWithProviders(); + + const nickNameInput = screen.getByDisplayValue(mockUser.nickName); + await user.clear(nickNameInput); + await user.type(nickNameInput, '페르난도 토레스'); + + const submitButton = screen.getByText('수정하기'); + await user.click(submitButton); + + await waitFor(() => { + expect(screen.getByText('이미 사용중인 닉네임 입니다.')).toBeInTheDocument(); + }); + }); + + test('닉네임이 변경되지 않으면 중복 검사를 하지 않는다.', async () => { + const user = userEvent.setup(); + const getNicknameAvailabilitySpy = jest.spyOn(API.userService, 'getNicknameAvailability'); + + await renderWithProviders(); + + const prevNickName = mockUser.nickName; + const nickNameInput = screen.getByDisplayValue(prevNickName); + await user.clear(nickNameInput); + await user.type(nickNameInput, prevNickName); + + const submitButton = screen.getByText('수정하기'); + await user.click(submitButton); + + await waitFor(() => { + expect(getNicknameAvailabilitySpy).not.toHaveBeenCalledWith({ + nickName: prevNickName, + }); + }); + + getNicknameAvailabilitySpy.mockRestore(); + }); + }); + + describe('MBTI 제출 테스트', () => { + test('MBTI가 대문자로 변환되어 전송된다.', async () => { + const user = userEvent.setup(); + + const updateMyInfoSpy = jest.spyOn(API.userService, 'updateMyInfo'); + + await renderWithProviders(); + + const mbtiInput = screen.getByDisplayValue(mockUser.mbti); + await user.clear(mbtiInput); + await user.type(mbtiInput, 'enfp'); + + const submitButton = screen.getByText('수정하기'); + await user.click(submitButton); + + await waitFor(() => { + expect(updateMyInfoSpy).toHaveBeenCalledWith({ mbti: 'ENFP' }); + }); + + updateMyInfoSpy.mockRestore(); + }); + }); + + describe('API 호출 테스트', () => { + test('이미지가 변경되지 않으면 updateMyImage API가 호출되지 않는다.', async () => { + const user = userEvent.setup(); + + const updateMyImageSpy = jest.spyOn(API.userService, 'updateMyImage'); + + await renderWithProviders(); + + const nickNameInput = screen.getByDisplayValue(mockUser.nickName); + await user.clear(nickNameInput); + await user.type(nickNameInput, '새로운 닉네임'); + + const messageInput = screen.getByDisplayValue(mockUser.profileMessage); + await user.clear(messageInput); + await user.type(messageInput, '새로운 소개글'); + + const mbtiInput = screen.getByDisplayValue(mockUser.mbti); + await user.clear(mbtiInput); + await user.type(mbtiInput, 'ISTJ'); + + const submitButton = screen.getByText('수정하기'); + await user.click(submitButton); + + await waitFor(() => { + expect(updateMyImageSpy).not.toHaveBeenCalled(); + }); + + updateMyImageSpy.mockRestore(); + }); + }); + + describe('제출 버튼 상태 테스트', () => { + test('제출 중일 때 버튼이 "수정 중..."으로 변경되며 비활성화 된다', async () => { + server.use( + http.patch(`*/users/profile`, async ({ request }) => { + await delay(200); + const body = (await request.json()) as User; + return HttpResponse.json( + createMockSuccessResponse({ + ...mockUserItems[0], + ...body, + }), + ); + }), + ); + + const user = userEvent.setup(); + + await renderWithProviders(); + + const mbtiInput = screen.getByDisplayValue(mockUser.mbti); + await user.clear(mbtiInput); + await user.type(mbtiInput, 'enfp'); + + const submitButton = screen.getByText('수정하기'); + await user.click(submitButton); + + expect(await screen.findByRole('button', { name: '수정 중...' })).toBeDisabled(); + }); + }); + }); }); diff --git a/src/components/pages/user/profile/profile-edit-modal/index.tsx b/src/components/pages/user/profile/profile-edit-modal/index.tsx index 9edc200e..ff478cd1 100644 --- a/src/components/pages/user/profile/profile-edit-modal/index.tsx +++ b/src/components/pages/user/profile/profile-edit-modal/index.tsx @@ -50,7 +50,7 @@ export const ProfileEditModal = ({ user }: Props) => { return { form: '입력값을 확인해주세요', fields: { - nickName: { message: '이미 사용 중인 닉네임입니다' }, + nickName: { message: '이미 사용중인 닉네임 입니다.' }, }, }; } diff --git a/src/mock/service/user/user-handler.ts b/src/mock/service/user/user-handler.ts index 7ac5b713..f09cbc48 100644 --- a/src/mock/service/user/user-handler.ts +++ b/src/mock/service/user/user-handler.ts @@ -5,6 +5,62 @@ import { User } from '@/types/service/user'; import { createMockErrorResponse, createMockSuccessResponse } from '../common/common-mock'; import { mockUserItems } from './user-mock'; +// 1. 팔로우 등록 모킹 +const followUserMock = http.post(`*/users/follow`, async ({ request }) => { + const url = new URL(request.url); + const followNickname = url.searchParams.get('followNickname'); + const user = mockUserItems.find((v) => v.nickName === followNickname); + + if (user?.isFollow === false) { + return HttpResponse.json(createMockSuccessResponse('팔로우 성공')); + } + + if (user?.isFollow === null) { + return HttpResponse.json( + createMockErrorResponse({ + status: 400, + detail: '회원 : 자기 자신을 팔로우할 수 없습니다.', + errorCode: 'NOT_SAME_FOLLOW', + }), + ); + } + + if (user?.isFollow === true) { + return HttpResponse.json( + createMockErrorResponse({ + status: 400, + detail: '회원 : 이미 팔로우 중입니다.', + errorCode: 'ALREADY_EXIST_FOLLOW', + }), + ); + } +}); + +// 2. 유저 프로필 변경 모킹 +const updateUserItemMock = http.patch(`*/users/profile`, async ({ request }) => { + const body = (await request.json()) as User; + return HttpResponse.json( + createMockSuccessResponse({ + ...mockUserItems[0], + ...body, + }), + ); +}); + +// 4. 알림 설정 변경 모킹 +const updateMyNotificationMock = http.patch(`*/users/notification`, async ({ request }) => { + const url = new URL(request.url); + const isNotificationEnabled = url.searchParams.get('isNotificationEnabled'); + + return HttpResponse.json( + createMockSuccessResponse({ + ...mockUserItems[0], + isNotificationEnabled: !!isNotificationEnabled, + }), + ); +}); + +// 5. 유저 프로필 조회 모킹 const getUserItemMock = http.get(`*/users/:userId`, ({ params }) => { const id = Number(params.userId); const user = mockUserItems.find((item) => item.userId === id); @@ -23,39 +79,74 @@ const getUserItemMock = http.get(`*/users/:userId`, ({ params }) => { return HttpResponse.json(createMockSuccessResponse(user)); }); +// 7. 닉네임 중복 검사 모킹 +const getNicknameAvailabilityMock = http.get(`*/users/nickname/availability`, ({ request }) => { + const url = new URL(request.url); + const nickname = url.searchParams.get('nickname'); + const user = mockUserItems.find((item) => item.nickName === nickname); + return HttpResponse.json( + createMockSuccessResponse({ + data: { available: !user }, + }), + ); +}); + +// 8. 본인 프로필 조회 모킹 const getMeItemMock = http.get(`*/users/me`, () => { const id = 1; const user = mockUserItems.find((item) => item.userId === id); return HttpResponse.json(createMockSuccessResponse(user)); }); -const updateUserItemMock = http.patch(`*/users`, async ({ request }) => { - const body = (await request.json()) as User; +// 7. 닉네임 중복 검사 모킹 +const getEmailAvailabilityMock = http.get(`*/users/email/availability`, ({ request }) => { + const url = new URL(request.url); + const email = url.searchParams.get('email'); + const user = mockUserItems.find((item) => item.email === email); return HttpResponse.json( createMockSuccessResponse({ - ...mockUserItems[0], - ...body, + data: { available: !user }, }), ); }); -const deleteUserItemMock = http.delete(`*/users`, async () => { - return HttpResponse.json(createMockSuccessResponse(null)); -}); +const unfollowUserMock = http.delete(`*/users/unfollow`, ({ request }) => { + const url = new URL(request.url); + const unFollowNickname = url.searchParams.get('unFollowNickname'); + const user = mockUserItems.find((v) => v.nickName === unFollowNickname); -const followUserItemMock = http.post(`*/follows`, async () => { - return HttpResponse.json(createMockSuccessResponse(null)); -}); + if (user?.isFollow === false) { + return HttpResponse.json(createMockSuccessResponse('팔로우 취소 성공')); + } + + if (user?.isFollow === null) { + return HttpResponse.json( + createMockErrorResponse({ + status: 400, + detail: '회원 : 팔로우 취소 대상은 본인 될 수 없습니다.', + errorCode: 'NOT_SAME_FOLLOW', + }), + ); + } -const unfollowUserItemMock = http.delete(`*/follows/:followId`, async () => { - return HttpResponse.json(createMockSuccessResponse(null)); + if (user?.isFollow === true) { + return HttpResponse.json( + createMockErrorResponse({ + status: 400, + detail: '회원 : 팔로우 관계를 찾을 수 없습니다.', + errorCode: 'NOT_FOUND_FOLLOW', + }), + ); + } }); export const userHandlers = [ + followUserMock, getUserItemMock, + updateMyNotificationMock, getMeItemMock, updateUserItemMock, - deleteUserItemMock, - followUserItemMock, - unfollowUserItemMock, + getNicknameAvailabilityMock, + getEmailAvailabilityMock, + unfollowUserMock, ];