Skip to content

[OT-350][TEST]: admin & user 테스트 코드 작성#209

Open
yubin012 wants to merge 2 commits intodevelopfrom
OT-350-test/unit-tests-playlist-preference
Open

[OT-350][TEST]: admin & user 테스트 코드 작성#209
yubin012 wants to merge 2 commits intodevelopfrom
OT-350-test/unit-tests-playlist-preference

Conversation

@yubin012
Copy link
Contributor

@yubin012 yubin012 commented Mar 18, 2026

📝 작업 내용

이번 PR에서 작업한 내용을 적어주세요

  • api-userapi-admin 모듈의 주요 서비스와 컨트롤러에 대한 단위 테스트를 구축

  • 테스트 커버리지 측정을 위한 JaCoCo 설정 완료

  • api-user: MoodRefresh, Playlist, ShortForm, WatchHistory 등 핵심 서비스의 단위 테스트를 Mockito를 활용해 작성. (커버리지 90% 이상 확보)

  • api-admin: UploadHelper, MediaTagLinker, ClickEvent 등 관리자 핵심 기능에 대한 검증 완료.

  • api-user: 39 tests completed, 0 failed

  • api-admin: 13 tests completed, 0 failed

일단은 DB·인프라 없이 단위로 돌릴 수 있는 코드들에 대해 순수하게 목(Mock) 데이터로만 단위 테스트

📷 스크린샷

☑️ 체크 리스트

체크 리스트를 확인해주세요

  • 테스트는 잘 통과했나요?
  • 충돌을 해결했나요?
  • 이슈는 등록했나요?
  • 라벨은 등록했나요?

#️⃣ 연관된 이슈

ex) # 이슈번호

💬 리뷰 요구사항

리뷰어가 특별히 봐주었으면 하는 부분이 있다면 작성해주세요

ex) 예외 처리를 이렇게 해도 괜찮을까요? / ~~부분 주의 깊게 봐주세요

Summary by CodeRabbit

릴리스 노트

  • 테스트 인프라 개선

    • 광범위한 단위 테스트 추가로 서비스 안정성 강화
    • JaCoCo를 통한 테스트 커버리지(HTML/XML) 리포팅 도입
    • 통합 컨텍스트 의존 테스트는 실행에서 제외하여 단위 중심 실행 지원
  • 버그 수정

    • 컬렉션 가변성 처리 개선으로 불변/가변 리스트 관련 문제 완화
    • 일부 공백·형식 조정으로 코드 정리
  • 로깅 개선

    • 진단용 로그 메시지 향상으로 모니터링 정보 강화

@coderabbitai
Copy link

coderabbitai bot commented Mar 18, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 1988dc3a-1396-4219-b8a4-7c46f1d0ae04

📥 Commits

Reviewing files that changed from the base of the PR and between 9e0ad0c and 870d494.

📒 Files selected for processing (2)
  • apps/api-admin/build.gradle
  • apps/api-user/src/main/java/com/ott/api_user/shortform/service/ClickEventService.java
💤 Files with no reviewable changes (1)
  • apps/api-user/src/main/java/com/ott/api_user/shortform/service/ClickEventService.java
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/api-admin/build.gradle

Walkthrough

앱 모듈 api-adminapi-user에 JaCoCo 코드커버리지 설정이 추가되고, 다수의 단위 테스트 클래스가 새로 도입되었으며 일부 통합 테스트는 비활성화되고 소수의 로깅·리스트·테스트 스텁 관련 소소한 코드 개선이 적용되었습니다.

Changes

Cohort / File(s) Summary
Build Configuration - JaCoCo
apps/api-admin/build.gradle, apps/api-user/build.gradle
jacoco 플러그인 적용·toolVersion 설정(0.8.10), testuseJUnitPlatform()·ignoreFailures=true, jacocoTestReport 태스크 추가(HTML/XML 보고서, 소스/클래스 디렉토리/실행 데이터 지정).
Integration Tests Disabled
apps/api-admin/src/test/java/com/ott/api_admin/ApiAdminApplicationTests.java, apps/api-user/src/test/java/com/ott/api_user/ApiUserApplicationTests.java
클래스 레벨 @Disabled("Integration context requires DB/Flight; skip during unit-focused runs") 추가로 통합 컨텍스트 테스트 건너뜀.
Admin API Unit Tests
apps/api-admin/src/test/java/com/ott/api_admin/click_event/.../BackOfficeClickEventServiceTest.java, apps/api-admin/src/test/java/com/ott/api_admin/content/.../BackOfficeContentsServiceTest.java, apps/api-admin/src/test/java/com/ott/api_admin/shortform/.../BackOfficeShortForm*.java, apps/api-admin/src/test/java/com/ott/api_admin/upload/support/*.java
BackOffice 영역의 서비스/지원 로직(전환율 계산, 업로드 완료, 숏폼 업로드/리더, 태그 링크 등)에 대한 Mockito 기반 단위 테스트 추가.
User API Unit Tests - 서비스/도메인
apps/api-user/src/test/java/com/ott/api_user/event/.../MoodRefreshEventListenerTest.java, .../history/.../WatchHistoryServiceTest.java, .../moodrefresh/.../MoodRefreshServiceTest.java, .../playback/.../PlaybackServiceTest.java, .../playlist/.../PlaylistPreferenceServiceTest.java, .../playlist/.../PlaylistServiceTest.java, .../playlist/.../PlaylistStrategyServiceTest.java, .../series/.../SeriesServiceTest.java, .../shortform/.../ClickEventServiceTest.java, .../shortform/.../ShortFormFeedServiceTest.java
시청기록·무드리프레시·재생·플레이리스트·시리즈·숏폼 관련 서비스들에 대한 광범위한 단위 테스트 추가.
User API Unit Tests - 컨트롤러
apps/api-user/src/test/java/com/ott/api_user/playlist/controller/PlaylistControllerTest.java, apps/api-user/src/test/java/com/ott/api_user/shortform/controller/ShortFormControllerTest.java
MockMvc 기반 컨트롤러 엔드포인트 테스트 추가(인증 파라미터 리졸버 포함).
Minor Code Adjustments
apps/api-user/src/main/java/com/ott/api_user/moodrefresh/service/MoodRefreshService.java, apps/api-user/src/main/java/com/ott/api_user/playlist/service/PlaylistPreferenceService.java, apps/api-user/src/main/java/com/ott/api_user/shortform/service/ClickEventService.java, apps/api-user/src/test/java/com/ott/api_user/playlist/service/PlaylistPreferenceServiceTest.java
로깅 메시지 개선(미디어 ID 포함), 레포지토리 반환을 new ArrayList<>(...)로 감싸 가변 리스트 보장, 소소한 공백/파라미터 네이밍 정리 및 테스트에서 lenient() 스텁 도입.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers

  • marulog
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 32.50% 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 Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목은 '[OT-350][TEST]: admin & user 테스트 코드 작성'으로 변경 사항의 핵심을 정확히 요약합니다. 제목은 단순하고 명확하며, 두 모듈(admin & user)에 테스트 코드가 추가되었다는 주요 목표를 직접적으로 전달합니다.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch OT-350-test/unit-tests-playlist-preference
📝 Coding Plan
  • Generate coding plan for human review comments

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.

@yubin012 yubin012 changed the title [TEST: admin & user 테스트 코드 작성 [OT-350][TEST]: admin & user 테스트 코드 작성 Mar 18, 2026
@yubin012 yubin012 self-assigned this Mar 18, 2026
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: 8

🧹 Nitpick comments (22)
apps/api-user/src/main/java/com/ott/api_user/playlist/service/PlaylistPreferenceService.java (2)

84-85: 정렬을 위한 가변 리스트 래핑 - LGTM!

findAllById()가 반환하는 리스트가 불변일 수 있으므로 ArrayList로 래핑하여 sort() 호출을 안전하게 처리한 점이 좋습니다.

동일하게 변수명 tagstagList로 변경하면 프로젝트 네이밍 컨벤션과 일관성을 유지할 수 있습니다.

♻️ 변수명 변경 제안
-        List<Tag> tags = new ArrayList<>(tagRepository.findAllById(topTagIds));
-        tags.sort(Comparator.comparing(tag -> topTagIds.indexOf(tag.getId())));
+        List<Tag> tagList = new ArrayList<>(tagRepository.findAllById(topTagIds));
+        tagList.sort(Comparator.comparing(tag -> topTagIds.indexOf(tag.getId())));

-        return tags;
+        return tagList;

As per coding guidelines, "Collection variable names with List suffix".

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

In
`@apps/api-user/src/main/java/com/ott/api_user/playlist/service/PlaylistPreferenceService.java`
around lines 84 - 85, The local variable name tags should be renamed to tagList
to follow the project's naming convention; update the declaration using new
ArrayList<>(tagRepository.findAllById(topTagIds)) to assign to tagList and
update the subsequent sort call (tags.sort(...)) to
tagList.sort(Comparator.comparing(tag -> topTagIds.indexOf(tag.getId()))), and
ensure any other references in PlaylistPreferenceService (e.g., later uses of
tags) are updated to tagList.

77-80: 변경 가능한 리스트로 래핑한 것은 좋은 접근입니다.

JPA 리포지토리가 불변 컬렉션을 반환할 수 있어 Collections.shuffle() 호출 시 UnsupportedOperationException이 발생할 수 있는데, ArrayList로 래핑하여 이를 방지한 점이 좋습니다.

다만, 코딩 가이드라인에 따라 컬렉션 변수명에 List 접미사를 사용해야 합니다. allTagsallTagList로 변경을 권장합니다.

♻️ 변수명 변경 제안
-            List<Tag> allTags = new ArrayList<>(tagRepository.findAll());
-            Collections.shuffle(allTags);
-            return allTags.stream().limit(3).collect(Collectors.toList());
+            List<Tag> allTagList = new ArrayList<>(tagRepository.findAll());
+            Collections.shuffle(allTagList);
+            return allTagList.stream().limit(3).collect(Collectors.toList());

As per coding guidelines, "Collection variable names with List suffix".

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

In
`@apps/api-user/src/main/java/com/ott/api_user/playlist/service/PlaylistPreferenceService.java`
around lines 77 - 80, The local collection variable allTags in
PlaylistPreferenceService is correctly wrapped with new
ArrayList<>(tagRepository.findAll()) to avoid UnsupportedOperationException, but
it violates the naming guideline; rename the variable to allTagList (update its
declaration and all usages in the method) so the list variable follows the
"List" suffix convention while leaving the logic using tagRepository and
Collections.shuffle intact.
apps/api-user/src/test/java/com/ott/api_user/shortform/service/ClickEventServiceTest.java (1)

67-84: 예외 케이스에서 저장 부수효과도 함께 잠가두는 걸 권장합니다.

현재는 예외 코드만 확인하므로, clickRepository.save(...) 미호출까지 검증하면 회귀 방지에 더 좋습니다.

테스트 보강 예시
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
@@
     void saveClickEvent_throwsWhenMemberMissing() {
         when(memberRepository.findById(5L)).thenReturn(Optional.empty());

         assertThatThrownBy(() -> clickEventService.saveClickEvent(5L, 1L, ClickType.SHORT_CLICK))
                 .isInstanceOf(BusinessException.class)
                 .hasFieldOrPropertyWithValue("errorCode", ErrorCode.USER_NOT_FOUND);
+        verifyNoInteractions(clickRepository);
     }
@@
     void saveClickEvent_throwsWhenShortFormMissing() {
         Member member = Member.builder().id(6L).email("u@ott").nickname("u").provider(Provider.KAKAO).role(Role.MEMBER).build();
         when(memberRepository.findById(6L)).thenReturn(Optional.of(member));
         when(shortFormRepository.findById(99L)).thenReturn(Optional.empty());

         assertThatThrownBy(() -> clickEventService.saveClickEvent(6L, 99L, ClickType.SHORT_CLICK))
                 .isInstanceOf(BusinessException.class)
                 .hasFieldOrPropertyWithValue("errorCode", ErrorCode.SHORT_FORM_NOT_FOUND);
+        verifyNoInteractions(clickRepository);
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/api-user/src/test/java/com/ott/api_user/shortform/service/ClickEventServiceTest.java`
around lines 67 - 84, The tests saveClickEvent_throwsWhenMemberMissing and
saveClickEvent_throwsWhenShortFormMissing currently only assert the
BusinessException and ERROR codes; also verify that the
clickRepository.save(...) interaction does not occur to prevent side-effect
regressions. In ClickEventServiceTest, after the assertThatThrownBy in both
methods, add a Mockito verifyZeroInteractions or verify(clickRepository,
never()).save(...) (or equivalent) against the mocked clickRepository to ensure
save is not called when member or short form is missing.
apps/api-admin/src/test/java/com/ott/api_admin/upload/support/MediaTagLinkerTest.java (4)

43-88: 카테고리 조회 실패 케이스 테스트 추가 고려

현재 테스트는 중복 태그, 카테고리 불일치, 정상 저장 시나리오를 잘 커버하고 있습니다. 추가로 categoryRepository.findByIdAndStatus가 빈 Optional을 반환하는 경우(존재하지 않는 카테고리)에 대한 테스트도 고려해 보세요.

💡 추가 테스트 케이스 예시
`@Test`
void linkTags_throwsWhenCategoryNotFound() {
    Media media = Media.builder().id(1L).build();
    when(categoryRepository.findByIdAndStatus(10L, Status.ACTIVE))
            .thenReturn(java.util.Optional.empty());

    assertThatThrownBy(() -> mediaTagLinker.linkTags(media, 10L, List.of(1L)))
            .isInstanceOf(BusinessException.class);
            // 적절한 ErrorCode 검증 추가
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/api-admin/src/test/java/com/ott/api_admin/upload/support/MediaTagLinkerTest.java`
around lines 43 - 88, Add a test that verifies mediaTagLinker.linkTags throws
when categoryRepository.findByIdAndStatus returns Optional.empty: create a Media
(Media.builder().id(1L).build()), stub categoryRepository.findByIdAndStatus(10L,
Status.ACTIVE) to return Optional.empty(), call mediaTagLinker.linkTags(media,
10L, List.of(1L)) and assert that a BusinessException is thrown and (if
applicable) its errorCode equals the expected code (e.g.,
ErrorCode.CATEGORY_NOT_FOUND); reference mediaTagLinker.linkTags and
categoryRepository.findByIdAndStatus when locating where to add the test.

19-20: 사용하지 않는 import 제거

java.util.Set이 import되어 있지만 테스트 코드에서 사용되지 않습니다.

♻️ 수정 제안
 import java.util.List;
-import java.util.Set;
 import org.junit.jupiter.api.Test;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/api-admin/src/test/java/com/ott/api_admin/upload/support/MediaTagLinkerTest.java`
around lines 19 - 20, Remove the unused import by deleting the line importing
java.util.Set in MediaTagLinkerTest; update the imports block (or run automatic
organize/imports) so only required imports like java.util.List remain, ensuring
MediaTagLinkerTest no longer contains the unused Set import.

84-87: 컬렉션 변수명에 List 접미사 사용 필요

코딩 가이드라인에 따라 컬렉션 변수명에는 List 접미사를 사용해야 합니다.

♻️ 수정 제안
-        List<MediaTag> saved = captor.getValue();
-        assertThat(saved).hasSize(1);
-        assertThat(saved.get(0).getTag()).isSameAs(tag);
-        assertThat(saved.get(0).getMedia()).isSameAs(media);
+        List<MediaTag> savedList = captor.getValue();
+        assertThat(savedList).hasSize(1);
+        assertThat(savedList.get(0).getTag()).isSameAs(tag);
+        assertThat(savedList.get(0).getMedia()).isSameAs(media);

As per coding guidelines, "Collection variable names with List suffix".

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

In
`@apps/api-admin/src/test/java/com/ott/api_admin/upload/support/MediaTagLinkerTest.java`
around lines 84 - 87, Rename the local collection variable in MediaTagLinkerTest
from saved to savedList to follow the "List" suffix guideline; update the
assignment (savedList = captor.getValue()), all subsequent uses and assertions
(assertThat(savedList).hasSize(1);
assertThat(savedList.get(0).getTag()).isSameAs(tag);
assertThat(savedList.get(0).getMedia()).isSameAs(media)) to reference savedList,
and ensure any imports or static references remain correct.

82-84: @Captor 어노테이션 사용 권장

ArgumentCaptor.forClass(List.class)는 제네릭 타입 정보를 잃어 unchecked assignment 경고가 발생합니다. @Captor 어노테이션을 사용하면 타입 안전성을 유지할 수 있습니다.

♻️ 수정 제안

필드 선언 추가:

`@Captor`
private ArgumentCaptor<List<MediaTag>> mediaTagCaptor;

테스트 메서드 내 변경:

-        ArgumentCaptor<List<MediaTag>> captor = ArgumentCaptor.forClass(List.class);
-        verify(mediaTagRepository).saveAll(captor.capture());
-        List<MediaTag> saved = captor.getValue();
+        verify(mediaTagRepository).saveAll(mediaTagCaptor.capture());
+        List<MediaTag> savedList = mediaTagCaptor.getValue();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/api-admin/src/test/java/com/ott/api_admin/upload/support/MediaTagLinkerTest.java`
around lines 82 - 84, Replace the raw ArgumentCaptor.forClass usage with a typed
`@Captor` field to avoid unchecked warnings: add a field `@Captor private
ArgumentCaptor<List<MediaTag>> mediaTagCaptor;` to MediaTagLinkerTest and then
use `verify(mediaTagRepository).saveAll(mediaTagCaptor.capture());
List<MediaTag> saved = mediaTagCaptor.getValue();` so the capture targets the
saveAll call in mediaTagRepository with full generic type safety.
apps/api-user/src/test/java/com/ott/api_user/history/service/WatchHistoryServiceTest.java (2)

59-62: 이벤트 타입만 검증하면 회귀를 놓칠 수 있습니다.

현재는 WatchHistoryCreatedEvent 인스턴스 여부만 확인합니다. memberId, contentsId 같은 핵심 필드까지 검증해 두면 이벤트 생성 로직 변경 시 더 빨리 감지할 수 있습니다.

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

In
`@apps/api-user/src/test/java/com/ott/api_user/history/service/WatchHistoryServiceTest.java`
around lines 59 - 62, The test currently only checks the event type; update the
assertion in WatchHistoryServiceTest to capture and cast the published event
(using eventCaptor and
verify(eventPublisher).publishEvent(eventCaptor.capture())), then assert the
event is a WatchHistoryCreatedEvent and verify its key fields (e.g.,
getMemberId()/memberId and getContentsId()/contentsId) match the expected values
from the test setup (use assertEquals or AssertJ assertions against the expected
memberId and contentsId); this ensures publishEvent emitted the correct payload,
not just the correct type.

76-77: Line 76의 intValue() 변환은 제거하는 편이 안전합니다.

Long 값을 int로 축소 변환했다가 다시 long 연산에 쓰고 있어 불필요하며, 큰 값에서 오버플로우 위험이 있습니다.

🔧 제안 변경
-                .id(100L + mediaId.intValue())
+                .id(100L + mediaId)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/api-user/src/test/java/com/ott/api_user/history/service/WatchHistoryServiceTest.java`
around lines 76 - 77, Replace the unnecessary int narrowing on mediaId in the
test: change the builder call currently using id(100L + mediaId.intValue()) to
use the Long directly (e.g., id(100L + mediaId)) so you avoid pointless
intValue() conversion and potential overflow; update the expression in
WatchHistoryServiceTest where id(...) is set for the Media builder.
apps/api-user/src/test/java/com/ott/api_user/series/service/SeriesServiceTest.java (3)

130-141: 테스트 데이터의 series ID가 혼란스럽습니다.

Line 134에서 createSeries(seriesId + 1, 10L)로 series ID가 401L인 시리즈를 생성하지만, mock은 seriesId=400L로 설정되어 있습니다. 현재 테스트는 동작하지만, 이런 불일치는 코드 이해와 유지보수를 어렵게 만들 수 있습니다.

♻️ 일관된 ID 사용 제안
     `@Test`
     void getFirstEpisodeMediaId_returnsFirstEpisodeMediaId() {
         Long seriesId = 400L;
         Pageable limitOne = PageRequest.of(0, 1);
-        Contents episode = createContents(401L, createSeries(seriesId + 1, 10L), 180);
+        Contents episode = createContents(401L, createSeries(seriesId, 10L), 180);
         when(contentsRepository.findBySeriesIdAndStatusAndMedia_PublicStatusOrderByIdAsc(seriesId, Status.ACTIVE, PublicStatus.PUBLIC, limitOne))
                 .thenReturn(new PageImpl<>(List.of(episode), limitOne, 1));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/api-user/src/test/java/com/ott/api_user/series/service/SeriesServiceTest.java`
around lines 130 - 141, The test uses inconsistent series IDs: it sets seriesId
= 400L but creates a Contents with createSeries(seriesId + 1, 10L) (401L), which
is confusing; update the test so the created series uses the same ID as the
mocked query (use createSeries(seriesId, 10L)) so that the mocked repository
call and the Contents passed to PageImpl align, ensuring getFirstEpisodeMediaId
(invoked on seriesService via ReflectionTestUtils) is validated against a
consistent series ID.

117-128: Reflection을 통한 private 메서드 테스트는 구현 세부사항에 결합됩니다.

ReflectionTestUtils.invokeMethod()로 private 메서드를 테스트하면 내부 구현 변경 시 테스트가 깨질 수 있습니다. 가능하다면 public 메서드(getSeriesContents 등)를 통해 간접적으로 해당 동작을 검증하는 방식을 고려해 주세요.

다만, 해당 예외 케이스가 public API를 통해 테스트하기 어려운 경우라면 현재 방식도 수용 가능합니다.

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

In
`@apps/api-user/src/test/java/com/ott/api_user/series/service/SeriesServiceTest.java`
around lines 117 - 128, The test uses ReflectionTestUtils.invokeMethod to call
the private method getFirstEpisodeMediaId which couples the test to
implementation details; change the test to exercise the equivalent public
behavior (e.g., call getSeriesContents or another public method on seriesService
that triggers the same EPISODE_NOT_REGISTERED path) and assert the
BusinessException with ErrorCode.EPISODE_NOT_REGISTERED, or if that public path
is not available, refactor getFirstEpisodeMediaId to package-private (or extract
a small helper class) so it can be tested without reflection and update the test
to call the new accessible method instead of ReflectionTestUtils.invokeMethod.

164-188: 파라미터 이름 mediaId가 실제 용도와 맞지 않습니다.

createContents 메서드에서 mediaId 파라미터가 Contents.id(line 167)와 Media.id(line 170) 모두에 사용되고 있습니다. 이 이름은 오해를 유발할 수 있으므로 id 또는 entityId로 변경하거나, 주석으로 의도를 명확히 해주세요.

♻️ 파라미터 이름 명확화 제안
-    // 컨텐츠/미디어 엔티티를 묶어서 재생 시간 단위 테스트에 쓰기 위한 헬퍼
-    private static Contents createContents(Long mediaId, Series series, Integer duration) {
+    // 컨텐츠/미디어 엔티티를 묶어서 재생 시간 단위 테스트에 쓰기 위한 헬퍼
+    // id는 테스트 편의상 Contents와 Media에 동일하게 적용됩니다.
+    private static Contents createContents(Long id, Series series, Integer duration) {
         return Contents.builder()
-                .id(mediaId)
+                .id(id)
                 .series(series)
                 .media(Media.builder()
-                        .id(mediaId)
+                        .id(id)
                         .uploader(createMember(200L))
-                        .title("episode-" + mediaId)
+                        .title("episode-" + id)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/api-user/src/test/java/com/ott/api_user/series/service/SeriesServiceTest.java`
around lines 164 - 188, The parameter name mediaId in the helper method
createContents causes confusion because it is used for both Contents.id and
Media.id; rename the parameter to a clearer name (e.g., id or entityId) in the
createContents signature and update all internal usages
(Contents.builder().id(...) and Media.builder().id(...)) and any test callers to
use the new name, or alternatively add a short comment above createContents
explaining that the same id is intentionally used for both Contents and Media;
ensure method name and references (createContents, Contents.builder,
Media.builder) remain consistent.
apps/api-user/src/test/java/com/ott/api_user/playback/service/PlaybackServiceTest.java (2)

53-56: 예외 코드 검증을 문자열 필드명 기반 대신 타입 안전 방식으로 바꾸는 것을 권장합니다.

hasFieldOrPropertyWithValue("errorCode", ...)는 필드명 변경에 취약합니다.

변경 예시
+import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@
         assertThatThrownBy(() -> playbackService.upsertPlayback(1L, 5L, 10))
-                .isInstanceOf(BusinessException.class)
-                .hasFieldOrPropertyWithValue("errorCode", ErrorCode.CONTENTS_NOT_FOUND);
+                .isInstanceOfSatisfying(BusinessException.class,
+                        ex -> assertThat(ex.getErrorCode()).isEqualTo(ErrorCode.CONTENTS_NOT_FOUND));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/api-user/src/test/java/com/ott/api_user/playback/service/PlaybackServiceTest.java`
around lines 53 - 56, The test in PlaybackServiceTest uses the fragile
hasFieldOrPropertyWithValue("errorCode", ...) assertion; change it to a
type-safe check by extracting the thrown BusinessException and asserting its
getErrorCode() equals ErrorCode.CONTENTS_NOT_FOUND — for example, replace the
hasFieldOrPropertyWithValue call after assertThatThrownBy(() ->
playbackService.upsertPlayback(1L, 5L, 10)) with a satisfies block that casts
the exception to BusinessException and asserts ((BusinessException)
e).getErrorCode() == ErrorCode.CONTENTS_NOT_FOUND; this uses the
BusinessException#getErrorCode method and ErrorCode enum directly instead of
relying on a string field name.

40-41: 테스트 픽스처에서 Contents.idMedia.id를 동일값으로 두면 회귀를 가릴 수 있습니다.

두 ID를 분리하면 서비스가 올바른 식별자를 사용하는지 더 정확히 검증할 수 있습니다.

변경 예시
-        when(contentsRepository.findByMediaIdAndStatusAndMedia_PublicStatus(mediaId, Status.ACTIVE, PublicStatus.PUBLIC))
-                .thenReturn(Optional.of(createContents(mediaId)));
+        when(contentsRepository.findByMediaIdAndStatusAndMedia_PublicStatus(mediaId, Status.ACTIVE, PublicStatus.PUBLIC))
+                .thenReturn(Optional.of(createContents(999L, mediaId)));
@@
-    private static Contents createContents(Long mediaId) {
+    private static Contents createContents(Long contentsId, Long mediaId) {
         return Contents.builder()
-                .id(mediaId)
+                .id(contentsId)
                 .media(Media.builder()
                         .id(mediaId)

Also applies to: 58-63

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

In
`@apps/api-user/src/test/java/com/ott/api_user/playback/service/PlaybackServiceTest.java`
around lines 40 - 41, The test fixture currently sets Contents.id and Media.id
to the same value which can mask regressions; update the fixture used by
PlaybackServiceTest (the createContents helper) to assign distinct identifiers
for Contents.id and its embedded Media.id so the service logic must use the
correct id; change the createContents call(s) used in the when(...) stubbing at
the referenced test cases (and the similar block around lines 58-63) to return a
Contents instance where Contents.id != Media.id, ensuring assertions validate
the service uses the proper identifier.
apps/api-user/src/test/java/com/ott/api_user/shortform/service/ShortFormFeedServiceTest.java (1)

64-67: 컬렉션 변수명에 List 접미사를 맞추는 편이 좋습니다.

recommendedMediaIds, recommendationIds는 리스트 타입이므로 recommendedMediaIdList, recommendationIdList처럼 타입 의도가 드러나게 맞추면 가독성이 좋아집니다.

As per coding guidelines, Enforce: Collection variable names with List suffix.

Also applies to: 119-124

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

In
`@apps/api-user/src/test/java/com/ott/api_user/shortform/service/ShortFormFeedServiceTest.java`
around lines 64 - 67, Rename the collection variables to include the List suffix
to match coding guidelines: change recommendedMediaIds to recommendedMediaIdList
and recommendationIds to recommendationIdList in ShortFormFeedServiceTest
(update the declaration where List.of(...) is assigned and all usages in the
test methods), and update any imports or static references if necessary so
compilation and tests continue to pass.
apps/api-admin/src/test/java/com/ott/api_admin/click_event/service/BackOfficeClickEventServiceTest.java (1)

45-50: 테스트의 월 경계 시간 의존성으로 인한 간헐적 실패 가능성이 있습니다.

테스트와 서비스 코드 모두 LocalDate.now()를 호출하는데, 월 변경 시점(예: 1월 31일 23:59:59)에 테스트 실행 중 두 호출 사이에 날짜가 바뀔 수 있습니다. 테스트는 (2026, 1)로 모킹하지만 서비스는 (2026, 2)로 쿼리할 수 있어 테스트가 실패합니다. Clock 주입 또는 테스트 고정 시간 설정으로 결정적 테스트로 변경하는 것을 권장합니다.

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

In
`@apps/api-admin/src/test/java/com/ott/api_admin/click_event/service/BackOfficeClickEventServiceTest.java`
around lines 45 - 50, The test and service call LocalDate.now() which can drift
across month boundaries causing flaky failures; update
BackOfficeClickEventServiceTest to use a deterministic Clock (e.g.,
Clock.fixed(Instant, ZoneId)) or otherwise set a fixed current date and
construct now/last from that clock instead of calling LocalDate.now() directly
so both the test and the service use the same fixed time; modify any code paths
that call LocalDate.now() (symbols: LocalDate.now(), variables now, thisYear,
thisMonth, last, lastYear, lastMonth) to accept or derive from an injected Clock
so the test can control time.
apps/api-user/src/test/java/com/ott/api_user/shortform/controller/ShortFormControllerTest.java (1)

64-119: 권한(인증/인가) 계약을 검증하는 테스트 케이스를 추가하는 것이 좋습니다.

현재 테스트는 기능 경로만 확인하고 있어, 미인증(401)·권한 불일치(403) 시나리오가 보장되지 않습니다. 최소한 각 엔드포인트에 대해 권한 실패 케이스를 1개씩 추가해 보안 계약을 고정해 주세요.
As per coding guidelines, Strict USER/EDITOR/ADMIN authorization checks.

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

In
`@apps/api-user/src/test/java/com/ott/api_user/shortform/controller/ShortFormControllerTest.java`
around lines 64 - 119, Add negative auth tests for each existing happy-path
test: for getShortFormFeed_returnsSuccessResponseWithPageInfo,
recordShortFormView_callsShortClick, and recordCtaClick_callsCtaClickType add at
least one case that performs the same request without a principal (expect 401)
and one case with a principal lacking required role (expect 403); ensure the
tests call mockMvc.perform on "/short-forms", "/short-forms/events", and
"/short-forms/cta" respectively, assert the expected status (401 or 403), and
verify that shortFormFeedService.getShortFormFeed or
clickEventService.saveClickEvent are NOT invoked (use
verifyNoInteractions/verify(..., never())).
apps/api-user/src/test/java/com/ott/api_user/moodrefresh/service/MoodRefreshServiceTest.java (2)

96-126: hideRefreshCard의 not-found 예외 경로 테스트도 추가해 주세요.

소유권 관련 테스트는 잘 작성되어 있지만, REFRESH_CARD_NOT_FOUND 경로가 빠져 있어 예외 계약 회귀를 놓칠 수 있습니다.

추가 테스트 예시
+    `@Test`
+    void hideRefreshCard_throwsWhenCardNotFound() {
+        Long refreshId = 999L;
+        when(refreshRepository.findById(refreshId)).thenReturn(Optional.empty());
+
+        assertThatThrownBy(() -> moodRefreshService.hideRefreshCard(1L, refreshId))
+                .isInstanceOf(BusinessException.class)
+                .hasFieldOrPropertyWithValue("errorCode", ErrorCode.REFRESH_CARD_NOT_FOUND);
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/api-user/src/test/java/com/ott/api_user/moodrefresh/service/MoodRefreshServiceTest.java`
around lines 96 - 126, Add a test in MoodRefreshServiceTest covering the
not-found path for hideRefreshCard: mock refreshRepository.findById(refreshId)
to return Optional.empty() and assert that calling
moodRefreshService.hideRefreshCard(memberId, refreshId) throws BusinessException
with errorCode REFRESH_CARD_NOT_FOUND (use ErrorCode.REFRESH_CARD_NOT_FOUND),
similar to the existing ownership test; reference hideRefreshCard,
refreshRepository, BusinessException and ErrorCode to locate the code under
test.

130-133: 컬렉션 변수명에 List 접미사를 맞춰 주세요.

예: histories, tags, targetTags, recommendedMediahistoryList, tagList, targetTagList, recommendedMediaList 형태로 통일하면 가이드라인 일관성이 맞습니다.

리네이밍 예시
-        List<WatchHistory> histories = List.of(
+        List<WatchHistory> historyList = List.of(
                 createWatchHistory(31L, 201L),
                 createWatchHistory(32L, 202L)
         );
...
-        List<String> tags = ReflectionTestUtils.invokeMethod(moodRefreshService, "extractTagsFromHistories", histories);
-        assertThat(tags).containsExactlyInAnyOrder("one", "two", "three");
+        List<String> tagList = ReflectionTestUtils.invokeMethod(moodRefreshService, "extractTagsFromHistories", historyList);
+        assertThat(tagList).containsExactlyInAnyOrder("one", "two", "three");

As per coding guidelines, "Collection variable names with List suffix".

Also applies to: 153-156, 171-173, 206-210, 230-234, 257-260

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

In
`@apps/api-user/src/test/java/com/ott/api_user/moodrefresh/service/MoodRefreshServiceTest.java`
around lines 130 - 133, Rename collection variables in MoodRefreshServiceTest to
use the "List" suffix to match coding guidelines: change histories to
historyList, tags to tagList, targetTags to targetTagList, recommendedMedia to
recommendedMediaList (and any other collection variables noted in the review).
Update all usages and assertions that reference these variables (e.g., where
createWatchHistory(...) results are assigned and consumed) so the test compiles
and behavior is unchanged.
apps/api-admin/src/test/java/com/ott/api_admin/upload/support/UploadHelperTest.java (2)

60-63: 예외 타입뿐 아니라 에러 코드까지 검증해 계약을 고정하세요.

Line 60~63은 BusinessException 타입만 검증하고 있어, 다른 원인으로 던져져도 통과할 수 있습니다.

변경 제안
-        assertThatThrownBy(() ->
-                uploadHelper.validateOriginObjectKey(objectKey, "https://wrong-url", ErrorCode.SHORTFORM_ORIGIN_OBJECT_KEY_MISMATCH))
-                .isInstanceOf(BusinessException.class);
+        assertThatThrownBy(() ->
+                uploadHelper.validateOriginObjectKey(objectKey, "https://wrong-url", ErrorCode.SHORTFORM_ORIGIN_OBJECT_KEY_MISMATCH))
+                .isInstanceOf(BusinessException.class)
+                .extracting("errorCode")
+                .isEqualTo(ErrorCode.SHORTFORM_ORIGIN_OBJECT_KEY_MISMATCH);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/api-admin/src/test/java/com/ott/api_admin/upload/support/UploadHelperTest.java`
around lines 60 - 63, The test currently only asserts the thrown type from
uploadHelper.validateOriginObjectKey(objectKey, "https://wrong-url",
ErrorCode.SHORTFORM_ORIGIN_OBJECT_KEY_MISMATCH);; update the assertion to also
verify the BusinessException contains the expected ErrorCode
(SHORTFORM_ORIGIN_OBJECT_KEY_MISMATCH) so the contract is fixed — catch or
assertThatThrownBy should inspect the exception (from
uploadHelper.validateOriginObjectKey) and assert the exception's
getErrorCode()/getError() matches
ErrorCode.SHORTFORM_ORIGIN_OBJECT_KEY_MISMATCH.

67-74: 테스트 검증값이 너무 약해서 회귀를 놓칠 수 있습니다.

현재는 isPositive()만 확인해서 내부 계산이 잘못되어도 통과할 수 있습니다. Line 72~73은 입력(1 byte) 기준 기대 파트 수를 정확히 검증하는 편이 안전합니다.

변경 제안
-        int partCount = uploadHelper.getMultipartPartCount(1);
-        assertThat(partCount).isPositive();
+        int partCount = uploadHelper.getMultipartPartCount(1);
+        assertThat(partCount).isEqualTo(1);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/api-admin/src/test/java/com/ott/api_admin/upload/support/UploadHelperTest.java`
around lines 67 - 74, The test
UploadHelperTest#getMultipartPartCount_usesMultipartPlan currently only asserts
isPositive() which is weak; change the assertion to verify the exact expected
part count for a 1-byte input given the configured multipartDefaultPartSizeBytes
(5L * 1024L * 1024L) and multipartMaxParts (2000). Specifically, call
uploadHelper.getMultipartPartCount(1) and assertThat(partCount).isEqualTo(1)
(since 1 byte with a 5MB part size should yield exactly 1 part); update any
related assertion imports if needed.
apps/api-admin/src/test/java/com/ott/api_admin/shortform/service/BackOfficeShortFormReaderTest.java (1)

10-10: 주석 처리된 코드 잔여물은 제거하는 것이 좋습니다.

Line 10, Line 105의 주석 코드는 현재 테스트 의도와 무관해 가독성을 떨어뜨립니다.

Also applies to: 105-105

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

In
`@apps/api-admin/src/test/java/com/ott/api_admin/shortform/service/BackOfficeShortFormReaderTest.java`
at line 10, Remove leftover commented-out code in the
BackOfficeShortFormReaderTest test class: delete the commented import for
MediaStatus and the unrelated commented code at the other location in the file
(the remnants around the second commented block). Ensure the test file
BackOfficeShortFormReaderTest contains only active, relevant test code and
imports to improve readability.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@apps/api-admin/src/test/java/com/ott/api_admin/shortform/service/BackOfficeShortFormReaderTest.java`:
- Around line 77-79: The test BackOfficeShortFormReaderTest currently creates
authorities as Role.MEMBER and calls reader.getShortFormUploadInfo(shortFormId,
objectKey, auth), which masks real back-office role checks; update the positive
success scenario to use a back-office role with sufficient privileges (e.g.,
Role.EDITOR or Role.ADMIN) by changing the authorities/auth setup accordingly,
and move or add a separate test that asserts MEMBER is denied (throws or returns
forbidden) to cover the negative path; ensure you reference the same variables
(authorities, auth) and the method getShortFormUploadInfo so the tests validate
proper role-based access rather than allowing MEMBER to pass.

In `@apps/api-user/build.gradle`:
- Around line 44-48: The test task configuration currently sets ignoreFailures =
true in the test { ... } block which allows failing tests to still succeed
during builds (e.g., when running gradle :apps:api-user:bootJar); revert this to
the safe default by removing the ignoreFailures override or setting
ignoreFailures = false in the test task so unit test failures fail the build and
prevent producing a bootJar on broken tests.

In
`@apps/api-user/src/main/java/com/ott/api_user/moodrefresh/service/MoodRefreshService.java`:
- Line 182: In MoodRefreshService.java update the log message emitted via
log.info to insert a space between sentences so it reads "...간주합니다. 미디어 IDs: {}"
instead of "...간주합니다.미디어 IDs: {}", locate the call to log.info(...) that
references mediaIds in the MoodRefreshService class and modify the string
literal accordingly (keep the same parameter mediaIds).

In
`@apps/api-user/src/test/java/com/ott/api_user/event/MoodRefreshEventListenerTest.java`:
- Around line 31-39: The test
handleWatchHistoryCreated_swallowsServiceExceptions currently only checks that
no exception propagates and may pass if the listener never calls the service;
update the test to explicitly verify that
moodRefreshService.analyzeAndCreateRefreshCard(memberId) was invoked when
calling listener.handleWatchHistoryCreated(new
WatchHistoryCreatedEvent(memberId)). Keep the doThrow setup, then after invoking
listener.handleWatchHistoryCreated add a Mockito verification (e.g.,
verify(moodRefreshService).analyzeAndCreateRefreshCard(memberId)) to assert the
service call occurred.

In
`@apps/api-user/src/test/java/com/ott/api_user/moodrefresh/service/MoodRefreshServiceTest.java`:
- Around line 171-173: The test in MoodRefreshServiceTest fixes tag order
causing flakiness: change the assertion on the List<String> tags (result of
invoking extractTagsFromHistories) to an order-independent assertion (e.g.,
replace containsExactly(...) with AssertJ's containsExactlyInAnyOrder(...) or
containsAll(...) so the test only asserts membership not order) to make the test
stable.

In
`@apps/api-user/src/test/java/com/ott/api_user/playlist/controller/PlaylistControllerTest.java`:
- Around line 96-107: Replace all plain Java assert statements in
PlaylistControllerTest with AssertJ/JUnit assertions so tests run without JVM
-ea; specifically update the assertions around the captured PlaylistCondition
and Pageable values: use
assertThat(conditionCaptor.getValue().getContentSource()).isEqualTo(ContentSource.RECOMMEND),
assertThat(conditionCaptor.getValue().getExcludeMediaId()).isEqualTo(123L),
assertThat(pageableCaptor.getValue().getPageNumber()).isEqualTo(2) and
assertThat(pageableCaptor.getValue().getPageSize()).isEqualTo(4). Also remove
the duplicate captured variable assertions (captured) or convert them to
assertThat equivalents to avoid redundant checks. Ensure necessary static
imports for AssertJ (assertThat) are present.

In
`@apps/api-user/src/test/java/com/ott/api_user/shortform/controller/ShortFormControllerTest.java`:
- Around line 49-58: The custom HandlerMethodArgumentResolver in the test
currently intercepts all Long parameters and lacks null checks; update
supportsParameter to only return true when
parameter.getParameterType().equals(Long.class) AND
parameter.getParameterName().equals("memberId"), and update resolveArgument to
defensively handle a null Principal by checking webRequest.getUserPrincipal()
before calling getName() (return null or throw a clear exception as appropriate
for the test). Target the anonymous HandlerMethodArgumentResolver implementation
and the methods supportsParameter and resolveArgument for these changes.

In
`@apps/api-user/src/test/java/com/ott/api_user/shortform/service/ShortFormFeedServiceTest.java`:
- Around line 110-132: The test getShortFormFeed_deduplicatesSameMediaIds
currently asserts two results with the same media ID which conflicts with its
deduplication intent; update the assertions (or the test name) so they match:
call shortFormFeedService.getShortFormFeed(memberId, 0, 5) and assert
response.getDataList() has size 1 and that the single element's getShortFormId()
equals recommend.getMedia().getId(); alternatively, if you intended to allow
duplicates, rename the test from getShortFormFeed_deduplicatesSameMediaIds to
reflect that behavior so the method under test
(shortFormFeedService.getShortFormFeed) and the mock setup (recommend and
latestWithSame) are consistent with the assertion.

---

Nitpick comments:
In
`@apps/api-admin/src/test/java/com/ott/api_admin/click_event/service/BackOfficeClickEventServiceTest.java`:
- Around line 45-50: The test and service call LocalDate.now() which can drift
across month boundaries causing flaky failures; update
BackOfficeClickEventServiceTest to use a deterministic Clock (e.g.,
Clock.fixed(Instant, ZoneId)) or otherwise set a fixed current date and
construct now/last from that clock instead of calling LocalDate.now() directly
so both the test and the service use the same fixed time; modify any code paths
that call LocalDate.now() (symbols: LocalDate.now(), variables now, thisYear,
thisMonth, last, lastYear, lastMonth) to accept or derive from an injected Clock
so the test can control time.

In
`@apps/api-admin/src/test/java/com/ott/api_admin/shortform/service/BackOfficeShortFormReaderTest.java`:
- Line 10: Remove leftover commented-out code in the
BackOfficeShortFormReaderTest test class: delete the commented import for
MediaStatus and the unrelated commented code at the other location in the file
(the remnants around the second commented block). Ensure the test file
BackOfficeShortFormReaderTest contains only active, relevant test code and
imports to improve readability.

In
`@apps/api-admin/src/test/java/com/ott/api_admin/upload/support/MediaTagLinkerTest.java`:
- Around line 43-88: Add a test that verifies mediaTagLinker.linkTags throws
when categoryRepository.findByIdAndStatus returns Optional.empty: create a Media
(Media.builder().id(1L).build()), stub categoryRepository.findByIdAndStatus(10L,
Status.ACTIVE) to return Optional.empty(), call mediaTagLinker.linkTags(media,
10L, List.of(1L)) and assert that a BusinessException is thrown and (if
applicable) its errorCode equals the expected code (e.g.,
ErrorCode.CATEGORY_NOT_FOUND); reference mediaTagLinker.linkTags and
categoryRepository.findByIdAndStatus when locating where to add the test.
- Around line 19-20: Remove the unused import by deleting the line importing
java.util.Set in MediaTagLinkerTest; update the imports block (or run automatic
organize/imports) so only required imports like java.util.List remain, ensuring
MediaTagLinkerTest no longer contains the unused Set import.
- Around line 84-87: Rename the local collection variable in MediaTagLinkerTest
from saved to savedList to follow the "List" suffix guideline; update the
assignment (savedList = captor.getValue()), all subsequent uses and assertions
(assertThat(savedList).hasSize(1);
assertThat(savedList.get(0).getTag()).isSameAs(tag);
assertThat(savedList.get(0).getMedia()).isSameAs(media)) to reference savedList,
and ensure any imports or static references remain correct.
- Around line 82-84: Replace the raw ArgumentCaptor.forClass usage with a typed
`@Captor` field to avoid unchecked warnings: add a field `@Captor private
ArgumentCaptor<List<MediaTag>> mediaTagCaptor;` to MediaTagLinkerTest and then
use `verify(mediaTagRepository).saveAll(mediaTagCaptor.capture());
List<MediaTag> saved = mediaTagCaptor.getValue();` so the capture targets the
saveAll call in mediaTagRepository with full generic type safety.

In
`@apps/api-admin/src/test/java/com/ott/api_admin/upload/support/UploadHelperTest.java`:
- Around line 60-63: The test currently only asserts the thrown type from
uploadHelper.validateOriginObjectKey(objectKey, "https://wrong-url",
ErrorCode.SHORTFORM_ORIGIN_OBJECT_KEY_MISMATCH);; update the assertion to also
verify the BusinessException contains the expected ErrorCode
(SHORTFORM_ORIGIN_OBJECT_KEY_MISMATCH) so the contract is fixed — catch or
assertThatThrownBy should inspect the exception (from
uploadHelper.validateOriginObjectKey) and assert the exception's
getErrorCode()/getError() matches
ErrorCode.SHORTFORM_ORIGIN_OBJECT_KEY_MISMATCH.
- Around line 67-74: The test
UploadHelperTest#getMultipartPartCount_usesMultipartPlan currently only asserts
isPositive() which is weak; change the assertion to verify the exact expected
part count for a 1-byte input given the configured multipartDefaultPartSizeBytes
(5L * 1024L * 1024L) and multipartMaxParts (2000). Specifically, call
uploadHelper.getMultipartPartCount(1) and assertThat(partCount).isEqualTo(1)
(since 1 byte with a 5MB part size should yield exactly 1 part); update any
related assertion imports if needed.

In
`@apps/api-user/src/main/java/com/ott/api_user/playlist/service/PlaylistPreferenceService.java`:
- Around line 84-85: The local variable name tags should be renamed to tagList
to follow the project's naming convention; update the declaration using new
ArrayList<>(tagRepository.findAllById(topTagIds)) to assign to tagList and
update the subsequent sort call (tags.sort(...)) to
tagList.sort(Comparator.comparing(tag -> topTagIds.indexOf(tag.getId()))), and
ensure any other references in PlaylistPreferenceService (e.g., later uses of
tags) are updated to tagList.
- Around line 77-80: The local collection variable allTags in
PlaylistPreferenceService is correctly wrapped with new
ArrayList<>(tagRepository.findAll()) to avoid UnsupportedOperationException, but
it violates the naming guideline; rename the variable to allTagList (update its
declaration and all usages in the method) so the list variable follows the
"List" suffix convention while leaving the logic using tagRepository and
Collections.shuffle intact.

In
`@apps/api-user/src/test/java/com/ott/api_user/history/service/WatchHistoryServiceTest.java`:
- Around line 59-62: The test currently only checks the event type; update the
assertion in WatchHistoryServiceTest to capture and cast the published event
(using eventCaptor and
verify(eventPublisher).publishEvent(eventCaptor.capture())), then assert the
event is a WatchHistoryCreatedEvent and verify its key fields (e.g.,
getMemberId()/memberId and getContentsId()/contentsId) match the expected values
from the test setup (use assertEquals or AssertJ assertions against the expected
memberId and contentsId); this ensures publishEvent emitted the correct payload,
not just the correct type.
- Around line 76-77: Replace the unnecessary int narrowing on mediaId in the
test: change the builder call currently using id(100L + mediaId.intValue()) to
use the Long directly (e.g., id(100L + mediaId)) so you avoid pointless
intValue() conversion and potential overflow; update the expression in
WatchHistoryServiceTest where id(...) is set for the Media builder.

In
`@apps/api-user/src/test/java/com/ott/api_user/moodrefresh/service/MoodRefreshServiceTest.java`:
- Around line 96-126: Add a test in MoodRefreshServiceTest covering the
not-found path for hideRefreshCard: mock refreshRepository.findById(refreshId)
to return Optional.empty() and assert that calling
moodRefreshService.hideRefreshCard(memberId, refreshId) throws BusinessException
with errorCode REFRESH_CARD_NOT_FOUND (use ErrorCode.REFRESH_CARD_NOT_FOUND),
similar to the existing ownership test; reference hideRefreshCard,
refreshRepository, BusinessException and ErrorCode to locate the code under
test.
- Around line 130-133: Rename collection variables in MoodRefreshServiceTest to
use the "List" suffix to match coding guidelines: change histories to
historyList, tags to tagList, targetTags to targetTagList, recommendedMedia to
recommendedMediaList (and any other collection variables noted in the review).
Update all usages and assertions that reference these variables (e.g., where
createWatchHistory(...) results are assigned and consumed) so the test compiles
and behavior is unchanged.

In
`@apps/api-user/src/test/java/com/ott/api_user/playback/service/PlaybackServiceTest.java`:
- Around line 53-56: The test in PlaybackServiceTest uses the fragile
hasFieldOrPropertyWithValue("errorCode", ...) assertion; change it to a
type-safe check by extracting the thrown BusinessException and asserting its
getErrorCode() equals ErrorCode.CONTENTS_NOT_FOUND — for example, replace the
hasFieldOrPropertyWithValue call after assertThatThrownBy(() ->
playbackService.upsertPlayback(1L, 5L, 10)) with a satisfies block that casts
the exception to BusinessException and asserts ((BusinessException)
e).getErrorCode() == ErrorCode.CONTENTS_NOT_FOUND; this uses the
BusinessException#getErrorCode method and ErrorCode enum directly instead of
relying on a string field name.
- Around line 40-41: The test fixture currently sets Contents.id and Media.id to
the same value which can mask regressions; update the fixture used by
PlaybackServiceTest (the createContents helper) to assign distinct identifiers
for Contents.id and its embedded Media.id so the service logic must use the
correct id; change the createContents call(s) used in the when(...) stubbing at
the referenced test cases (and the similar block around lines 58-63) to return a
Contents instance where Contents.id != Media.id, ensuring assertions validate
the service uses the proper identifier.

In
`@apps/api-user/src/test/java/com/ott/api_user/series/service/SeriesServiceTest.java`:
- Around line 130-141: The test uses inconsistent series IDs: it sets seriesId =
400L but creates a Contents with createSeries(seriesId + 1, 10L) (401L), which
is confusing; update the test so the created series uses the same ID as the
mocked query (use createSeries(seriesId, 10L)) so that the mocked repository
call and the Contents passed to PageImpl align, ensuring getFirstEpisodeMediaId
(invoked on seriesService via ReflectionTestUtils) is validated against a
consistent series ID.
- Around line 117-128: The test uses ReflectionTestUtils.invokeMethod to call
the private method getFirstEpisodeMediaId which couples the test to
implementation details; change the test to exercise the equivalent public
behavior (e.g., call getSeriesContents or another public method on seriesService
that triggers the same EPISODE_NOT_REGISTERED path) and assert the
BusinessException with ErrorCode.EPISODE_NOT_REGISTERED, or if that public path
is not available, refactor getFirstEpisodeMediaId to package-private (or extract
a small helper class) so it can be tested without reflection and update the test
to call the new accessible method instead of ReflectionTestUtils.invokeMethod.
- Around line 164-188: The parameter name mediaId in the helper method
createContents causes confusion because it is used for both Contents.id and
Media.id; rename the parameter to a clearer name (e.g., id or entityId) in the
createContents signature and update all internal usages
(Contents.builder().id(...) and Media.builder().id(...)) and any test callers to
use the new name, or alternatively add a short comment above createContents
explaining that the same id is intentionally used for both Contents and Media;
ensure method name and references (createContents, Contents.builder,
Media.builder) remain consistent.

In
`@apps/api-user/src/test/java/com/ott/api_user/shortform/controller/ShortFormControllerTest.java`:
- Around line 64-119: Add negative auth tests for each existing happy-path test:
for getShortFormFeed_returnsSuccessResponseWithPageInfo,
recordShortFormView_callsShortClick, and recordCtaClick_callsCtaClickType add at
least one case that performs the same request without a principal (expect 401)
and one case with a principal lacking required role (expect 403); ensure the
tests call mockMvc.perform on "/short-forms", "/short-forms/events", and
"/short-forms/cta" respectively, assert the expected status (401 or 403), and
verify that shortFormFeedService.getShortFormFeed or
clickEventService.saveClickEvent are NOT invoked (use
verifyNoInteractions/verify(..., never())).

In
`@apps/api-user/src/test/java/com/ott/api_user/shortform/service/ClickEventServiceTest.java`:
- Around line 67-84: The tests saveClickEvent_throwsWhenMemberMissing and
saveClickEvent_throwsWhenShortFormMissing currently only assert the
BusinessException and ERROR codes; also verify that the
clickRepository.save(...) interaction does not occur to prevent side-effect
regressions. In ClickEventServiceTest, after the assertThatThrownBy in both
methods, add a Mockito verifyZeroInteractions or verify(clickRepository,
never()).save(...) (or equivalent) against the mocked clickRepository to ensure
save is not called when member or short form is missing.

In
`@apps/api-user/src/test/java/com/ott/api_user/shortform/service/ShortFormFeedServiceTest.java`:
- Around line 64-67: Rename the collection variables to include the List suffix
to match coding guidelines: change recommendedMediaIds to recommendedMediaIdList
and recommendationIds to recommendationIdList in ShortFormFeedServiceTest
(update the declaration where List.of(...) is assigned and all usages in the
test methods), and update any imports or static references if necessary so
compilation and tests continue to pass.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: da0b42e3-efb3-42bd-a1e4-e36920286150

📥 Commits

Reviewing files that changed from the base of the PR and between 2084f75 and 9e0ad0c.

📒 Files selected for processing (25)
  • apps/api-admin/build.gradle
  • apps/api-admin/src/test/java/com/ott/api_admin/ApiAdminApplicationTests.java
  • apps/api-admin/src/test/java/com/ott/api_admin/click_event/service/BackOfficeClickEventServiceTest.java
  • apps/api-admin/src/test/java/com/ott/api_admin/content/service/BackOfficeContentsServiceTest.java
  • apps/api-admin/src/test/java/com/ott/api_admin/shortform/service/BackOfficeShortFormReaderTest.java
  • apps/api-admin/src/test/java/com/ott/api_admin/shortform/service/BackOfficeShortFormServiceTest.java
  • apps/api-admin/src/test/java/com/ott/api_admin/upload/support/MediaTagLinkerTest.java
  • apps/api-admin/src/test/java/com/ott/api_admin/upload/support/UploadHelperTest.java
  • apps/api-user/build.gradle
  • apps/api-user/src/main/java/com/ott/api_user/moodrefresh/service/MoodRefreshService.java
  • apps/api-user/src/main/java/com/ott/api_user/playlist/service/PlaylistPreferenceService.java
  • apps/api-user/src/main/java/com/ott/api_user/shortform/service/ClickEventService.java
  • apps/api-user/src/test/java/com/ott/api_user/ApiUserApplicationTests.java
  • apps/api-user/src/test/java/com/ott/api_user/event/MoodRefreshEventListenerTest.java
  • apps/api-user/src/test/java/com/ott/api_user/history/service/WatchHistoryServiceTest.java
  • apps/api-user/src/test/java/com/ott/api_user/moodrefresh/service/MoodRefreshServiceTest.java
  • apps/api-user/src/test/java/com/ott/api_user/playback/service/PlaybackServiceTest.java
  • apps/api-user/src/test/java/com/ott/api_user/playlist/controller/PlaylistControllerTest.java
  • apps/api-user/src/test/java/com/ott/api_user/playlist/service/PlaylistPreferenceServiceTest.java
  • apps/api-user/src/test/java/com/ott/api_user/playlist/service/PlaylistServiceTest.java
  • apps/api-user/src/test/java/com/ott/api_user/playlist/service/PlaylistStrategyServiceTest.java
  • apps/api-user/src/test/java/com/ott/api_user/series/service/SeriesServiceTest.java
  • apps/api-user/src/test/java/com/ott/api_user/shortform/controller/ShortFormControllerTest.java
  • apps/api-user/src/test/java/com/ott/api_user/shortform/service/ClickEventServiceTest.java
  • apps/api-user/src/test/java/com/ott/api_user/shortform/service/ShortFormFeedServiceTest.java

Comment on lines +77 to +79
List<GrantedAuthority> authorities = List.of((GrantedAuthority) () -> Role.MEMBER.getKey());
var auth = new UsernamePasswordAuthenticationToken(55L, null, authorities);
int totalParts = reader.getShortFormUploadInfo(shortFormId, objectKey, auth);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

관리자 모듈 성공 시나리오에 MEMBER 권한을 쓰면 권한 우회가 테스트로 고정됩니다.

apps/api-admin 경로의 성공 케이스라면 최소 EDITOR/ADMIN 권한으로 검증해야 합니다. Line 77~79는 MEMBER로도 정상 동작을 기대하게 만들어 보안 정책을 약화시킵니다. MEMBER 거부(예외) 케이스를 별도 테스트로 분리해 주세요.

변경 제안
-        List<GrantedAuthority> authorities = List.of((GrantedAuthority) () -> Role.MEMBER.getKey());
+        List<GrantedAuthority> authorities = List.of((GrantedAuthority) () -> Role.ADMIN.getKey());
         var auth = new UsernamePasswordAuthenticationToken(55L, null, authorities);

As per coding guidelines apps/api-admin/**: Check back-office authorization and role constraints. Flag missing route protection or accidental permitAll as P0. Ensure EDITOR restrictions are not bypassable.

📝 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
List<GrantedAuthority> authorities = List.of((GrantedAuthority) () -> Role.MEMBER.getKey());
var auth = new UsernamePasswordAuthenticationToken(55L, null, authorities);
int totalParts = reader.getShortFormUploadInfo(shortFormId, objectKey, auth);
List<GrantedAuthority> authorities = List.of((GrantedAuthority) () -> Role.ADMIN.getKey());
var auth = new UsernamePasswordAuthenticationToken(55L, null, authorities);
int totalParts = reader.getShortFormUploadInfo(shortFormId, objectKey, auth);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/api-admin/src/test/java/com/ott/api_admin/shortform/service/BackOfficeShortFormReaderTest.java`
around lines 77 - 79, The test BackOfficeShortFormReaderTest currently creates
authorities as Role.MEMBER and calls reader.getShortFormUploadInfo(shortFormId,
objectKey, auth), which masks real back-office role checks; update the positive
success scenario to use a back-office role with sufficient privileges (e.g.,
Role.EDITOR or Role.ADMIN) by changing the authorities/auth setup accordingly,
and move or add a separate test that asserts MEMBER is denied (throws or returns
forbidden) to cover the negative path; ensure you reference the same variables
(authorities, auth) and the method getShortFormUploadInfo so the tests validate
proper role-based access rather than allowing MEMBER to pass.

Comment on lines +44 to +48
test {
useJUnitPlatform()
ignoreFailures = true
finalizedBy(jacocoTestReport)
}
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 검증 목적:
# 1) test task에서 실패 무시 설정 존재 여부 확인
# 2) CI workflow가 어떤 Gradle task(test/check/build)를 호출하는지 확인

rg -n --type=gradle '^\s*ignoreFailures\s*=\s*true'
fd -e yml -e yaml .github/workflows | xargs -r rg -n '(\./gradlew|gradle).*(test|check|build)'

Repository: OpenTheTaste/backend

Length of output: 561


🏁 Script executed:

#!/bin/bash
# 1) CI workflow 파일 찾기 (올바른 방법)
echo "=== CI Workflows ==="
fd . '.github/workflows' -e yml -e yaml 2>/dev/null || echo "No .github/workflows found"

# 2) apps/api-user/build.gradle 전체 내용 확인
echo -e "\n=== apps/api-user/build.gradle (full) ==="
wc -l apps/api-user/build.gradle

# 3) test task 주변 맥락 확인
echo -e "\n=== test task context ==="
sed -n '40,60p' apps/api-user/build.gradle | cat -n

# 4) check task 또는 다른 품질 게이트 확인
echo -e "\n=== check/verify tasks in api-user ==="
rg -n 'task\s+(check|verify|test)' apps/api-user/build.gradle

Repository: OpenTheTaste/backend

Length of output: 1043


🏁 Script executed:

#!/bin/bash
# 5) 전체 Gradle 플러그인 및 구성 확인
echo "=== Gradle plugins in api-user ==="
head -30 apps/api-user/build.gradle | cat -n

Repository: OpenTheTaste/backend

Length of output: 1455


🏁 Script executed:

#!/bin/bash
# 6) CI workflow 파일 검색 (gitlab-ci, workflow 등 다양한 위치)
echo "=== All workflow/CI config files ==="
find . -maxdepth 3 \( -name "*.yml" -o -name "*.yaml" -o -name ".gitlab-ci.yml" \) 2>/dev/null | head -20

Repository: OpenTheTaste/backend

Length of output: 531


🏁 Script executed:

#!/bin/bash
# 1) 주요 배포 워크플로우 파일 확인
echo "=== deploy-ec2-docker.yml (주요 배포 워크플로우) ==="
cat .github/workflows/deploy-ec2-docker.yml

Repository: OpenTheTaste/backend

Length of output: 12089


🏁 Script executed:

#!/bin/bash
# 2) qodana.yaml 확인 (코드 품질 관련)
echo "=== qodana.yaml ==="
cat qodana.yaml

Repository: OpenTheTaste/backend

Length of output: 2026


🏁 Script executed:

#!/bin/bash
# 3) 최상위 build.gradle 또는 settings.gradle 확인
echo "=== Root build.gradle or settings.gradle ==="
if [ -f build.gradle ]; then
  echo "=== build.gradle exists ==="
  wc -l build.gradle
  head -50 build.gradle | cat -n
else
  echo "No root build.gradle"
fi

if [ -f settings.gradle ]; then
  echo -e "\n=== settings.gradle ==="
  cat settings.gradle
fi

Repository: OpenTheTaste/backend

Length of output: 1765


🏁 Script executed:

#!/bin/bash
# 4) apps/api-admin/build.gradle에서 동일 설정 확인
echo "=== api-admin test task context ==="
sed -n '28,40p' apps/api-admin/build.gradle | cat -n

Repository: OpenTheTaste/backend

Length of output: 367


🏁 Script executed:

#!/bin/bash
# 1) Dockerfile 확인
echo "=== Dockerfiles ==="
find . -name "Dockerfile" -type f | head -10

Repository: OpenTheTaste/backend

Length of output: 193


🏁 Script executed:

#!/bin/bash
# 2) api-user Dockerfile 확인
echo "=== apps/api-user/Dockerfile ==="
if [ -f apps/api-user/Dockerfile ]; then
  cat apps/api-user/Dockerfile
else
  echo "Not found"
fi

Repository: OpenTheTaste/backend

Length of output: 466


🏁 Script executed:

#!/bin/bash
# 3) 다른 workflow 파일들 확인 (test, check 관련)
echo "=== Checking other workflow files for test/check tasks ==="
ls -la .github/workflows/
rg -l 'gradlew|gradle' .github/workflows/ 2>/dev/null

Repository: OpenTheTaste/backend

Length of output: 657


bootJar 빌드 시 테스트 실패가 무시되어 배포 품질이 저하됩니다

Line 46의 ignoreFailures = true 설정으로 인해, Dockerfile에서 gradle :apps:api-user:bootJar를 실행할 때 테스트가 실패해도 빌드가 성공 처리됩니다. 단위 테스트는 회귀 방지의 기본 안전장치이므로 기본값(false)으로 유지해야 합니다.

🔧 제안 수정안
 test {
 	useJUnitPlatform()
-	ignoreFailures = true
 	finalizedBy(jacocoTestReport)
 }
📝 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
test {
useJUnitPlatform()
ignoreFailures = true
finalizedBy(jacocoTestReport)
}
test {
useJUnitPlatform()
finalizedBy(jacocoTestReport)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api-user/build.gradle` around lines 44 - 48, The test task configuration
currently sets ignoreFailures = true in the test { ... } block which allows
failing tests to still succeed during builds (e.g., when running gradle
:apps:api-user:bootJar); revert this to the safe default by removing the
ignoreFailures override or setting ignoreFailures = false in the test task so
unit test failures fail the build and prevent producing a bootJar on broken
tests.

// 태그가 누락된 영상이 하나라도 있다면 환기 불가로 간주
if (primaryTags.size() < mediaIds.size()) {
log.info("[Mood Refresh]: 해당 영상에는 태그가 없어 누락으로 간주합니다.");
log.info("🚨 [Mood Refresh]: 해당 영상에는 태그가 없어 누락으로 간주합니다.미디어 IDs: {}", mediaIds);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

로그 메시지 문장 공백 누락을 수정해 주세요.

"간주합니다.미디어"는 로그 검색/가독성을 떨어뜨립니다. 문장 사이 공백을 넣어 주세요.

수정 예시
-            log.info("🚨 [Mood Refresh]: 해당 영상에는 태그가 없어 누락으로 간주합니다.미디어 IDs: {}", mediaIds);
+            log.info("🚨 [Mood Refresh]: 해당 영상에는 태그가 없어 누락으로 간주합니다. 미디어 IDs: {}", mediaIds);
📝 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
log.info("🚨 [Mood Refresh]: 해당 영상에는 태그가 없어 누락으로 간주합니다.미디어 IDs: {}", mediaIds);
log.info("🚨 [Mood Refresh]: 해당 영상에는 태그가 없어 누락으로 간주합니다. 미디어 IDs: {}", mediaIds);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/api-user/src/main/java/com/ott/api_user/moodrefresh/service/MoodRefreshService.java`
at line 182, In MoodRefreshService.java update the log message emitted via
log.info to insert a space between sentences so it reads "...간주합니다. 미디어 IDs: {}"
instead of "...간주합니다.미디어 IDs: {}", locate the call to log.info(...) that
references mediaIds in the MoodRefreshService class and modify the string
literal accordingly (keep the same parameter mediaIds).

Comment on lines +31 to +39
@Test
void handleWatchHistoryCreated_swallowsServiceExceptions() {
Long memberId = 22L;
doThrow(new RuntimeException("boom")).when(moodRefreshService).analyzeAndCreateRefreshCard(memberId);

listener.handleWatchHistoryCreated(new WatchHistoryCreatedEvent(memberId));

// 예외가 외부로 전파되지 않음을 검증하기 위해 별도 검증은 없음
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

예외 경로 테스트에 호출 단언이 없어 거짓 양성이 가능합니다.

현재는 예외 전파 여부만 간접 확인하고 있어, 리스너가 서비스를 호출하지 않아도 테스트가 통과할 수 있습니다. Line [38] 상태를 유지하지 말고 호출 검증을 추가해 주세요.

제안 수정안
 `@Test`
 void handleWatchHistoryCreated_swallowsServiceExceptions() {
     Long memberId = 22L;
     doThrow(new RuntimeException("boom")).when(moodRefreshService).analyzeAndCreateRefreshCard(memberId);

     listener.handleWatchHistoryCreated(new WatchHistoryCreatedEvent(memberId));
+    verify(moodRefreshService).analyzeAndCreateRefreshCard(memberId);

-    // 예외가 외부로 전파되지 않음을 검증하기 위해 별도 검증은 없음
+    // 예외 전파 없음 + 서비스 호출 시도 여부 검증
 }
📝 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
@Test
void handleWatchHistoryCreated_swallowsServiceExceptions() {
Long memberId = 22L;
doThrow(new RuntimeException("boom")).when(moodRefreshService).analyzeAndCreateRefreshCard(memberId);
listener.handleWatchHistoryCreated(new WatchHistoryCreatedEvent(memberId));
// 예외가 외부로 전파되지 않음을 검증하기 위해 별도 검증은 없음
}
`@Test`
void handleWatchHistoryCreated_swallowsServiceExceptions() {
Long memberId = 22L;
doThrow(new RuntimeException("boom")).when(moodRefreshService).analyzeAndCreateRefreshCard(memberId);
listener.handleWatchHistoryCreated(new WatchHistoryCreatedEvent(memberId));
verify(moodRefreshService).analyzeAndCreateRefreshCard(memberId);
// 예외 전파 없음 + 서비스 호출 시도 여부 검증
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/api-user/src/test/java/com/ott/api_user/event/MoodRefreshEventListenerTest.java`
around lines 31 - 39, The test
handleWatchHistoryCreated_swallowsServiceExceptions currently only checks that
no exception propagates and may pass if the listener never calls the service;
update the test to explicitly verify that
moodRefreshService.analyzeAndCreateRefreshCard(memberId) was invoked when
calling listener.handleWatchHistoryCreated(new
WatchHistoryCreatedEvent(memberId)). Keep the doThrow setup, then after invoking
listener.handleWatchHistoryCreated add a Mockito verification (e.g.,
verify(moodRefreshService).analyzeAndCreateRefreshCard(memberId)) to assert the
service call occurred.

Comment on lines +171 to +173
List<String> tags = ReflectionTestUtils.invokeMethod(moodRefreshService, "extractTagsFromHistories", histories);
assertThat(tags).containsExactly("one", "two", "three");
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

태그 순서 고정 단정은 flaky 테스트를 만들 수 있습니다.

현재 단정은 순서를 강제(containsExactly)하고 있어, 내부 수집 순서가 달라지면 간헐 실패할 수 있습니다. 순서 비의존 단정으로 바꾸는 편이 안전합니다.

수정 예시
-        assertThat(tags).containsExactly("one", "two", "three");
+        assertThat(tags).containsExactlyInAnyOrder("one", "two", "three");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/api-user/src/test/java/com/ott/api_user/moodrefresh/service/MoodRefreshServiceTest.java`
around lines 171 - 173, The test in MoodRefreshServiceTest fixes tag order
causing flakiness: change the assertion on the List<String> tags (result of
invoking extractTagsFromHistories) to an order-independent assertion (e.g.,
replace containsExactly(...) with AssertJ's containsExactlyInAnyOrder(...) or
containsAll(...) so the test only asserts membership not order) to make the test
stable.

Comment on lines +96 to +107
ArgumentCaptor<PlaylistCondition> conditionCaptor = ArgumentCaptor.forClass(PlaylistCondition.class);
ArgumentCaptor<Pageable> pageableCaptor = ArgumentCaptor.forClass(Pageable.class);
verify(playlistStrategyService).getPlaylists(conditionCaptor.capture(), pageableCaptor.capture());
PlaylistCondition capturedCondition = conditionCaptor.getValue();
assert capturedCondition.getContentSource() == ContentSource.RECOMMEND;
assert capturedCondition.getExcludeMediaId().equals(123L);
Pageable capturedPageable = pageableCaptor.getValue();
assert capturedPageable.getPageNumber() == 2;
assert capturedPageable.getPageSize() == 4;
PlaylistCondition captured = conditionCaptor.getValue();
assert captured.getContentSource() == ContentSource.RECOMMEND;
assert captured.getExcludeMediaId().equals(123L);
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 목적: 테스트 코드에서 Java assert 사용 위치 확인
# 기대 결과: 단위 테스트 파일에서 assert 문이 0건(또는 AssertJ/JUnit으로 대체)
rg -nP --type=java '^\s*assert\s+' -g '**/*Test.java'

Repository: OpenTheTaste/backend

Length of output: 1332


Java assert 대신 AssertJ/JUnit 어썰션을 사용하세요.

Java assert-ea JVM 옵션 없이는 실행되지 않아 테스트가 검증되지 않은 채로 통과할 수 있습니다. 현재 코드의 모든 assert 문(라인 100-101, 103-104, 106-107, 142-143)을 AssertJ의 assertThat()으로 교체하세요.

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

In
`@apps/api-user/src/test/java/com/ott/api_user/playlist/controller/PlaylistControllerTest.java`
around lines 96 - 107, Replace all plain Java assert statements in
PlaylistControllerTest with AssertJ/JUnit assertions so tests run without JVM
-ea; specifically update the assertions around the captured PlaylistCondition
and Pageable values: use
assertThat(conditionCaptor.getValue().getContentSource()).isEqualTo(ContentSource.RECOMMEND),
assertThat(conditionCaptor.getValue().getExcludeMediaId()).isEqualTo(123L),
assertThat(pageableCaptor.getValue().getPageNumber()).isEqualTo(2) and
assertThat(pageableCaptor.getValue().getPageSize()).isEqualTo(4). Also remove
the duplicate captured variable assertions (captured) or convert them to
assertThat equivalents to avoid redundant checks. Ensure necessary static
imports for AssertJ (assertThat) are present.

Comment on lines +49 to +58
.setCustomArgumentResolvers(
new HandlerMethodArgumentResolver() { // memberId(Principal) 자동 해석기
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.getParameterType().equals(Long.class);
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
return Long.valueOf(webRequest.getUserPrincipal().getName());
}
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

controller_file="$(fd 'ShortFormController.java' | head -n 1)"
echo "[1] 대상 컨트롤러: ${controller_file}"
sed -n '1,260p' "${controller_file}"

echo
echo "[2] Long/long 파라미터 사용 위치 확인"
rg -nP --type=java '\b(Long|long)\s+\w+' "${controller_file}"

echo
echo "[3] 권한 어노테이션 존재 여부 확인"
rg -n --type=java '@PreAuthorize|@Secured|@RolesAllowed' "${controller_file}"

Repository: OpenTheTaste/backend

Length of output: 6795


🏁 Script executed:

find apps/api-user -name "ShortFormController.java" -type f

Repository: OpenTheTaste/backend

Length of output: 154


🏁 Script executed:

find apps/api-user -name "ShortFormControllerTest.java" -type f

Repository: OpenTheTaste/backend

Length of output: 158


🏁 Script executed:

cat apps/api-user/src/main/java/com/ott/api_user/shortform/controller/ShortFormController.java

Repository: OpenTheTaste/backend

Length of output: 2601


🏁 Script executed:

cat apps/api-user/src/test/java/com/ott/api_user/shortform/controller/ShortFormControllerTest.java

Repository: OpenTheTaste/backend

Length of output: 5377


커스텀 인자 해석기가 모든 Long 타입을 무분별하게 가로채고 있으며, null 방어가 필요합니다.

현재 supportsParameter 메서드가 Long.class 타입 전체를 처리하므로, 컨트롤러에 다른 Long 파라미터가 추가될 경우 테스트가 실제 바인딩 오류를 가릴 수 있습니다. 파라미터명을 memberId로 명시적으로 제한하고, getUserPrincipal() null 방어를 추가해야 합니다.

수정 예시
 .setCustomArgumentResolvers(
         new HandlerMethodArgumentResolver() {
             `@Override`
             public boolean supportsParameter(MethodParameter parameter) {
-                return parameter.getParameterType().equals(Long.class);
+                return parameter.getParameterType().equals(Long.class)
+                        && "memberId".equals(parameter.getParameterName());
             }
             `@Override`
             public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
-                return Long.valueOf(webRequest.getUserPrincipal().getName());
+                var principal = webRequest.getUserPrincipal();
+                if (principal == null) {
+                    throw new IllegalStateException("Principal is required");
+                }
+                return Long.valueOf(principal.getName());
             }
         }
 )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/api-user/src/test/java/com/ott/api_user/shortform/controller/ShortFormControllerTest.java`
around lines 49 - 58, The custom HandlerMethodArgumentResolver in the test
currently intercepts all Long parameters and lacks null checks; update
supportsParameter to only return true when
parameter.getParameterType().equals(Long.class) AND
parameter.getParameterName().equals("memberId"), and update resolveArgument to
defensively handle a null Principal by checking webRequest.getUserPrincipal()
before calling getName() (return null or throw a clear exception as appropriate
for the test). Target the anonymous HandlerMethodArgumentResolver implementation
and the methods supportsParameter and resolveArgument for these changes.

Comment on lines +110 to +132
void getShortFormFeed_deduplicatesSameMediaIds() {
Long memberId = 6L;
when(playlistPreferenceService.getTotalTagScores(memberId)).thenReturn(Map.of(2L, 5));

ShortForm recommend = createShortForm(201L, 2001L, "중복추천", true);
when(shortFormRepository.findRecommendedShortForms(eq(Map.of(2L, 5)), eq(3), eq(0L)))
.thenReturn(List.of(recommend));

ShortForm latestWithSame = createShortForm(201L, 2001L, "중복최신", false);
List<Long> recommendationIds = List.of(recommend.getMedia().getId());
when(shortFormRepository.findLatestShortForms(eq(2), eq(0L), eq(recommendationIds)))
.thenReturn(List.of(latestWithSame));

ArgumentCaptor<List<Long>> likedCaptor = ArgumentCaptor.forClass(List.class);
ArgumentCaptor<List<Long>> bookmarkedCaptor = ArgumentCaptor.forClass(List.class);

when(likesRepository.findLikedMediaIds(eq(memberId), any())).thenReturn(Set.of());
when(bookmarkRepository.findBookmarkedMediaIds(eq(memberId), any())).thenReturn(Set.of());

PageResponse<ShortFormFeedResponse> response = shortFormFeedService.getShortFormFeed(memberId, 0, 5);
assertThat(response.getDataList()).hasSize(2);
assertThat(response.getDataList()).allMatch(dto -> dto.getShortFormId().equals(recommend.getMedia().getId()));

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

중복 제거 테스트 의도와 검증식이 서로 충돌합니다.

Line 110의 테스트명은 dedup 검증인데, Line 130-131은 중복이 남은 결과(2개, 동일 ID)를 기대합니다. 현재 상태로는 중복 제거 회귀를 놓칠 수 있습니다.

수정 예시
-        assertThat(response.getDataList()).hasSize(2);
-        assertThat(response.getDataList()).allMatch(dto -> dto.getShortFormId().equals(recommend.getMedia().getId()));
+        assertThat(response.getDataList()).hasSize(1);
+        assertThat(response.getDataList())
+                .extracting(ShortFormFeedResponse::getShortFormId)
+                .containsExactly(recommend.getMedia().getId());
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/api-user/src/test/java/com/ott/api_user/shortform/service/ShortFormFeedServiceTest.java`
around lines 110 - 132, The test getShortFormFeed_deduplicatesSameMediaIds
currently asserts two results with the same media ID which conflicts with its
deduplication intent; update the assertions (or the test name) so they match:
call shortFormFeedService.getShortFormFeed(memberId, 0, 5) and assert
response.getDataList() has size 1 and that the single element's getShortFormId()
equals recommend.getMedia().getId(); alternatively, if you intended to allow
duplicates, rename the test from getShortFormFeed_deduplicatesSameMediaIds to
reflect that behavior so the method under test
(shortFormFeedService.getShortFormFeed) and the mock setup (recommend and
latestWithSame) are consistent with the assertion.

Copy link
Contributor

@phonil phonil left a comment

Choose a reason for hiding this comment

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

고생했슴요요

@phonil phonil self-requested a review March 19, 2026 09:07
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.

2 participants