diff --git a/src/main/java/org/runnect/server/common/resolver/sort/SortStatusIdResolver.java b/src/main/java/org/runnect/server/common/resolver/sort/SortStatusIdResolver.java index 8da9425..7a3fa6d 100644 --- a/src/main/java/org/runnect/server/common/resolver/sort/SortStatusIdResolver.java +++ b/src/main/java/org/runnect/server/common/resolver/sort/SortStatusIdResolver.java @@ -31,8 +31,11 @@ public boolean supportsParameter(MethodParameter parameter) { public Object resolveArgument(@NotNull MethodParameter parameter, ModelAndViewContainer modelAndViewContainer, @NotNull NativeWebRequest webRequest, WebDataBinderFactory binderFactory) { final HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest(); String queryString = request.getQueryString(); - Map> splitQuery = Util.splitQuery(queryString); - + // 쿼리 파라미터가 하나도 없으면(예: ~url~/public-course) getQueryString()이 null을 반환해 + // split()에서 NPE(500)가 나던 부분 — sort 키가 없는 경우와 동일하게 기본값으로 처리한다. + Map> splitQuery = queryString == null + ? Collections.emptyMap() + : Util.splitQuery(queryString); if(!splitQuery.containsKey("sort") || splitQuery.get("sort").get(0)==null){ // 1번째 조건 : ~url~/public-course diff --git a/src/test/java/org/runnect/server/publicCourse/controller/PublicCourseControllerTest.java b/src/test/java/org/runnect/server/publicCourse/controller/PublicCourseControllerTest.java new file mode 100644 index 0000000..37e990a --- /dev/null +++ b/src/test/java/org/runnect/server/publicCourse/controller/PublicCourseControllerTest.java @@ -0,0 +1,297 @@ +package org.runnect.server.publicCourse.controller; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.Collections; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.BDDMockito; +import org.runnect.server.common.constant.ErrorStatus; +import org.runnect.server.common.constant.TokenStatus; +import org.runnect.server.common.exception.BadRequestException; +import org.runnect.server.common.exception.NotFoundException; +import org.runnect.server.common.exception.PermissionDeniedException; +import org.runnect.server.config.jwt.JwtService; +import org.runnect.server.config.slack.SlackApi; +import org.runnect.server.publicCourse.dto.response.CreatePublicCourseResponseDto; +import org.runnect.server.publicCourse.dto.response.DeletePublicCoursesResponseDto; +import org.runnect.server.publicCourse.dto.response.GetPublicCourseTotalPageCountResponseDto; +import org.runnect.server.publicCourse.dto.response.UpdatePublicCourseResponseDto; +import org.runnect.server.publicCourse.dto.response.getMarathonPublicCourse.GetMarathonPublicCourseResponseDto; +import org.runnect.server.publicCourse.dto.response.getPublicCourseByUser.GetPublicCourseByUserResponseDto; +import org.runnect.server.publicCourse.dto.response.recommendPublicCourse.RecommendPublicCourseResponseDto; +import org.runnect.server.publicCourse.dto.response.searchPublicCourse.SearchPublicCourseResponseDto; +import org.runnect.server.publicCourse.entity.PublicCourse; +import org.runnect.server.publicCourse.service.PublicCourseService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; + +@WebMvcTest(PublicCourseController.class) +class PublicCourseControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private PublicCourseService publicCourseService; + @MockBean + private JwtService jwtService; + @MockBean + private SlackApi slackApi; + + @BeforeEach + void setUpAuth() { + BDDMockito.lenient().when(jwtService.verifyToken("valid")).thenReturn(TokenStatus.TOKEN_VALID); + BDDMockito.lenient().when(jwtService.getJwtContents("valid")).thenReturn("1"); + } + + private B withAuth(B builder) { + builder.header("accessToken", "valid").header("refreshToken", "valid"); + return builder; + } + + @Nested + @DisplayName("GET /api/public-course") + class RecommendPublicCourse { + + @Test + @DisplayName("정상 요청이면 200과 추천 코스 목록을 반환한다") + void 정상_조회() throws Exception { + when(publicCourseService.recommendPublicCourse(eq(1L), eq(1), eq("date"))) + .thenReturn(RecommendPublicCourseResponseDto.of("date", 3, false, Collections.emptyList())); + + mockMvc.perform(withAuth(get("/api/public-course"))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.ordering").value("date")); + } + + @Test + @DisplayName("sort 파라미터가 유효하지 않으면 400을 반환한다") + void 정렬값_유효하지_않음() throws Exception { + mockMvc.perform(withAuth(get("/api/public-course?sort=invalid"))) + .andExpect(status().isBadRequest()); + + BDDMockito.verifyNoInteractions(publicCourseService); + } + + @Test + @DisplayName("[설계상 주의] pageNo에 @Positive가 있어도 클래스에 @Validated가 없어 실제로는 검증되지 않는다") + void 페이지번호_0이어도_검증되지_않음() throws Exception { + when(publicCourseService.recommendPublicCourse(eq(1L), eq(0), eq("date"))) + .thenReturn(RecommendPublicCourseResponseDto.of("date", 3, false, Collections.emptyList())); + + mockMvc.perform(withAuth(get("/api/public-course").param("pageNo", "0"))) + .andExpect(status().isOk()); + } + } + + @Nested + @DisplayName("GET /api/public-course/search") + class SearchPublicCourse { + + @Test + @DisplayName("정상 요청이면 200과 검색 결과를 반환한다") + void 정상_검색() throws Exception { + when(publicCourseService.searchPublicCourse(eq(1L), eq("정왕역"))) + .thenReturn(SearchPublicCourseResponseDto.of(Collections.emptyList())); + + mockMvc.perform(withAuth(get("/api/public-course/search").param("keyword", "정왕역"))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.publicCourses").isEmpty()); + } + + @Test + @DisplayName("keyword가 없으면 400을 반환한다") + void 검색어_없음() throws Exception { + mockMvc.perform(withAuth(get("/api/public-course/search"))) + .andExpect(status().isBadRequest()); + + BDDMockito.verifyNoInteractions(publicCourseService); + } + } + + @Nested + @DisplayName("GET /api/public-course/marathon") + class GetMarathonPublicCourse { + + @Test + @DisplayName("정상 요청이면 200과 마라톤 코스 목록을 반환한다") + void 정상_조회() throws Exception { + when(publicCourseService.getMarathonPublicCourse(1L)) + .thenReturn(GetMarathonPublicCourseResponseDto.of(Collections.emptyList())); + + mockMvc.perform(withAuth(get("/api/public-course/marathon"))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.marathonPublicCourses").isEmpty()); + } + } + + @Nested + @DisplayName("POST /api/public-course") + class CreatePublicCourse { + + @Test + @DisplayName("정상 요청이면 201과 생성된 퍼블릭 코스 정보를 반환한다") + void 정상_생성() throws Exception { + when(publicCourseService.createPublicCourse(eq(1L), any())) + .thenReturn(CreatePublicCourseResponseDto.of(100L, "2026-01-01T00:00:00")); + + mockMvc.perform(withAuth(post("/api/public-course")) + .contentType("application/json") + .content("{\"courseId\":10,\"title\":\"정왕역 코스\",\"description\":\"설명\"}")) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.data.publicCourse.id").value(100)); + } + + @Test + @DisplayName("title이 없으면 400을 반환한다") + void 제목_없음() throws Exception { + mockMvc.perform(withAuth(post("/api/public-course")) + .contentType("application/json") + .content("{\"courseId\":10,\"description\":\"설명\"}")) + .andExpect(status().isBadRequest()); + + BDDMockito.verifyNoInteractions(publicCourseService); + } + } + + @Nested + @DisplayName("GET /api/public-course/detail/{publicCourseId}") + class GetPublicCourseDetail { + + @Test + @DisplayName("존재하지 않는 퍼블릭 코스면 400을 반환한다") + void 존재하지_않음() throws Exception { + when(publicCourseService.getPublicCourseDetail(anyLong(), eq(999L))) + .thenThrow(new NotFoundException( + ErrorStatus.NOT_FOUND_PUBLIC_COURSE_EXCEPTION, + ErrorStatus.NOT_FOUND_PUBLIC_COURSE_EXCEPTION.getMessage())); + + mockMvc.perform(withAuth(get("/api/public-course/detail/999"))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.success").value(false)); + } + } + + @Nested + @DisplayName("GET /api/public-course/user") + class GetPublicCourseByUser { + + @Test + @DisplayName("정상 요청이면 200과 유저의 퍼블릭 코스 목록을 반환한다") + void 정상_조회() throws Exception { + when(publicCourseService.getPublicCourseByUser(1L)) + .thenReturn(GetPublicCourseByUserResponseDto.of(1L, Collections.emptyList())); + + mockMvc.perform(withAuth(get("/api/public-course/user"))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.user.id").value(1)); + } + } + + @Nested + @DisplayName("GET /api/public-course/total-page-count") + class GetPublicCourseTotalPageCount { + + @Test + @DisplayName("정상 요청이면 200과 전체 페이지 수를 반환한다") + void 정상_조회() throws Exception { + when(publicCourseService.getPublicCourseTotalPageCount()) + .thenReturn(GetPublicCourseTotalPageCountResponseDto.of(5L)); + + mockMvc.perform(get("/api/public-course/total-page-count")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.totalPageCount").value(5)); + } + } + + @Nested + @DisplayName("PUT /api/public-course") + class DeletePublicCourses { + + @Test + @DisplayName("정상 요청이면 200과 삭제 개수를 반환한다") + void 정상_삭제() throws Exception { + when(publicCourseService.deletePublicCourses(eq(1L), any())) + .thenReturn(DeletePublicCoursesResponseDto.from(1)); + + mockMvc.perform(withAuth(put("/api/public-course")) + .contentType("application/json") + .content("{\"publicCourseIdList\":[10]}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.deletedPublicCourseCount").value(1)); + } + + @Test + @DisplayName("publicCourseIdList가 비어있으면 400을 반환한다") + void 목록_비어있음() throws Exception { + mockMvc.perform(withAuth(put("/api/public-course")) + .contentType("application/json") + .content("{\"publicCourseIdList\":[]}")) + .andExpect(status().isBadRequest()); + + BDDMockito.verifyNoInteractions(publicCourseService); + } + } + + @Nested + @DisplayName("PATCH /api/public-course/{publicCourseId}") + class UpdatePublicCourse { + + @Test + @DisplayName("정상 요청이면 200과 수정된 정보를 반환한다") + void 정상_수정() throws Exception { + PublicCourse publicCourse = PublicCourse.builder().title("새 제목").description("새 설명").build(); + ReflectionTestUtils.setField(publicCourse, "id", 10L); + when(publicCourseService.updatePublicCourse(eq(1L), eq(10L), eq("새 제목"), eq("새 설명"))) + .thenReturn(UpdatePublicCourseResponseDto.of(publicCourse)); + + mockMvc.perform(withAuth(patch("/api/public-course/10")) + .contentType("application/json") + .content("{\"title\":\"새 제목\",\"description\":\"새 설명\"}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.publicCourse.title").value("새 제목")); + } + + @Test + @DisplayName("title이 없으면 400을 반환한다") + void 제목_없음() throws Exception { + mockMvc.perform(withAuth(patch("/api/public-course/10")) + .contentType("application/json") + .content("{\"title\":\"\",\"description\":\"설명\"}")) + .andExpect(status().isBadRequest()); + + BDDMockito.verifyNoInteractions(publicCourseService); + } + + @Test + @DisplayName("소유권이 없으면 403을 반환한다") + void 소유권_없음() throws Exception { + when(publicCourseService.updatePublicCourse(eq(1L), eq(10L), any(), any())) + .thenThrow(new PermissionDeniedException( + ErrorStatus.PERMISSION_DENIED_PUBLIC_COURSE_UPDATE_EXCEPTION, + ErrorStatus.PERMISSION_DENIED_PUBLIC_COURSE_UPDATE_EXCEPTION.getMessage())); + + mockMvc.perform(withAuth(patch("/api/public-course/10")) + .contentType("application/json") + .content("{\"title\":\"새 제목\",\"description\":\"새 설명\"}")) + .andExpect(status().isForbidden()); + } + } +} diff --git a/src/test/java/org/runnect/server/record/controller/RecordControllerTest.java b/src/test/java/org/runnect/server/record/controller/RecordControllerTest.java new file mode 100644 index 0000000..055aeb8 --- /dev/null +++ b/src/test/java/org/runnect/server/record/controller/RecordControllerTest.java @@ -0,0 +1,161 @@ +package org.runnect.server.record.controller; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.Collections; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.BDDMockito; +import org.runnect.server.common.constant.TokenStatus; +import org.runnect.server.config.jwt.JwtService; +import org.runnect.server.config.slack.SlackApi; +import org.runnect.server.record.dto.response.CreateRecordDto; +import org.runnect.server.record.dto.response.CreateRecordResponseDto; +import org.runnect.server.record.dto.response.DeleteRecordsResponseDto; +import org.runnect.server.record.dto.response.GetRecordResponseDto; +import org.runnect.server.record.dto.response.UpdateRecordResponse; +import org.runnect.server.record.dto.response.UpdateRecordResponseDto; +import org.runnect.server.record.dto.response.UserResponse; +import org.runnect.server.record.service.RecordService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; + +@WebMvcTest(RecordController.class) +class RecordControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private RecordService recordService; + @MockBean + private JwtService jwtService; + @MockBean + private SlackApi slackApi; + + @BeforeEach + void setUpAuth() { + BDDMockito.lenient().when(jwtService.verifyToken("valid")).thenReturn(TokenStatus.TOKEN_VALID); + BDDMockito.lenient().when(jwtService.getJwtContents("valid")).thenReturn("1"); + } + + private B withAuth(B builder) { + builder.header("accessToken", "valid").header("refreshToken", "valid"); + return builder; + } + + @Nested + @DisplayName("POST /api/record") + class CreateRecord { + + @Test + @DisplayName("정상 요청이면 201과 생성된 레코드 정보를 반환한다") + void 정상_생성() throws Exception { + when(recordService.createRecord(eq(1L), any())) + .thenReturn(new CreateRecordResponseDto(new CreateRecordDto(100L, "2026-01-01T00:00:00"))); + + mockMvc.perform(withAuth(post("/api/record")) + .contentType("application/json") + .content("{\"courseId\":10,\"title\":\"정왕역 코스\",\"time\":\"00:30:00\",\"pace\":\"6'00\\\"\"}")) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.data.record.id").value(100)); + } + + @Test + @DisplayName("title이 없으면 400을 반환한다") + void 제목_없음() throws Exception { + mockMvc.perform(withAuth(post("/api/record")) + .contentType("application/json") + .content("{\"courseId\":10,\"time\":\"00:30:00\",\"pace\":\"6'00\\\"\"}")) + .andExpect(status().isBadRequest()); + + BDDMockito.verifyNoInteractions(recordService); + } + } + + @Nested + @DisplayName("GET /api/record/user") + class GetRecordByUser { + + @Test + @DisplayName("정상 요청이면 200과 레코드 목록을 반환한다") + void 정상_조회() throws Exception { + when(recordService.getRecordByUser(1L)) + .thenReturn(GetRecordResponseDto.of(UserResponse.of(1L), Collections.emptyList())); + + mockMvc.perform(withAuth(get("/api/record/user"))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.user.userId").value(1)); + } + } + + @Nested + @DisplayName("PATCH /api/record/{recordId}") + class UpdateRecord { + + @Test + @DisplayName("정상 요청이면 200과 수정된 제목을 반환한다") + void 정상_수정() throws Exception { + when(recordService.updateRecord(eq(1L), eq(10L), any())) + .thenReturn(UpdateRecordResponseDto.of(UpdateRecordResponse.of(10L, "새 제목"))); + + mockMvc.perform(withAuth(patch("/api/record/10")) + .contentType("application/json") + .content("{\"title\":\"새 제목\"}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.record.title").value("새 제목")); + } + + @Test + @DisplayName("title이 없으면 400을 반환한다") + void 제목_없음() throws Exception { + mockMvc.perform(withAuth(patch("/api/record/10")) + .contentType("application/json") + .content("{\"title\":\"\"}")) + .andExpect(status().isBadRequest()); + + BDDMockito.verifyNoInteractions(recordService); + } + } + + @Nested + @DisplayName("PUT /api/record") + class DeleteRecords { + + @Test + @DisplayName("정상 요청이면 200과 삭제 개수를 반환한다") + void 정상_삭제() throws Exception { + when(recordService.deleteRecords(eq(1L), any())).thenReturn(DeleteRecordsResponseDto.from(1L)); + + mockMvc.perform(withAuth(put("/api/record")) + .contentType("application/json") + .content("{\"recordIdList\":[10]}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.deletedRecordIdCount").value(1)); + } + + @Test + @DisplayName("recordIdList가 비어있으면 400을 반환한다") + void 목록_비어있음() throws Exception { + mockMvc.perform(withAuth(put("/api/record")) + .contentType("application/json") + .content("{\"recordIdList\":[]}")) + .andExpect(status().isBadRequest()); + + BDDMockito.verifyNoInteractions(recordService); + } + } +} diff --git a/src/test/java/org/runnect/server/user/controller/UserControllerTest.java b/src/test/java/org/runnect/server/user/controller/UserControllerTest.java new file mode 100644 index 0000000..4b24817 --- /dev/null +++ b/src/test/java/org/runnect/server/user/controller/UserControllerTest.java @@ -0,0 +1,168 @@ +package org.runnect.server.user.controller; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.Collections; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.BDDMockito; +import org.runnect.server.common.constant.ErrorStatus; +import org.runnect.server.common.constant.TokenStatus; +import org.runnect.server.config.jwt.JwtService; +import org.runnect.server.config.slack.SlackApi; +import org.runnect.server.user.dto.response.DeleteUserResponseDto; +import org.runnect.server.user.dto.response.MyPageResponseDto; +import org.runnect.server.user.dto.response.UpdateUserNicknameResponseDto; +import org.runnect.server.user.dto.response.UserProfileResponseDto; +import org.runnect.server.user.entity.RunnectUser; +import org.runnect.server.user.entity.SocialType; +import org.runnect.server.user.exception.userException.NotFoundUserException; +import org.runnect.server.user.service.UserService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; + +@WebMvcTest(UserController.class) +class UserControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private UserService userService; + @MockBean + private JwtService jwtService; + @MockBean + private SlackApi slackApi; + + @BeforeEach + void setUpAuth() { + BDDMockito.lenient().when(jwtService.verifyToken("valid")).thenReturn(TokenStatus.TOKEN_VALID); + BDDMockito.lenient().when(jwtService.getJwtContents("valid")).thenReturn("1"); + } + + private B withAuth(B builder) { + builder.header("accessToken", "valid").header("refreshToken", "valid"); + return builder; + } + + private RunnectUser buildUser(Long id, String nickname) { + RunnectUser user = RunnectUser.builder() + .nickname(nickname).socialId("social-" + id).email("user" + id + "@runnect.io") + .provider(SocialType.KAKAO).build(); + ReflectionTestUtils.setField(user, "id", id); + return user; + } + + @Nested + @DisplayName("GET /api/user") + class GetMyPage { + + @Test + @DisplayName("정상 요청이면 200과 마이페이지 정보를 반환한다") + void 정상_조회() throws Exception { + RunnectUser user = buildUser(1L, "러너1"); + when(userService.getMyPage(1L)).thenReturn(MyPageResponseDto.of(user, 50)); + + mockMvc.perform(withAuth(get("/api/user"))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.user.id").value(1)) + .andExpect(jsonPath("$.data.user.nickname").value("러너1")); + } + + @Test + @DisplayName("존재하지 않는 유저면 404를 반환한다") + void 존재하지_않는_유저() throws Exception { + when(userService.getMyPage(1L)).thenThrow(new NotFoundUserException( + ErrorStatus.NOT_FOUND_USER_EXCEPTION, ErrorStatus.NOT_FOUND_USER_EXCEPTION.getMessage())); + + mockMvc.perform(withAuth(get("/api/user"))) + .andExpect(status().isNotFound()); + } + } + + @Nested + @DisplayName("PATCH /api/user") + class UpdateUserNickname { + + @Test + @DisplayName("정상 요청이면 200과 변경된 닉네임을 반환한다") + void 정상_변경() throws Exception { + RunnectUser user = buildUser(1L, "새닉네임"); + when(userService.updateUserNickname(eq(1L), any())).thenReturn( + UpdateUserNicknameResponseDto.of(user, 50)); + + mockMvc.perform(withAuth(patch("/api/user")) + .contentType("application/json") + .content("{\"nickname\":\"새닉네임\"}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.user.nickname").value("새닉네임")); + } + + @Test + @DisplayName("닉네임이 빈 값이면 400을 반환한다") + void 닉네임_빈값() throws Exception { + mockMvc.perform(withAuth(patch("/api/user")) + .contentType("application/json") + .content("{\"nickname\":\"\"}")) + .andExpect(status().isBadRequest()); + + BDDMockito.verifyNoInteractions(userService); + } + } + + @Nested + @DisplayName("GET /api/user/{profileUserId}") + class GetUserProfile { + + @Test + @DisplayName("정상 요청이면 200과 프로필 정보를 반환한다") + void 정상_조회() throws Exception { + RunnectUser profileUser = buildUser(2L, "상대유저"); + UserProfileResponseDto response = UserProfileResponseDto.of( + UserProfileResponseDto.UserProfile.of(profileUser, 30), Collections.emptyList()); + when(userService.getUserProfile(2L, 1L)).thenReturn(response); + + mockMvc.perform(withAuth(get("/api/user/2"))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.user.userId").value(2)) + .andExpect(jsonPath("$.data.user.nickname").value("상대유저")); + } + } + + @Nested + @DisplayName("DELETE /api/user") + class DeleteUser { + + @Test + @DisplayName("정상 요청이면 200과 삭제된 유저 id를 반환한다") + void 정상_삭제() throws Exception { + when(userService.deleteUser(1L, "apple-token")).thenReturn(DeleteUserResponseDto.of(1L)); + + mockMvc.perform(withAuth(delete("/api/user")).header("appleAccessToken", "apple-token")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.deletedUserId").value(1)); + } + + @Test + @DisplayName("appleAccessToken 헤더가 없어도 400 없이 서비스에 null로 위임된다") + void 애플토큰_헤더_없음() throws Exception { + when(userService.deleteUser(1L, null)).thenReturn(DeleteUserResponseDto.of(1L)); + + mockMvc.perform(withAuth(delete("/api/user"))) + .andExpect(status().isOk()); + } + } +}