Skip to content

Conversation

@jhan0121
Copy link
Collaborator

🚀 작업 내용

  • ReviewService에 커스텀 복습 주기 소유권 검증 로직을 추가
  • cycleCustomCycleSelection인 경우에만 주기 소유권 검증 수행
  • 커스텀 주기 조회를 위해 CycleOptionRepository 의존성 추가
  • 커스텀 주기가 존재하지 않거나 요청자 소유가 아니면 NotFoundException 반환
  • 검증 통과 시 기존 복습 스케줄 계산/저장 흐름 유지

📸 이슈 번호

✍ 궁금한 점

  • X

@jhan0121 jhan0121 added this to the 1.1.* milestone Feb 13, 2026
@jhan0121 jhan0121 requested a review from Copilot February 13, 2026 07:37
@jhan0121 jhan0121 self-assigned this Feb 13, 2026
@jhan0121 jhan0121 added the 🛠 fix 버그 수정 label Feb 13, 2026
@github-actions
Copy link

🧪 테스트 커버리지 리포트

Overall Project 96.56% 🍏
Files changed 100% 🍏

File Coverage
ReviewService.java 100% 🍏

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR addresses a security vulnerability where users could reference custom review cycles they don't own when creating a review. The fix adds ownership validation in ReviewService to ensure only the cycle owner can use their custom cycles.

Changes:

  • Added ownership validation logic to ReviewService.saveReview() for custom cycle selections
  • Added CycleOptionRepository dependency to enable cycle lookup and validation
  • Added comprehensive test coverage for ownership validation scenarios (owner, not found, not owner)

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 7 comments.

File Description
src/main/java/com/recyclestudy/review/service/ReviewService.java Added validateCycleSelectionOwnership method and CycleOptionRepository dependency to validate custom cycle ownership before processing reviews
src/test/java/com/recyclestudy/review/service/ReviewServiceTest.java Added three test cases covering successful custom cycle usage, non-existent cycle handling, and unauthorized access attempts

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +226 to +228
given(memberRepository.findByIdentifier(identifier)).willReturn(Optional.of(member));
given(reviewRepository.save(any(Review.class))).willReturn(review);
given(cycleOptionRepository.findById(cycleOptionId)).willReturn(Optional.empty());
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

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

Test demonstrates the bug: This test mocks reviewRepository.save to succeed (line 227) before the validation failure occurs. Because the actual implementation saves the review before validating ownership (lines 50-54 in ReviewService), this test will leave an orphaned Review in the database when run against a real database. Once the bug in ReviewService is fixed to validate before saving, this mock setup should be removed since reviewRepository.save should never be called when validation fails.

Copilot uses AI. Check for mistakes.
Comment on lines +249 to +251
given(memberRepository.findByIdentifier(identifier)).willReturn(Optional.of(member));
given(reviewRepository.save(any(Review.class))).willReturn(review);
given(cycleOptionRepository.findById(cycleOptionId)).willReturn(Optional.of(cycleOption));
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

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

Test demonstrates the bug: Similar to the notFoundCycleOption test, this test mocks reviewRepository.save to succeed (line 250) before the validation failure. Once the bug in ReviewService is fixed to validate ownership before saving the review, this mock setup should be removed since reviewRepository.save should never be called when ownership validation fails.

Copilot uses AI. Check for mistakes.
Comment on lines 51 to 56
final Review savedReview = reviewRepository.save(review);
log.info("[REVIEW_SAVED] 복습 주제 저장 성공: reviewId={}", savedReview.getId());

validateCycleSelectionOwnership(input.cycle(), member);
final List<LocalDateTime> scheduledAts = calculateScheduledAts(input.cycle(), member);

Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

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

Critical ordering issue: The review is saved to the database (line 51) before ownership validation occurs (line 54). If the validation fails, an orphaned Review entity will remain in the database without any associated ReviewCycles or NotificationHistory. The validation should be performed before persisting the Review entity to prevent data inconsistency.

Suggested change
final Review savedReview = reviewRepository.save(review);
log.info("[REVIEW_SAVED] 복습 주제 저장 성공: reviewId={}", savedReview.getId());
validateCycleSelectionOwnership(input.cycle(), member);
final List<LocalDateTime> scheduledAts = calculateScheduledAts(input.cycle(), member);
validateCycleSelectionOwnership(input.cycle(), member);
final List<LocalDateTime> scheduledAts = calculateScheduledAts(input.cycle(), member);
final Review savedReview = reviewRepository.save(review);
log.info("[REVIEW_SAVED] 복습 주제 저장 성공: reviewId={}", savedReview.getId());

Copilot uses AI. Check for mistakes.
Comment on lines +114 to +117
.orElseThrow(() -> new NotFoundException("존재하지 않는 복습 주기입니다"));

if (!cycleOption.isOwner(member)) {
throw new NotFoundException("존재하지 않는 복습 주기입니다");
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

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

Error message inconsistency: The error message "존재하지 않는 복습 주기입니다" differs from the pattern used elsewhere in the codebase. In CycleOptionService (lines 66, 81, 97 in cycle/service/CycleOptionService.java) and CustomCycleSelectionResolver (line 27 in cycle/service/resolver/CustomCycleSelectionResolver.java), the message is "존재하지 않는 주기 옵션입니다." with a period. For consistency with the existing codebase patterns, use "존재하지 않는 주기 옵션입니다." instead.

Suggested change
.orElseThrow(() -> new NotFoundException("존재하지 않는 복습 주기입니다"));
if (!cycleOption.isOwner(member)) {
throw new NotFoundException("존재하지 않는 복습 주기입니다");
.orElseThrow(() -> new NotFoundException("존재하지 않는 주기 옵션입니다."));
if (!cycleOption.isOwner(member)) {
throw new NotFoundException("존재하지 않는 주기 옵션입니다.");

Copilot uses AI. Check for mistakes.
.orElseThrow(() -> new NotFoundException("존재하지 않는 복습 주기입니다"));

if (!cycleOption.isOwner(member)) {
throw new NotFoundException("존재하지 않는 복습 주기입니다");
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

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

Error message inconsistency: Same as above - use "존재하지 않는 주기 옵션입니다." (with period) to match the pattern in CycleOptionService.validateOwnership (line 97 in cycle/service/CycleOptionService.java) which throws NotFoundException with this exact message when ownership validation fails.

Suggested change
throw new NotFoundException("존재하지 않는 복습 주기입니다");
throw new NotFoundException("존재하지 않는 주기 옵션입니다.");

Copilot uses AI. Check for mistakes.
@jhan0121 jhan0121 merged commit af0e9ec into be/dev Feb 13, 2026
7 checks passed
@jhan0121 jhan0121 deleted the fix/review-cycle-validation branch February 13, 2026 07:46
@github-project-automation github-project-automation bot moved this from Backlog to Done in recycle-study Feb 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🛠 fix 버그 수정

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant