From de9afde787890038ee6ccdb6134c6749374867a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=82=98=EB=AF=B8?= Date: Thu, 6 Aug 2026 15:24:40 +0900 Subject: [PATCH] =?UTF-8?q?test:=20Health/Scrap/Banner/UserStamp/ServerPro?= =?UTF-8?q?file=20=EC=BB=A8=ED=8A=B8=EB=A1=A4=EB=9F=AC=20@WebMvcTest=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CourseController 파일럿에 이어 나머지 단순 컨트롤러들의 라우팅/@Valid 검증/ @UserId 인증/예외→HTTP 상태코드 매핑을 슬라이스 테스트로 검증한다. --- .../controller/BannerControllerTest.java | 56 ++++++ .../ServerProfileControllerTest.java | 36 ++++ .../controller/HealthControllerTest.java | 187 ++++++++++++++++++ .../scrap/controller/ScrapControllerTest.java | 122 ++++++++++++ .../controller/UserStampControllerTest.java | 64 ++++++ 5 files changed, 465 insertions(+) create mode 100644 src/test/java/org/runnect/server/banner/controller/BannerControllerTest.java create mode 100644 src/test/java/org/runnect/server/common/controller/ServerProfileControllerTest.java create mode 100644 src/test/java/org/runnect/server/health/controller/HealthControllerTest.java create mode 100644 src/test/java/org/runnect/server/scrap/controller/ScrapControllerTest.java create mode 100644 src/test/java/org/runnect/server/user/controller/UserStampControllerTest.java diff --git a/src/test/java/org/runnect/server/banner/controller/BannerControllerTest.java b/src/test/java/org/runnect/server/banner/controller/BannerControllerTest.java new file mode 100644 index 0000000..cd08adb --- /dev/null +++ b/src/test/java/org/runnect/server/banner/controller/BannerControllerTest.java @@ -0,0 +1,56 @@ +package org.runnect.server.banner.controller; + +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +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 java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.runnect.server.banner.dto.response.BannerResponse; +import org.runnect.server.banner.dto.response.GetBannerResponseDto; +import org.runnect.server.banner.service.BannerService; +import org.runnect.server.config.jwt.JwtService; +import org.runnect.server.config.slack.SlackApi; +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; + +@WebMvcTest(BannerController.class) +class BannerControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private BannerService bannerService; + @MockBean + private JwtService jwtService; + @MockBean + private SlackApi slackApi; + + @Test + @DisplayName("GET /api/banner - 정상 요청이면 200과 배너 목록을 반환한다") + void 정상_조회() throws Exception { + List banners = Collections.singletonList(BannerResponse.of(0, "image.png", "https://a.com")); + when(bannerService.getBanners()).thenReturn(GetBannerResponseDto.of(banners)); + + mockMvc.perform(get("/api/banner")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.banners[0].index").value(0)) + .andExpect(jsonPath("$.data.banners[0].imageUrl").value("image.png")); + } + + @Test + @DisplayName("GET /api/banner - 배너가 없으면 200과 빈 목록을 반환한다") + void 배너_없음() throws Exception { + when(bannerService.getBanners()).thenReturn(GetBannerResponseDto.of(Collections.emptyList())); + + mockMvc.perform(get("/api/banner")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.banners").isEmpty()); + } +} diff --git a/src/test/java/org/runnect/server/common/controller/ServerProfileControllerTest.java b/src/test/java/org/runnect/server/common/controller/ServerProfileControllerTest.java new file mode 100644 index 0000000..1c30afa --- /dev/null +++ b/src/test/java/org/runnect/server/common/controller/ServerProfileControllerTest.java @@ -0,0 +1,36 @@ +package org.runnect.server.common.controller; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.runnect.server.config.jwt.JwtService; +import org.runnect.server.config.slack.SlackApi; +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.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; + +@WebMvcTest(ServerProfileController.class) +@ActiveProfiles("test") +class ServerProfileControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private JwtService jwtService; + @MockBean + private SlackApi slackApi; + + @Test + @DisplayName("GET /profile - 활성 프로파일명을 그대로 반환한다") + void 활성_프로파일_반환() throws Exception { + mockMvc.perform(get("/profile")) + .andExpect(status().isOk()) + .andExpect(content().string("test")); + } +} diff --git a/src/test/java/org/runnect/server/health/controller/HealthControllerTest.java b/src/test/java/org/runnect/server/health/controller/HealthControllerTest.java new file mode 100644 index 0000000..3475fc3 --- /dev/null +++ b/src/test/java/org/runnect/server/health/controller/HealthControllerTest.java @@ -0,0 +1,187 @@ +package org.runnect.server.health.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.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.fasterxml.jackson.databind.ObjectMapper; +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.ConflictException; +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.health.dto.request.HealthDataRequestDto; +import org.runnect.server.health.dto.response.CreateHealthDataResponseDto; +import org.runnect.server.health.dto.response.GetHealthDataResponseDto; +import org.runnect.server.health.dto.response.GetHealthSummaryResponseDto; +import org.runnect.server.health.service.HealthService; +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(HealthController.class) +class HealthControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @MockBean + private HealthService healthService; + @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 HealthDataRequestDto validRequest() { + return new HealthDataRequestDto(150.0, 180.0, 100.0, 320.0, 60, 120, 90, 30, 10, 190.0, Collections.emptyList()); + } + + @Nested + @DisplayName("POST /api/record/{recordId}/health") + class CreateHealthData { + + @Test + @DisplayName("정상 요청이면 201과 생성된 건강 데이터 id를 반환한다") + void 정상_생성() throws Exception { + when(healthService.createHealthData(eq(1L), eq(10L), any())) + .thenReturn(CreateHealthDataResponseDto.of(100L)); + + mockMvc.perform(withAuth(post("/api/record/10/health")) + .contentType("application/json") + .content(objectMapper.writeValueAsString(validRequest()))) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.data.healthDataId").value(100)); + } + + @Test + @DisplayName("필수값(avgHeartRate)이 없으면 400을 반환한다") + void 필수값_없음() throws Exception { + mockMvc.perform(withAuth(post("/api/record/10/health")) + .contentType("application/json") + .content("{\"calories\":320.0,\"zone1Seconds\":1,\"zone2Seconds\":1,\"zone3Seconds\":1,\"zone4Seconds\":1,\"zone5Seconds\":1}")) + .andExpect(status().isBadRequest()); + + BDDMockito.verifyNoInteractions(healthService); + } + + @Test + @DisplayName("다른 유저의 레코드면 403을 반환한다") + void 소유권_없음() throws Exception { + when(healthService.createHealthData(eq(1L), eq(10L), any())) + .thenThrow(new PermissionDeniedException( + ErrorStatus.PERMISSION_DENIED_HEALTH_DATA_EXCEPTION, + ErrorStatus.PERMISSION_DENIED_HEALTH_DATA_EXCEPTION.getMessage())); + + mockMvc.perform(withAuth(post("/api/record/10/health")) + .contentType("application/json") + .content(objectMapper.writeValueAsString(validRequest()))) + .andExpect(status().isForbidden()); + } + + @Test + @DisplayName("이미 건강 데이터가 있으면 409를 반환한다") + void 이미_존재() throws Exception { + when(healthService.createHealthData(eq(1L), eq(10L), any())) + .thenThrow(new ConflictException( + ErrorStatus.ALREADY_EXIST_HEALTH_DATA_EXCEPTION, + ErrorStatus.ALREADY_EXIST_HEALTH_DATA_EXCEPTION.getMessage())); + + mockMvc.perform(withAuth(post("/api/record/10/health")) + .contentType("application/json") + .content(objectMapper.writeValueAsString(validRequest()))) + .andExpect(status().isConflict()); + } + } + + @Nested + @DisplayName("GET /api/record/{recordId}/health") + class GetHealthData { + + @Test + @DisplayName("정상 요청이면 200과 건강 데이터를 반환한다") + void 정상_조회() throws Exception { + GetHealthDataResponseDto.ZoneResponse zones = + GetHealthDataResponseDto.ZoneResponse.of(60, 120, 90, 30, 10); + GetHealthDataResponseDto response = GetHealthDataResponseDto.of( + GetHealthDataResponseDto.HealthDataDetailResponse.of( + 100L, 10L, 150.0, 180.0, 100.0, 320.0, zones, 190.0, Collections.emptyList())); + when(healthService.getHealthData(anyLong(), eq(10L))).thenReturn(response); + + mockMvc.perform(withAuth(get("/api/record/10/health"))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.healthData.id").value(100)); + } + } + + @Nested + @DisplayName("GET /api/health/summary") + class GetHealthSummary { + + @Test + @DisplayName("정상 요청이면 200과 요약 데이터를 반환한다") + void 정상_조회() throws Exception { + GetHealthSummaryResponseDto response = GetHealthSummaryResponseDto.of( + GetHealthSummaryResponseDto.HealthSummaryResponse.of( + 10L, 5L, 150.0, 300.0, 1500.0, + GetHealthDataResponseDto.ZoneResponse.of(60, 120, 90, 30, 10))); + when(healthService.getHealthSummary(1L, "2026-01-01", "2026-01-31")).thenReturn(response); + + mockMvc.perform(withAuth(get("/api/health/summary") + .param("startDate", "2026-01-01") + .param("endDate", "2026-01-31"))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.summary.totalRecords").value(10)); + } + + @Test + @DisplayName("필수 파라미터가 없으면 400을 반환한다") + void 파라미터_없음() throws Exception { + mockMvc.perform(withAuth(get("/api/health/summary").param("startDate", "2026-01-01"))) + .andExpect(status().isBadRequest()); + + BDDMockito.verifyNoInteractions(healthService); + } + } + + @Nested + @DisplayName("DELETE /api/record/{recordId}/health") + class DeleteHealthData { + + @Test + @DisplayName("정상 요청이면 200을 반환한다") + void 정상_삭제() throws Exception { + mockMvc.perform(withAuth(delete("/api/record/10/health"))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)); + } + } +} diff --git a/src/test/java/org/runnect/server/scrap/controller/ScrapControllerTest.java b/src/test/java/org/runnect/server/scrap/controller/ScrapControllerTest.java new file mode 100644 index 0000000..cda5a92 --- /dev/null +++ b/src/test/java/org/runnect/server/scrap/controller/ScrapControllerTest.java @@ -0,0 +1,122 @@ +package org.runnect.server.scrap.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.post; +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.scrap.dto.response.CreateAndDeleteScrapResponseDto; +import org.runnect.server.scrap.dto.response.GetScrapCourseResponseDto; +import org.runnect.server.scrap.dto.response.UserResponse; +import org.runnect.server.scrap.service.ScrapService; +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(ScrapController.class) +class ScrapControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private ScrapService scrapService; + @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/scrap") + class CreateAndDeleteScrap { + + @Test + @DisplayName("scrapTF가 true면 200과 스크랩 생성 결과를 반환한다") + void 스크랩_생성() throws Exception { + when(scrapService.createAndDeleteScrap(eq(1L), any())) + .thenReturn(CreateAndDeleteScrapResponseDto.of(10L, 1L, true)); + + mockMvc.perform(withAuth(post("/api/scrap")) + .contentType("application/json") + .content("{\"publicCourseId\":10,\"scrapTF\":true}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.scrapTF").value(true)); + } + + @Test + @DisplayName("scrapTF가 false면 200과 스크랩 삭제 결과를 반환한다") + void 스크랩_삭제() throws Exception { + when(scrapService.createAndDeleteScrap(eq(1L), any())) + .thenReturn(CreateAndDeleteScrapResponseDto.of(10L, 0L, false)); + + mockMvc.perform(withAuth(post("/api/scrap")) + .contentType("application/json") + .content("{\"publicCourseId\":10,\"scrapTF\":false}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.scrapTF").value(false)); + } + + @Test + @DisplayName("publicCourseId가 없으면 400을 반환한다") + void 코스아이디_없음() throws Exception { + mockMvc.perform(withAuth(post("/api/scrap")) + .contentType("application/json") + .content("{\"scrapTF\":true}")) + .andExpect(status().isBadRequest()); + + BDDMockito.verifyNoInteractions(scrapService); + } + + @Test + @DisplayName("scrapTF가 없으면 400을 반환한다") + void 스크랩여부_없음() throws Exception { + mockMvc.perform(withAuth(post("/api/scrap")) + .contentType("application/json") + .content("{\"publicCourseId\":10}")) + .andExpect(status().isBadRequest()); + + BDDMockito.verifyNoInteractions(scrapService); + } + } + + @Nested + @DisplayName("GET /api/scrap/user") + class GetScrapCourseByUser { + + @Test + @DisplayName("정상 요청이면 200과 스크랩 목록을 반환한다") + void 정상_조회() throws Exception { + when(scrapService.getScrapCourseByUser(1L)) + .thenReturn(GetScrapCourseResponseDto.of(UserResponse.of(1L), Collections.emptyList())); + + mockMvc.perform(withAuth(get("/api/scrap/user"))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.user.userId").value(1)); + } + } +} diff --git a/src/test/java/org/runnect/server/user/controller/UserStampControllerTest.java b/src/test/java/org/runnect/server/user/controller/UserStampControllerTest.java new file mode 100644 index 0000000..16a1f4c --- /dev/null +++ b/src/test/java/org/runnect/server/user/controller/UserStampControllerTest.java @@ -0,0 +1,64 @@ +package org.runnect.server.user.controller; + +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +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.user.dto.response.GetUserStampsResponseDto; +import org.runnect.server.user.entity.RunnectUser; +import org.runnect.server.user.entity.SocialType; +import org.runnect.server.user.service.UserStampService; +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(UserStampController.class) +class UserStampControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private UserStampService userStampService; + @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; + } + + @Test + @DisplayName("GET /api/stamp/user - 정상 요청이면 200과 유저 스탬프 목록을 반환한다") + void 정상_조회() throws Exception { + RunnectUser user = RunnectUser.builder() + .nickname("러너1").socialId("social-1").email("user1@runnect.io") + .provider(SocialType.KAKAO).build(); + ReflectionTestUtils.setField(user, "id", 1L); + when(userStampService.findUserStamps(1L)).thenReturn(GetUserStampsResponseDto.from(user)); + + mockMvc.perform(withAuth(get("/api/stamp/user"))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.user.id").value(1)) + .andExpect(jsonPath("$.data.stamps").isEmpty()); + } +}