Skip to content

[FEAT]: 테스트코드 추가 및 개선#142

Open
zerochani wants to merge 9 commits intomainfrom
feature/test1
Open

[FEAT]: 테스트코드 추가 및 개선#142
zerochani wants to merge 9 commits intomainfrom
feature/test1

Conversation

@zerochani
Copy link
Contributor

@zerochani zerochani commented Feb 17, 2026

💡 작업 개요

  • 결제(Payment) 도메인의 안정성을 확보하기 위해 Controller와 Service 계층의 단위 테스트 코드를 작성했습니다.
  • 민감한 키 값 환경변수화했습니다.
  • 인증되지 않은 요청이나 다른 사용자의 결제 내역에 접근하는 시도가 차단됩니다.
  • 결제 내역 조회 시 발생할 수 있는 N+1 문제를 해결하고, 다른 사용자의 결제 내역을 조회할 수 없도록 보안 로직을 강화했습니다.

✅ 작업 내용

  • 기능 개발
  • 버그 수정
  • 리팩토링
  • 주석/포맷 정리
  • 기타 설정

🧪 테스트 내용

  • JUnit 5 & Mockito를 사용하여 단위 테스트 진행
  • ./gradlew test 실행 결과 모든 테스트 통과

📝 기타 참고 사항

  • N+1 문제 해결: 기존에는 결제 내역 조회 후 예약 정보(Booking), 가게 정보(Store)를 접근할 때마다 추가 쿼리가 발생할 수 있었으나, JOIN FETCH를 통해 한 번의 쿼리로 데이터를 가져오도록 최적화했습니다.
  • 보안 강화: getPaymentDetail 에서 소유권 검증 로직을 서비스 단에서 강제함으로써, ID만 알면 다른 사람의 결제 내역을 볼 수 있는 보안 취약점을 예방했습니다.

Summary by CodeRabbit

릴리스 노트

  • 새 기능

    • 인증된 사용자만 자신의 결제 정보를 조회할 수 있도록 개선
  • 개선 사항

    • 결제 상세 조회 시 접근 권한 검증 추가
    • 결제 정보 조회 시 관련 데이터 함께 로드 최적화
    • 환경 변수 기반 설정으로 보안 강화
  • 테스트

    • 결제 기능 통합 및 단위 테스트 추가

@zerochani zerochani self-assigned this Feb 17, 2026
@coderabbitai
Copy link

coderabbitai bot commented Feb 17, 2026

📝 Walkthrough

Walkthrough

결제 작업에 사용자 기반 인증을 추가하여 인증된 사용자에서 userId를 추출하고 결제 소유권을 검증합니다. 저장소 쿼리를 관련 엔터티를 즉시 로드하도록 업데이트하고 비밀값에 대한 환경 기반 구성을 도입합니다.

Changes

Cohort / File(s) Summary
Authentication & Authorization
src/main/java/com/eatsfine/eatsfine/domain/payment/controller/PaymentController.java, src/main/java/com/eatsfine/eatsfine/domain/payment/service/PaymentService.java
@AuthenticationPrincipal로 인증된 사용자를 받아 userId를 해석하고, 결제 상세 조회 시 소유권 검증을 추가합니다. UserRepository 의존성 추가 및 접근 제어 로직 구현.
Repository Query Optimization
src/main/java/com/eatsfine/eatsfine/domain/payment/repository/PaymentRepository.java
메서드 기반 쿼리를 @Query 주석 방식으로 대체하고 JOIN FETCH를 통해 booking, store, user 엔터티를 즉시 로드합니다. 새로운 소유권 검증을 위한 findByIdWithDetails 메서드 추가.
Error Handling
src/main/java/com/eatsfine/eatsfine/domain/payment/status/PaymentErrorStatus.java
결제 접근 권한 거부(403 Forbidden)를 나타내는 _PAYMENT_ACCESS_DENIED 열거형 상수 추가.
Configuration
src/main/resources/application-local.yml
Toss 위젯 시크릿 키, AWS 지역, S3 버킷, 서비스 API 키 등을 환경 변수 참조로 대체합니다.
Test Coverage
src/test/java/com/eatsfine/eatsfine/domain/payment/controller/PaymentControllerTest.java, src/test/java/com/eatsfine/eatsfine/domain/payment/service/PaymentServiceTest.java
결제 요청, 확인, 취소, 목록 조회, 상세 조회에 대한 컨트롤러 및 서비스 단위 테스트 추가 (MockMvc 및 Mockito 활용).

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant PaymentController
    participant UserRepository
    participant PaymentService
    participant PaymentRepository
    participant Database

    Client->>PaymentController: GET /payments/detail/{paymentId}<br/>(with auth token)
    PaymentController->>PaymentController: Extract User from `@AuthenticationPrincipal`
    PaymentController->>UserRepository: findByEmail(user.email)
    UserRepository->>Database: Query user by email
    Database-->>UserRepository: User record
    UserRepository-->>PaymentController: User ID
    PaymentController->>PaymentService: getPaymentDetail(paymentId, userId)
    PaymentService->>PaymentRepository: findByIdWithDetails(paymentId)
    PaymentRepository->>Database: SELECT p FROM Payment p<br/>JOIN FETCH p.booking b<br/>JOIN FETCH b.store<br/>JOIN FETCH b.user
    Database-->>PaymentRepository: Payment with associations
    PaymentRepository-->>PaymentService: Payment entity
    PaymentService->>PaymentService: Validate ownership<br/>(payment.booking.user.id == userId)
    alt Ownership Valid
        PaymentService-->>PaymentController: Payment with details
        PaymentController-->>Client: 200 OK + Payment JSON
    else Ownership Invalid
        PaymentService-->>PaymentController: PaymentException<br/>(PAYMENT_ACCESS_DENIED)
        PaymentController-->>Client: 403 Forbidden
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers

  • sonjunkyu
  • SungMinju
  • CokaNuri

Poem

🐰 결제마다 누구의 것인지 확인하고,
권한 없는 자는 문을 걷어차네!
환경 변수로 비밀도 잠그고,
JOIN FETCH로 한 번에 쓱쓱,
테스트도 빈틈없이 마무리 ~🎉

🚥 Pre-merge checks | ✅ 2 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning PR 제목은 '테스트코드 추가 및 개선'이지만, 실제 변경사항은 테스트 코드 추가뿐 아니라 결제 도메인의 보안 강화, N+1 문제 해결, 환경변수화, 인증 로직 개선 등 광범위한 기능 개발을 포함하고 있어 주요 변경사항을 충분히 반영하지 못함. PR 제목을 '[FEAT]: 결제 도메인 보안 강화 및 성능 최적화'와 같이 더 구체적으로 수정하여 인증/접근제어, N+1 해결 등 핵심 변경사항을 포함시키기 바랍니다.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description check ✅ Passed PR 설명은 템플릿의 모든 필수 섹션(작업 개요, 작업 내용, 테스트 내용, 기타 참고 사항)을 포함하고 있으며, 변경사항에 대한 상세한 설명과 보안/성능 개선에 대한 기술적 배경도 제공하고 있습니다.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into main

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/test1

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main/java/com/eatsfine/eatsfine/domain/payment/service/PaymentService.java (1)

43-53: ⚠️ Potential issue | 🟠 Major

requestPayment, confirmPayment, cancelPayment 엔드포인트에 사용자 인증 및 소유권 검증이 누락되어 있습니다.

getPaymentListgetPaymentDetail에는 @AuthenticationPrincipal과 소유권 검증이 적용되었지만, 결제 요청·승인·취소 엔드포인트는 인증 없이 호출 가능한 상태입니다:

  • requestPayment: 임의의 bookingId로 누구나 결제를 생성할 수 있음
  • confirmPayment: orderId, amount, paymentKey를 알면 누구나 결제를 승인할 수 있음
  • cancelPayment: paymentKey만 알면 누구나 결제를 취소할 수 있음

이는 PaymentWebhookController가 별도로 존재하고 서명 검증을 수행하는 것과 별개로, 사용자 요청용 컨트롤러 엔드포인트에도 인증 및 소유권 검증이 필요합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/eatsfine/eatsfine/domain/payment/service/PaymentService.java`
around lines 43 - 53, Add authentication and ownership checks to requestPayment,
confirmPayment, and cancelPayment: require the authenticated principal (add an
`@AuthenticationPrincipal` parameter, e.g., Principal or CustomUserDetails) in
each method signature, then verify ownership by comparing the booking or payment
owner id (use Booking booking = bookingRepository.findById(...) in
requestPayment and validate booking.getMemberId()/getUserId() against
principal.getId(); in confirmPayment locate the payment by orderId/paymentKey
and validate its owner; in cancelPayment locate the payment by paymentKey and
validate its owner) and throw the appropriate PaymentException (use the existing
PaymentErrorStatus value for unauthorized access) when the principal does not
match. Ensure you re-use method names requestPayment, confirmPayment,
cancelPayment and repository lookup methods to locate the entities before
proceeding.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@src/main/java/com/eatsfine/eatsfine/domain/payment/controller/PaymentController.java`:
- Line 31: The PaymentController currently injects and uses UserRepository
(field userRepository), which violates layering; move user-lookup logic out of
the controller by delegating to a dedicated component such as a UserService or
AuthUserResolver: create or reuse a service (e.g., UserService or
AuthUserResolver with a method resolveUserId(User user) or similar) that
encapsulates the repository call and exception handling, inject that service
into PaymentController in place of UserRepository, and update BookingController
and any other controllers using the same pattern to call the shared
resolver/service instead of referencing UserRepository directly; ensure
PaymentService handles business rules and controllers only orchestrate requests.

In `@src/main/resources/application-local.yml`:
- Around line 72-74: The YAML is missing a blank line between the top-level
sections 'api' and 'jwt', causing inconsistent spacing with other sections like
'payment' and 'cloud'; insert a single empty line between the 'api' block
(service-key: ${BIZ_API_KEY}) and the 'jwt' key in application-local.yml so all
top-level sections have the same blank-line separation for consistency.

In
`@src/test/java/com/eatsfine/eatsfine/domain/payment/controller/PaymentControllerTest.java`:
- Around line 153-200: Tests only cover success paths for getPaymentList and
getPaymentDetail; add negative/auth and error-case unit tests: (1) an
unauthenticated request (call the same endpoints without `@WithMockUser`)
asserting 401/403; (2) a "user not found" case by stubbing
userRepository.findByEmail(...) to return Optional.empty() and asserting the
controller returns the expected error response/status; (3) an access-denied case
by stubbing paymentService.getPaymentDetail(paymentId, userId) to throw the
PaymentException with code _PAYMENT_ACCESS_DENIED and asserting the controller
maps it to the correct error status/body; reference the existing test methods
getPaymentList_success and getPaymentDetail_success, the
userRepository.findByEmail, paymentService.getPaymentDetail, and
PaymentException/_PAYMENT_ACCESS_DENIED when adding these new test methods.
- Around line 53-66: The tests use deprecated `@MockBean`; update the five mock
declarations in PaymentControllerTest (fields paymentService,
jwtAuthenticationFilter, customAuthenticationEntryPoint,
customAccessDeniedHandler, userRepository) to use `@MockitoBean` instead and
change the import from org.springframework.boot.test.mock.mockito.MockBean to
org.springframework.test.context.bean.override.mockito.MockitoBean; apply the
same import+annotation replacement in other test classes named (e.g.,
HealthControllerTest, InquiryControllerTest) so all Mockito-based test beans use
`@MockitoBean`.

In
`@src/test/java/com/eatsfine/eatsfine/domain/payment/service/PaymentServiceTest.java`:
- Around line 39-217: Add unit tests in PaymentServiceTest covering ownership
checks and list retrieval: (1) a success case for
PaymentService.getPaymentDetail where the requesting user's id matches the
payment's owner and the method returns expected DTO; (2) an access-denied case
for getPaymentDetail where paymentRepository.findById(...) returns a Payment
owned by a different user and assert that calling
paymentService.getPaymentDetail(...) throws PaymentException with code
PaymentErrorStatus._PAYMENT_ACCESS_DENIED and that the payment status remains
unchanged; and (3) a normal getPaymentList case that stubs
paymentRepository.findByBookingMemberId(...) or the repository method used by
PaymentService.getPaymentList to return a page/list of payments and assert the
returned list/DTO content matches expectations; use the existing test patterns
(given(...).willReturn(...), assertThat(...), assertThatThrownBy(...),
verify(...)) and the same helper symbols PaymentService, PaymentRepository,
PaymentResponseDTO, PaymentErrorStatus._PAYMENT_ACCESS_DENIED, and
PaymentServiceTest to locate where to add these tests.
- Around line 129-142: Remove the long inline Korean comment after the
TossPaymentResponse constructor call in PaymentServiceTest and replace it by
extracting a helper/builder method (e.g., createTossPaymentResponse(...) or a
TossPaymentResponseTestBuilder) to construct the test response; update the test
to call that helper (referencing TossPaymentResponse and PaymentServiceTest) so
the test body stays concise and all construction details live in the new helper
method.

---

Outside diff comments:
In
`@src/main/java/com/eatsfine/eatsfine/domain/payment/service/PaymentService.java`:
- Around line 43-53: Add authentication and ownership checks to requestPayment,
confirmPayment, and cancelPayment: require the authenticated principal (add an
`@AuthenticationPrincipal` parameter, e.g., Principal or CustomUserDetails) in
each method signature, then verify ownership by comparing the booking or payment
owner id (use Booking booking = bookingRepository.findById(...) in
requestPayment and validate booking.getMemberId()/getUserId() against
principal.getId(); in confirmPayment locate the payment by orderId/paymentKey
and validate its owner; in cancelPayment locate the payment by paymentKey and
validate its owner) and throw the appropriate PaymentException (use the existing
PaymentErrorStatus value for unauthorized access) when the principal does not
match. Ensure you re-use method names requestPayment, confirmPayment,
cancelPayment and repository lookup methods to locate the entities before
proceeding.

public class PaymentController {

private final PaymentService paymentService;
private final UserRepository userRepository;
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Controller에서 UserRepository를 직접 참조하는 것은 계층 구조 위반입니다.

Controller가 Repository를 직접 의존하면 계층 간 결합도가 높아지고 단일 책임 원칙(SRP)에 어긋납니다. 사용자 조회 로직은 별도의 UserService로 위임하거나, PaymentService에서 인증 사용자 해석을 담당하는 것이 바람직합니다.

다만, 기존 BookingController 등에서 동일한 패턴을 사용하고 있다면, 프로젝트 전체적으로 공통 유틸리티(예: AuthUserResolver)를 추출하는 리팩토링을 고려해 주세요.

As per coding guidelines, "Controller, Service, Repository 등 계층 구조가 올바르게 나뉘어 있는지" 확인 필요.

♻️ 공통 유틸리티 추출 제안

별도의 컴포넌트로 사용자 ID 해석 로직을 추출:

`@Component`
`@RequiredArgsConstructor`
public class AuthUserResolver {
    private final UserRepository userRepository;

    public Long resolveUserId(User user) {
        String email = user.getUsername();
        return userRepository.findByEmail(email)
                .orElseThrow(() -> new UserException(UserErrorStatus.MEMBER_NOT_FOUND))
                .getId();
    }
}

또는 HandlerMethodArgumentResolver를 구현하여 @CurrentUserId Long userId와 같은 커스텀 어노테이션으로 자동 해석하는 방법도 있습니다.

Also applies to: 75-79

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/eatsfine/eatsfine/domain/payment/controller/PaymentController.java`
at line 31, The PaymentController currently injects and uses UserRepository
(field userRepository), which violates layering; move user-lookup logic out of
the controller by delegating to a dedicated component such as a UserService or
AuthUserResolver: create or reuse a service (e.g., UserService or
AuthUserResolver with a method resolveUserId(User user) or similar) that
encapsulates the repository call and exception handling, inject that service
into PaymentController in place of UserRepository, and update BookingController
and any other controllers using the same pattern to call the shared
resolver/service instead of referencing UserRepository directly; ensure
PaymentService handles business rules and controllers only orchestrate requests.

Comment on lines +72 to 74
api:
service-key: ${BIZ_API_KEY}
jwt:
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

apijwt 섹션 사이에 빈 줄 누락.

다른 최상위 섹션(payment, cloud 등) 사이에는 빈 줄이 있지만, api(Line 72)와 jwt(Line 74) 사이에는 빈 줄이 없어 일관성이 떨어집니다.

제안
 api:
   service-key: ${BIZ_API_KEY}
+
 jwt:
   secret: ${SECRET_KEY}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
api:
service-key: ${BIZ_API_KEY}
jwt:
api:
service-key: ${BIZ_API_KEY}
jwt:
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/resources/application-local.yml` around lines 72 - 74, The YAML is
missing a blank line between the top-level sections 'api' and 'jwt', causing
inconsistent spacing with other sections like 'payment' and 'cloud'; insert a
single empty line between the 'api' block (service-key: ${BIZ_API_KEY}) and the
'jwt' key in application-local.yml so all top-level sections have the same
blank-line separation for consistency.

Comment on lines 53 to 66
@MockBean
private PaymentService paymentService;

@MockBean
private JwtAuthenticationFilter jwtAuthenticationFilter;

@MockBean
private CustomAuthenticationEntryPoint customAuthenticationEntryPoint;

@MockBean
private CustomAccessDeniedHandler customAccessDeniedHandler;

@MockBean
private UserRepository userRepository;
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check for Spring Boot version in build files
echo "=== Checking for Spring Boot version in gradle build files ==="
find . -type f -name "build.gradle*" | head -5 | xargs cat 2>/dev/null | grep -i "spring.boot\|springBootVersion\|org.springframework.boot" | head -10

echo ""
echo "=== Checking for Spring Boot version in Maven pom files ==="
find . -type f -name "pom.xml" | head -5 | xargs cat 2>/dev/null | grep -A 2 "spring-boot" | head -15

echo ""
echo "=== Checking usage of `@MockBean` in test files ==="
rg '@MockBean' --type java -g '*Test*.java' -c | head -10

echo ""
echo "=== Checking usage of `@MockitoBean` in test files ==="
rg '@MockitoBean' --type java -g '*Test*.java' -c | head -10

Repository: Eatsfine/BE

Length of output: 1193


🌐 Web query:

Spring Boot 3.4 @MockBeandeprecated@MockitoBean replacement

💡 Result:

In Spring Boot 3.4, @MockBean / @MockBeans are deprecated. Replace them with Spring Framework’s @MockitoBean (and @SpyBean with @MockitoSpyBean). (github.com)

1) Field-level mock (most common)

Before

import org.springframework.boot.test.mock.mockito.MockBean;

`@MockBean`
MyService myService;

After

import org.springframework.test.context.bean.override.mockito.MockitoBean;

`@MockitoBean`
MyService myService;

(docs.spring.io)

2) Type-level / multiple mocks (replacement for @MockBean({ ... }) and many @MockBeans)

Use types = { ... } (or repeat @MockitoBean since it’s @Repeatable):

import org.springframework.test.context.bean.override.mockito.MockitoBean;

`@MockitoBean`(types = { Foo.class, Bar.class })
`@SpringBootTest`
class MyTest { }

(docs.spring.io)

Important migration note

Spring Boot calls out that Spring Framework’s replacements are not identical to Boot’s old behavior (e.g., @MockitoBean is not supported on @Configuration classes), so you may need to move mocks onto test-class fields (or a shared base test class). (github.com)

Citations:


@MockBean은 Spring Boot 3.4+에서 deprecated되었으므로 @MockitoBean으로 마이그레이션이 필요합니다.

프로젝트가 Spring Boot 3.4.1을 사용 중이며, @MockBean은 공식 문서에서 deprecated 처리되었습니다. 해당 파일의 다섯 개 MockBean 선언을 다음과 같이 마이그레이션하세요:

  • import 변경: org.springframework.boot.test.mock.mockito.MockBeanorg.springframework.test.context.bean.override.mockito.MockitoBean
  • 어노테이션 변경: @MockBean@MockitoBean

마찬가지로 프로젝트의 다른 테스트 파일(HealthControllerTest, InquiryControllerTest)도 동일하게 업데이트하세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/test/java/com/eatsfine/eatsfine/domain/payment/controller/PaymentControllerTest.java`
around lines 53 - 66, The tests use deprecated `@MockBean`; update the five mock
declarations in PaymentControllerTest (fields paymentService,
jwtAuthenticationFilter, customAuthenticationEntryPoint,
customAccessDeniedHandler, userRepository) to use `@MockitoBean` instead and
change the import from org.springframework.boot.test.mock.mockito.MockBean to
org.springframework.test.context.bean.override.mockito.MockitoBean; apply the
same import+annotation replacement in other test classes named (e.g.,
HealthControllerTest, InquiryControllerTest) so all Mockito-based test beans use
`@MockitoBean`.

Comment on lines 153 to 200
@Test
@DisplayName("결제 내역 조회 성공")
void getPaymentList_success() throws Exception {
// given
PaymentResponseDTO.PaginationDTO pagination = new PaymentResponseDTO.PaginationDTO(1, 1, 1L);
PaymentResponseDTO.PaymentHistoryResultDTO history = new PaymentResponseDTO.PaymentHistoryResultDTO(
1L, 1L, "Store Name", BigDecimal.valueOf(10000), "DEPOSIT", "CARD", "TOSS", "COMPLETED",
LocalDateTime.now());
PaymentResponseDTO.PaymentListResponseDTO response = new PaymentResponseDTO.PaymentListResponseDTO(
Collections.singletonList(history), pagination);

User user = User.builder().id(1L).email("user").role(Role.ROLE_CUSTOMER).build();
given(userRepository.findByEmail("user")).willReturn(Optional.of(user));

given(paymentService.getPaymentList(eq(1L), any(Integer.class), any(Integer.class), any()))
.willReturn(response);

// when & then
mockMvc.perform(get("/api/v1/payments")
.param("page", "1")
.param("limit", "10")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.isSuccess").value(true))
.andExpect(jsonPath("$.result.payments[0].storeName").value("Store Name"));
}

@Test
@DisplayName("결제 상세 조회 성공")
void getPaymentDetail_success() throws Exception {
// given
Long paymentId = 1L;
PaymentResponseDTO.PaymentDetailResultDTO response = new PaymentResponseDTO.PaymentDetailResultDTO(
paymentId, 1L, "Store Name", "CARD", "TOSS", BigDecimal.valueOf(10000), "DEPOSIT",
"COMPLETED", LocalDateTime.now(), LocalDateTime.now(), "http://receipt.url", null);

User user = User.builder().id(1L).email("user").role(Role.ROLE_CUSTOMER).build();
given(userRepository.findByEmail("user")).willReturn(Optional.of(user));

given(paymentService.getPaymentDetail(eq(paymentId), eq(1L))).willReturn(response);

// when & then
mockMvc.perform(get("/api/v1/payments/{paymentId}", paymentId)
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.isSuccess").value(true))
.andExpect(jsonPath("$.result.paymentId").value(paymentId));
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

인증 실패 및 에러 케이스 테스트가 누락되었습니다.

getPaymentListgetPaymentDetail의 성공 케이스만 테스트되어 있습니다. 보안 강화가 PR의 핵심 목표인 만큼, 다음 케이스를 추가해 주세요:

  1. 인증되지 않은 요청: @WithMockUser 없이 호출 시 401/403 응답 확인
  2. 사용자 미조회: userRepository.findByEmail()Optional.empty()를 반환할 때 예외 처리 확인
  3. 다른 사용자의 결제 상세 접근: paymentService.getPaymentDetail()에서 PaymentException(_PAYMENT_ACCESS_DENIED) 발생 시 적절한 에러 응답 확인

As per coding guidelines, "경계값 테스트, 예외 케이스 테스트가 포함되어 있는지" 확인 필요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/test/java/com/eatsfine/eatsfine/domain/payment/controller/PaymentControllerTest.java`
around lines 153 - 200, Tests only cover success paths for getPaymentList and
getPaymentDetail; add negative/auth and error-case unit tests: (1) an
unauthenticated request (call the same endpoints without `@WithMockUser`)
asserting 401/403; (2) a "user not found" case by stubbing
userRepository.findByEmail(...) to return Optional.empty() and asserting the
controller returns the expected error response/status; (3) an access-denied case
by stubbing paymentService.getPaymentDetail(paymentId, userId) to throw the
PaymentException with code _PAYMENT_ACCESS_DENIED and asserting the controller
maps it to the correct error status/body; reference the existing test methods
getPaymentList_success and getPaymentDetail_success, the
userRepository.findByEmail, paymentService.getPaymentDetail, and
PaymentException/_PAYMENT_ACCESS_DENIED when adding these new test methods.

Comment on lines 39 to 217
@ExtendWith(MockitoExtension.class)
class PaymentServiceTest {

@InjectMocks
private PaymentService paymentService;

@Mock
private PaymentRepository paymentRepository;

@Mock
private BookingRepository bookingRepository;

@Mock
private TossPaymentService tossPaymentService;

@Test
@DisplayName("결제 요청 성공")
void requestPayment_success() {
// given
Long bookingId = 1L;
PaymentRequestDTO.RequestPaymentDTO request = new PaymentRequestDTO.RequestPaymentDTO(bookingId);

Booking booking = Booking.builder()
.id(bookingId)
.build();
ReflectionTestUtils.setField(booking, "depositAmount", BigDecimal.valueOf(10000));

Payment payment = Payment.builder()
.id(1L)
.booking(booking)
.amount(BigDecimal.valueOf(10000))
.orderId("generated-order-id")
.paymentStatus(PaymentStatus.PENDING)
.requestedAt(LocalDateTime.now())
.build();

given(bookingRepository.findById(bookingId)).willReturn(Optional.of(booking));
given(paymentRepository.save(any(Payment.class))).willReturn(payment);

// when
PaymentResponseDTO.PaymentRequestResultDTO response = paymentService.requestPayment(request);

// then
assertThat(response.amount()).isEqualTo(BigDecimal.valueOf(10000));
assertThat(response.orderId()).isEqualTo("generated-order-id");
verify(paymentRepository, times(1)).save(any(Payment.class));
}

@Test
@DisplayName("결제 요청 실패 - 예약 없음")
void requestPayment_fail_bookingNotFound() {
// given
PaymentRequestDTO.RequestPaymentDTO request = new PaymentRequestDTO.RequestPaymentDTO(999L);
given(bookingRepository.findById(999L)).willReturn(Optional.empty());

// when & then
assertThatThrownBy(() -> paymentService.requestPayment(request))
.isInstanceOf(PaymentException.class)
.extracting("code")
.isEqualTo(PaymentErrorStatus._BOOKING_NOT_FOUND);
}

@Test
@DisplayName("결제 승인 성공")
void confirmPayment_success() {
// given
String orderId = "order-id-123";
String paymentKey = "payment-key-123";
BigDecimal amount = BigDecimal.valueOf(10000);
PaymentConfirmDTO request = PaymentConfirmDTO.builder()
.orderId(orderId)
.amount(amount)
.paymentKey(paymentKey)
.build();

Booking booking = Booking.builder().id(1L).build();
Payment payment = Payment.builder()
.id(1L)
.booking(booking)
.orderId(orderId)
.amount(amount)
.paymentStatus(PaymentStatus.PENDING)
.build();

TossPaymentResponse.EasyPay easyPay = new TossPaymentResponse.EasyPay("토스페이", 10000, 0);
TossPaymentResponse tossResponse = new TossPaymentResponse(
paymentKey, "NORMAL", orderId, "orderName", "mId", "KRW", "CARD",
10000, 10000, "DONE",
java.time.OffsetDateTime.now(), java.time.OffsetDateTime.now(),
false, null, 10000, 0,
easyPay, new TossPaymentResponse.Receipt("http://receipt.url"));// TossPaymentResponse
// record 생성자가 많아서
// 필드에 맞게 넣어줌 (가정)
// 실제 record 구조에 따라 맞춰야
// 함. 위 내용은 예시.
// TossPaymentResponse가
// record이므로 생성자
// 파라미터 순서 중요.
// 여기서는 Mocking을 하거나,
// 필드가 많으면 빌더나 생성자를
// 확인해야 함.
// TossPaymentService가
// Mock이므로 response
// 리턴값만 잘 맞춰주면 됨.

given(paymentRepository.findByOrderId(orderId)).willReturn(Optional.of(payment));
given(tossPaymentService.confirm(any(PaymentConfirmDTO.class))).willReturn(tossResponse);

// when
PaymentResponseDTO.PaymentSuccessResultDTO response = paymentService.confirmPayment(request);

// then
assertThat(response.status()).isEqualTo(PaymentStatus.COMPLETED.name());
assertThat(payment.getPaymentStatus()).isEqualTo(PaymentStatus.COMPLETED);
}

@Test
@DisplayName("결제 승인 실패 - 금액 불일치")
void confirmPayment_fail_invalidAmount() {
// given
String orderId = "order-id-123";
BigDecimal originalAmount = BigDecimal.valueOf(10000);
BigDecimal requestAmount = BigDecimal.valueOf(5000); // Mismatch

PaymentConfirmDTO request = PaymentConfirmDTO.builder()
.orderId(orderId)
.amount(requestAmount)
.paymentKey("key")
.build();

Payment payment = Payment.builder()
.id(1L)
.orderId(orderId)
.amount(originalAmount)
.paymentStatus(PaymentStatus.PENDING)
.build();

given(paymentRepository.findByOrderId(orderId)).willReturn(Optional.of(payment));

// when & then
assertThatThrownBy(() -> paymentService.confirmPayment(request))
.isInstanceOf(PaymentException.class)
.extracting("code")
.isEqualTo(PaymentErrorStatus._PAYMENT_INVALID_AMOUNT);

assertThat(payment.getPaymentStatus()).isEqualTo(PaymentStatus.FAILED);
}

@Test
@DisplayName("결제 취소 성공")
void cancelPayment_success() {
// given
String paymentKey = "payment-key-123";
PaymentRequestDTO.CancelPaymentDTO request = new PaymentRequestDTO.CancelPaymentDTO("단순 변심");

Payment payment = Payment.builder()
.id(1L)
.paymentKey(paymentKey)
.orderId("order-id-123")
.paymentStatus(PaymentStatus.COMPLETED)
.build();

TossPaymentResponse tossResponse = new TossPaymentResponse(
paymentKey, "NORMAL", "order-id-123", "orderName", "mId", "KRW", "CARD",
10000, 0, "CANCELED",
java.time.OffsetDateTime.now(), java.time.OffsetDateTime.now(),
false, null, 10000, 0,
null, null);
given(paymentRepository.findByPaymentKey(paymentKey)).willReturn(Optional.of(payment));
given(tossPaymentService.cancel(eq(paymentKey), any(PaymentRequestDTO.CancelPaymentDTO.class)))
.willReturn(tossResponse);

// when
PaymentResponseDTO.CancelPaymentResultDTO response = paymentService.cancelPayment(paymentKey, request);

// then
assertThat(response.status()).isEqualTo("REFUNDED");
assertThat(payment.getPaymentStatus()).isEqualTo(PaymentStatus.REFUNDED);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

getPaymentDetail의 소유권 검증 테스트가 누락되었습니다.

이 PR의 핵심 보안 변경사항인 getPaymentDetail의 소유권 검증(_PAYMENT_ACCESS_DENIED) 및 정상 조회 케이스에 대한 테스트가 없습니다. getPaymentList에 대한 테스트도 누락되어 있습니다. 최소한 다음 케이스를 추가해 주세요:

  1. getPaymentDetail — 본인의 결제 조회 성공
  2. getPaymentDetail — 다른 사용자의 결제 접근 시 _PAYMENT_ACCESS_DENIED 예외 발생
  3. getPaymentList — 정상 조회

As per coding guidelines, "단위 테스트가 충분히 작성되었는지, 핵심 로직의 검증이 누락되지 않았는지" 및 "경계값 테스트, 예외 케이스 테스트가 포함되어 있는지" 확인 필요.

해당 테스트 코드를 생성해 드릴까요?

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/test/java/com/eatsfine/eatsfine/domain/payment/service/PaymentServiceTest.java`
around lines 39 - 217, Add unit tests in PaymentServiceTest covering ownership
checks and list retrieval: (1) a success case for
PaymentService.getPaymentDetail where the requesting user's id matches the
payment's owner and the method returns expected DTO; (2) an access-denied case
for getPaymentDetail where paymentRepository.findById(...) returns a Payment
owned by a different user and assert that calling
paymentService.getPaymentDetail(...) throws PaymentException with code
PaymentErrorStatus._PAYMENT_ACCESS_DENIED and that the payment status remains
unchanged; and (3) a normal getPaymentList case that stubs
paymentRepository.findByBookingMemberId(...) or the repository method used by
PaymentService.getPaymentList to return a page/list of payments and assert the
returned list/DTO content matches expectations; use the existing test patterns
(given(...).willReturn(...), assertThat(...), assertThatThrownBy(...),
verify(...)) and the same helper symbols PaymentService, PaymentRepository,
PaymentResponseDTO, PaymentErrorStatus._PAYMENT_ACCESS_DENIED, and
PaymentServiceTest to locate where to add these tests.

Comment on lines +129 to +142
easyPay, new TossPaymentResponse.Receipt("http://receipt.url"));// TossPaymentResponse
// record 생성자가 많아서
// 필드에 맞게 넣어줌 (가정)
// 실제 record 구조에 따라 맞춰야
// 함. 위 내용은 예시.
// TossPaymentResponse가
// record이므로 생성자
// 파라미터 순서 중요.
// 여기서는 Mocking을 하거나,
// 필드가 많으면 빌더나 생성자를
// 확인해야 함.
// TossPaymentService가
// Mock이므로 response
// 리턴값만 잘 맞춰주면 됨.
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

불필요한 인라인 주석을 정리해 주세요.

TossPaymentResponse 생성자 호출 뒤에 붙은 긴 한국어 주석(Lines 129-142)은 코드 가독성을 저해합니다. 테스트 코드에서 생성자 파라미터가 많다면, 헬퍼 메서드나 테스트 빌더를 추출하는 것이 더 효과적입니다.

♻️ 헬퍼 메서드 추출 제안
+        private TossPaymentResponse createSuccessTossResponse(String paymentKey, String orderId) {
+                TossPaymentResponse.EasyPay easyPay = new TossPaymentResponse.EasyPay("토스페이", 10000, 0);
+                return new TossPaymentResponse(
+                                paymentKey, "NORMAL", orderId, "orderName", "mId", "KRW", "CARD",
+                                10000, 10000, "DONE",
+                                java.time.OffsetDateTime.now(), java.time.OffsetDateTime.now(),
+                                false, null, 10000, 0,
+                                easyPay, new TossPaymentResponse.Receipt("http://receipt.url"));
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/test/java/com/eatsfine/eatsfine/domain/payment/service/PaymentServiceTest.java`
around lines 129 - 142, Remove the long inline Korean comment after the
TossPaymentResponse constructor call in PaymentServiceTest and replace it by
extracting a helper/builder method (e.g., createTossPaymentResponse(...) or a
TossPaymentResponseTestBuilder) to construct the test response; update the test
to call that helper (referencing TossPaymentResponse and PaymentServiceTest) so
the test body stays concise and all construction details live in the new helper
method.

- Service 계층으로 User 조회 로직 이관 및 Controller 의존성 제거
- PaymentService 단위 테스트(성공/실패/예외 케이스) 추가
- 결제 상세 조회 시, 예약자뿐만 아니라 상점 주인도 접근 가능하도록 권한 로직 수정
- Spring Boot 3.4 마이그레이션: @MockBean -> @MockitoBean 교체 (Deprecated API 대응)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant