From 37556d7f5ce63462ee62d08fc0b38ac2e60a625e Mon Sep 17 00:00:00 2001 From: moses249 Date: Mon, 27 Oct 2025 21:32:53 +0900 Subject: [PATCH 1/6] =?UTF-8?q?test=20#223:=20=EC=BF=A0=ED=8F=B0=20api=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/docs/asciidoc/_coupon.adoc | 27 +++ src/docs/asciidoc/index.adoc | 1 + .../controller/CouponController.java | 7 +- .../dto/response/CouponResponse.java | 9 +- .../domain/coupon/CouponControllerTest.java | 170 ++++++++++++++++++ 5 files changed, 209 insertions(+), 5 deletions(-) create mode 100644 src/docs/asciidoc/_coupon.adoc create mode 100644 src/test/java/com/irum/come2us/domain/coupon/CouponControllerTest.java diff --git a/src/docs/asciidoc/_coupon.adoc b/src/docs/asciidoc/_coupon.adoc new file mode 100644 index 00000000..8cd18e0f --- /dev/null +++ b/src/docs/asciidoc/_coupon.adoc @@ -0,0 +1,27 @@ +== 쿠폰 (Coupons) API + +=== 1. 쿠폰 생성 +`POST /coupons` + +요청 본문: +include::{snippets}/coupon-create/request-fields.adoc[] + +응답: +include::{snippets}/coupon-create/http-response.adoc[] + + +=== 2. 쿠폰 조회 +`GET /coupons` + +응답 본문: +include::{snippets}/coupon-list/response-fields.adoc[] + +응답: +include::{snippets}/coupon-list/http-response.adoc[] + + +=== 7. 쿠폰 삭제 +`DELETE /coupons` + +응답: +include::{snippets}/coupon-delete/http-response.adoc[] \ No newline at end of file diff --git a/src/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc index bb7a95dc..41a9f973 100644 --- a/src/docs/asciidoc/index.adoc +++ b/src/docs/asciidoc/index.adoc @@ -8,6 +8,7 @@ include::_member.adoc[] include::_manager.adoc[] +include::_coupon.adoc[] // include::_product.adoc[] diff --git a/src/main/java/com/irum/come2us/domain/coupon/presentation/controller/CouponController.java b/src/main/java/com/irum/come2us/domain/coupon/presentation/controller/CouponController.java index 63e4ea9a..d936d92a 100644 --- a/src/main/java/com/irum/come2us/domain/coupon/presentation/controller/CouponController.java +++ b/src/main/java/com/irum/come2us/domain/coupon/presentation/controller/CouponController.java @@ -32,13 +32,12 @@ public ResponseEntity createCoupon( public ResponseEntity> getCoupon( @AuthenticationPrincipal UserDetails userDetails) { Long memberId = Long.parseLong(userDetails.getUsername()); - List coupons = couponService.getCouponByMember(memberId); - return ResponseEntity.ok(coupons); + return ResponseEntity.ok(couponService.getCouponByMember(memberId)); } - @DeleteMapping("/{coupon-id}") + @DeleteMapping("/{couponId}") public ResponseEntity deleteCoupon( - @PathVariable("coupon-id") UUID couponId, + @PathVariable("couponId") UUID couponId, @AuthenticationPrincipal UserDetails userDetails) { Long memberId = Long.parseLong(userDetails.getUsername()); diff --git a/src/main/java/com/irum/come2us/domain/coupon/presentation/dto/response/CouponResponse.java b/src/main/java/com/irum/come2us/domain/coupon/presentation/dto/response/CouponResponse.java index 50deebf8..3dcf4b88 100644 --- a/src/main/java/com/irum/come2us/domain/coupon/presentation/dto/response/CouponResponse.java +++ b/src/main/java/com/irum/come2us/domain/coupon/presentation/dto/response/CouponResponse.java @@ -1,10 +1,17 @@ package com.irum.come2us.domain.coupon.presentation.dto.response; +import com.fasterxml.jackson.annotation.JsonFormat; import com.irum.come2us.domain.coupon.domain.entity.Coupon; import java.time.LocalDateTime; import java.util.UUID; -public record CouponResponse(UUID id, String name, int discountAmount, LocalDateTime expiration) { +public record CouponResponse( + UUID id, + String name, + int discountAmount, + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") // 명시적 포맷 추가 + // 또는 요청 DTO와 동일하게 @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + LocalDateTime expiration) { public static CouponResponse from(Coupon coupon) { return new CouponResponse( coupon.getId(), diff --git a/src/test/java/com/irum/come2us/domain/coupon/CouponControllerTest.java b/src/test/java/com/irum/come2us/domain/coupon/CouponControllerTest.java new file mode 100644 index 00000000..cb797375 --- /dev/null +++ b/src/test/java/com/irum/come2us/domain/coupon/CouponControllerTest.java @@ -0,0 +1,170 @@ +package com.irum.come2us.domain.coupon; + +import static org.hamcrest.Matchers.hasSize; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.*; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.payload.PayloadDocumentation.*; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +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 com.irum.come2us.domain.coupon.application.service.CouponService; +import com.irum.come2us.domain.coupon.presentation.controller.CouponController; +import com.irum.come2us.domain.coupon.presentation.dto.request.CouponGenerateRequest; +import com.irum.come2us.domain.coupon.presentation.dto.response.CouponResponse; +import com.irum.come2us.global.config.SecurityTestConfig; +import com.irum.come2us.global.presentation.advice.exception.CommonException; +import com.irum.come2us.global.presentation.advice.exception.errorcode.CouponErrorCode; +import java.time.LocalDateTime; +import java.util.List; +import java.util.UUID; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.Primary; +import org.springframework.test.web.servlet.MockMvc; + +@WebMvcTest(CouponController.class) +@AutoConfigureRestDocs +@Import({SecurityTestConfig.class, CouponControllerTest.TestConfig.class}) +public class CouponControllerTest { + @Autowired private MockMvc mockMvc; + @Autowired private CouponService couponService; + @Autowired private ObjectMapper objectMapper; + + @TestConfiguration + static class TestConfig { + + @Bean + @Primary + public CouponService couponService() { + return Mockito.mock(CouponService.class); + } + } + + @Test + @DisplayName("쿠폰 생성 API") + void couponCreateApiTest() throws Exception { + + // Given + CouponGenerateRequest request = + new CouponGenerateRequest( + "기간 한정 5000원 할인쿠폰", 5000, LocalDateTime.of(2028, 10, 10, 12, 12, 12)); + String requestJson = objectMapper.writeValueAsString(request); + + doNothing().when(couponService).createCoupon(any(CouponGenerateRequest.class), anyLong()); + + // when + mockMvc.perform( + post("/coupons") + .with(csrf()) + .with(user("1")) + .contentType("application/json") + .content(requestJson)) + .andExpect(status().isCreated()) + .andDo( + document( + "coupon-create", + requestFields( + fieldWithPath("name").description("쿠폰명"), + fieldWithPath("discountAmount").description("할인 금액"), + fieldWithPath("expiration").description("유효기간")))); + } + + @Test + @DisplayName("쿠폰 정보 조회 API") + void couponListTest() throws Exception { + // Given + LocalDateTime now = LocalDateTime.of(2028, 10, 10, 12, 12, 12); + CouponResponse coupon1 = + new CouponResponse( + UUID.fromString("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"), + "기간 한정 5000원 할인쿠폰", + 5000, + now); + + CouponResponse coupon2 = + new CouponResponse( + UUID.fromString("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"), + "신규가입 10% 할인", + 10, + now.plusDays(1)); + + List responses = List.of(coupon1, coupon2); + when(couponService.getCouponByMember(anyLong())).thenReturn(responses); + + // When & Then + mockMvc.perform(get("/coupons").with(user("1")).accept("application/json")) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.status").value(200)) + .andExpect(jsonPath("$.data").isArray()) + .andExpect(jsonPath("$.data", hasSize(2))) + // coupon1 + .andExpect(jsonPath("$.data[0].id").value("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa")) + .andExpect(jsonPath("$.data[0].name").value("기간 한정 5000원 할인쿠폰")) + .andExpect(jsonPath("$.data[0].discountAmount").value(5000)) + .andExpect(jsonPath("$.data[0].expiration").value("2028-10-10 12:12:12")) + // coupon2 + .andExpect(jsonPath("$.data[1].id").value("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb")) + .andExpect(jsonPath("$.data[1].name").value("신규가입 10% 할인")) + .andExpect(jsonPath("$.data[1].discountAmount").value(10)) + .andExpect(jsonPath("$.data[1].expiration").value("2028-10-11 12:12:12")) + .andExpect(jsonPath("$.timestamp").exists()) + .andDo( + document( + "coupon-list", + responseFields( + fieldWithPath("success").description("성공 여부"), + fieldWithPath("status").description("HTTP 상태 코드"), + fieldWithPath("data").description("쿠폰 목록"), + fieldWithPath("data[].id").description("쿠폰 ID"), + fieldWithPath("data[].name").description("쿠폰명"), + fieldWithPath("data[].discountAmount").description("할인 금액"), + fieldWithPath("data[].expiration") + .description("만료 일시 (yyyy-MM-dd HH:mm:ss)"), + fieldWithPath("timestamp").description("응답 생성 시각")))); + } + + @Test + @DisplayName("쿠폰 삭제 API") + void couponDeleteApiTest() throws Exception { + // Given + UUID couponId = UUID.fromString("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"); + doNothing().when(couponService).deleteCoupon(couponId, 1L); + + // When & Then + mockMvc.perform(delete("/coupons/{couponId}", couponId).with(csrf()).with(user("1"))) + .andExpect(status().isNoContent()) + .andDo(document("coupon-delete")); + + Mockito.verify(couponService).deleteCoupon(couponId, 1L); + } + + // import static org.mockito.Mockito.doThrow; + + @Test + @DisplayName("쿠폰 삭제 API - 쿠폰 없음") + void couponDelete_NotFound() throws Exception { + UUID couponId = UUID.fromString("cccccccc-bbbb-bbbb-bbbb-bbbbbbbbbbbb"); + doThrow(new CommonException(CouponErrorCode.COUPON_NOT_FOUND)) + .when(couponService) + .deleteCoupon(couponId, 1L); + + mockMvc.perform(delete("/coupons/{coupon-id}", couponId).with(csrf()).with(user("1"))) + .andExpect(status().isNotFound()); + } +} From 3f3ff854ea8a4d9d323c2e4147a1f8fb4ba4fd71 Mon Sep 17 00:00:00 2001 From: moses249 Date: Tue, 28 Oct 2025 08:34:34 +0900 Subject: [PATCH 2/6] =?UTF-8?q?test=20#223:=20=EC=88=98=EC=A0=95=EC=82=AC?= =?UTF-8?q?=ED=95=AD=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/CouponController.java | 19 ++++++++----------- .../domain/coupon/CouponControllerTest.java | 2 +- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/irum/come2us/domain/coupon/presentation/controller/CouponController.java b/src/main/java/com/irum/come2us/domain/coupon/presentation/controller/CouponController.java index d936d92a..5db55779 100644 --- a/src/main/java/com/irum/come2us/domain/coupon/presentation/controller/CouponController.java +++ b/src/main/java/com/irum/come2us/domain/coupon/presentation/controller/CouponController.java @@ -3,6 +3,7 @@ import com.irum.come2us.domain.coupon.application.service.CouponService; import com.irum.come2us.domain.coupon.presentation.dto.request.CouponGenerateRequest; import com.irum.come2us.domain.coupon.presentation.dto.response.CouponResponse; +import com.irum.come2us.global.util.MemberUtil; import jakarta.validation.Valid; import java.util.List; import java.util.UUID; @@ -18,29 +19,25 @@ @RequiredArgsConstructor public class CouponController { private final CouponService couponService; + private final MemberUtil memberUtil; @PostMapping - public ResponseEntity createCoupon( - @Valid @RequestBody CouponGenerateRequest request, - @AuthenticationPrincipal UserDetails userDetails) { - Long memberId = Long.parseLong(userDetails.getUsername()); + public ResponseEntity createCoupon(@Valid @RequestBody CouponGenerateRequest request) { + Long memberId = memberUtil.getCurrentMember().getMemberId(); couponService.createCoupon(request, memberId); return ResponseEntity.status(HttpStatus.CREATED).build(); } @GetMapping - public ResponseEntity> getCoupon( - @AuthenticationPrincipal UserDetails userDetails) { - Long memberId = Long.parseLong(userDetails.getUsername()); + public ResponseEntity> getCoupon() { + Long memberId = memberUtil.getCurrentMember().getMemberId(); return ResponseEntity.ok(couponService.getCouponByMember(memberId)); } @DeleteMapping("/{couponId}") public ResponseEntity deleteCoupon( - @PathVariable("couponId") UUID couponId, - @AuthenticationPrincipal UserDetails userDetails) { - - Long memberId = Long.parseLong(userDetails.getUsername()); + @PathVariable("couponId") UUID couponId) { + Long memberId = memberUtil.getCurrentMember().getMemberId(); couponService.deleteCoupon(couponId, memberId); return ResponseEntity.noContent().build(); } diff --git a/src/test/java/com/irum/come2us/domain/coupon/CouponControllerTest.java b/src/test/java/com/irum/come2us/domain/coupon/CouponControllerTest.java index cb797375..3f0ea265 100644 --- a/src/test/java/com/irum/come2us/domain/coupon/CouponControllerTest.java +++ b/src/test/java/com/irum/come2us/domain/coupon/CouponControllerTest.java @@ -38,7 +38,7 @@ @WebMvcTest(CouponController.class) @AutoConfigureRestDocs -@Import({SecurityTestConfig.class, CouponControllerTest.TestConfig.class}) +@Import(SecurityTestConfig.class) public class CouponControllerTest { @Autowired private MockMvc mockMvc; @Autowired private CouponService couponService; From 7222b54878842adea17abd93e3ff3c89cfe755f9 Mon Sep 17 00:00:00 2001 From: moses249 Date: Tue, 28 Oct 2025 08:35:11 +0900 Subject: [PATCH 3/6] =?UTF-8?q?test=20#223:=20=EC=88=98=EC=A0=95=EC=82=AC?= =?UTF-8?q?=ED=95=AD=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../coupon/presentation/controller/CouponController.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/main/java/com/irum/come2us/domain/coupon/presentation/controller/CouponController.java b/src/main/java/com/irum/come2us/domain/coupon/presentation/controller/CouponController.java index 5db55779..006a73aa 100644 --- a/src/main/java/com/irum/come2us/domain/coupon/presentation/controller/CouponController.java +++ b/src/main/java/com/irum/come2us/domain/coupon/presentation/controller/CouponController.java @@ -10,8 +10,6 @@ import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.security.core.userdetails.UserDetails; import org.springframework.web.bind.annotation.*; @RestController @@ -35,8 +33,7 @@ public ResponseEntity> getCoupon() { } @DeleteMapping("/{couponId}") - public ResponseEntity deleteCoupon( - @PathVariable("couponId") UUID couponId) { + public ResponseEntity deleteCoupon(@PathVariable("couponId") UUID couponId) { Long memberId = memberUtil.getCurrentMember().getMemberId(); couponService.deleteCoupon(couponId, memberId); return ResponseEntity.noContent().build(); From 50c852b21c1cf3ac64bb7eb11af817e69badab75 Mon Sep 17 00:00:00 2001 From: moses249 Date: Tue, 28 Oct 2025 08:38:32 +0900 Subject: [PATCH 4/6] =?UTF-8?q?test=20#223:=20=EC=88=98=EC=A0=95=EC=82=AC?= =?UTF-8?q?=ED=95=AD=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/docs/asciidoc/index.adoc | 1 + 1 file changed, 1 insertion(+) diff --git a/src/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc index 5328e74e..7d53b47a 100644 --- a/src/docs/asciidoc/index.adoc +++ b/src/docs/asciidoc/index.adoc @@ -11,6 +11,7 @@ include::_manager.adoc[] include::_auth.adoc[] include::_delivery_address.adoc[] include::_discount.adoc[] +include::_coupon.adoc[] // include::_product.adoc[] From 16ddcb763fc43cbaf7c873b19bf2c375706d80d6 Mon Sep 17 00:00:00 2001 From: moses249 Date: Tue, 28 Oct 2025 08:57:40 +0900 Subject: [PATCH 5/6] =?UTF-8?q?test=20#223:=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=20=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/coupon/CouponControllerTest.java | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/test/java/com/irum/come2us/domain/coupon/CouponControllerTest.java b/src/test/java/com/irum/come2us/domain/coupon/CouponControllerTest.java index 3f0ea265..8021bd19 100644 --- a/src/test/java/com/irum/come2us/domain/coupon/CouponControllerTest.java +++ b/src/test/java/com/irum/come2us/domain/coupon/CouponControllerTest.java @@ -18,9 +18,11 @@ import com.irum.come2us.domain.coupon.presentation.controller.CouponController; import com.irum.come2us.domain.coupon.presentation.dto.request.CouponGenerateRequest; import com.irum.come2us.domain.coupon.presentation.dto.response.CouponResponse; +import com.irum.come2us.domain.member.domain.entity.Member; import com.irum.come2us.global.config.SecurityTestConfig; import com.irum.come2us.global.presentation.advice.exception.CommonException; import com.irum.come2us.global.presentation.advice.exception.errorcode.CouponErrorCode; +import com.irum.come2us.global.util.MemberUtil; import java.time.LocalDateTime; import java.util.List; import java.util.UUID; @@ -33,7 +35,6 @@ import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; -import org.springframework.context.annotation.Primary; import org.springframework.test.web.servlet.MockMvc; @WebMvcTest(CouponController.class) @@ -42,22 +43,33 @@ public class CouponControllerTest { @Autowired private MockMvc mockMvc; @Autowired private CouponService couponService; + @Autowired private MemberUtil memberUtil; @Autowired private ObjectMapper objectMapper; + private void mockCurrentMemberId(long memberId) { + Member member = Mockito.mock(Member.class); + when(member.getMemberId()).thenReturn(memberId); + when(memberUtil.getCurrentMember()).thenReturn(member); + } + @TestConfiguration static class TestConfig { @Bean - @Primary public CouponService couponService() { return Mockito.mock(CouponService.class); } + + @Bean + public MemberUtil memberUtil() { + return Mockito.mock(MemberUtil.class); + } } @Test @DisplayName("쿠폰 생성 API") void couponCreateApiTest() throws Exception { - + mockCurrentMemberId(1L); // Given CouponGenerateRequest request = new CouponGenerateRequest( @@ -86,6 +98,7 @@ void couponCreateApiTest() throws Exception { @Test @DisplayName("쿠폰 정보 조회 API") void couponListTest() throws Exception { + mockCurrentMemberId(1L); // Given LocalDateTime now = LocalDateTime.of(2028, 10, 10, 12, 12, 12); CouponResponse coupon1 = @@ -142,6 +155,7 @@ void couponListTest() throws Exception { @Test @DisplayName("쿠폰 삭제 API") void couponDeleteApiTest() throws Exception { + mockCurrentMemberId(1L); // Given UUID couponId = UUID.fromString("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"); doNothing().when(couponService).deleteCoupon(couponId, 1L); @@ -164,7 +178,7 @@ void couponDelete_NotFound() throws Exception { .when(couponService) .deleteCoupon(couponId, 1L); - mockMvc.perform(delete("/coupons/{coupon-id}", couponId).with(csrf()).with(user("1"))) + mockMvc.perform(delete("/coupons/{couponId}", couponId).with(csrf()).with(user("1"))) .andExpect(status().isNotFound()); } } From 635b83bfd1dc57b47c490b5b37427d49776be9c0 Mon Sep 17 00:00:00 2001 From: Sherlock_Moses Date: Wed, 29 Oct 2025 16:53:12 +0900 Subject: [PATCH 6/6] =?UTF-8?q?test=20#223:=20=EC=BF=A0=ED=8F=B0=20API=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/docs/asciidoc/_coupon.adoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/docs/asciidoc/_coupon.adoc b/src/docs/asciidoc/_coupon.adoc index 8cd18e0f..d0872dca 100644 --- a/src/docs/asciidoc/_coupon.adoc +++ b/src/docs/asciidoc/_coupon.adoc @@ -20,8 +20,8 @@ include::{snippets}/coupon-list/response-fields.adoc[] include::{snippets}/coupon-list/http-response.adoc[] -=== 7. 쿠폰 삭제 +=== 3. 쿠폰 삭제 `DELETE /coupons` 응답: -include::{snippets}/coupon-delete/http-response.adoc[] \ No newline at end of file +include::{snippets}/coupon-delete/http-response.adoc[]