Files

72 lines
4.6 KiB
Markdown

# 추천 API 실제 HTTP 검증
## 실행 증거
- 실행일: 2026-10-31. 제품 변경 기준: `969f999a`와 동일한 제품 코드.
- 표면: `SpringBootTest(RANDOM_PORT)` + `TestRestTemplate`, loopback HTTP, 격리 H2 및 embedded Redis.
- 인증: 테스트 DB에 저장한 회원과 Auth, 실제 `TokenProvider.createToken`으로 발급한 Bearer JWT. MockMvc principal 주입이 아니다.
- fixture는 `TransactionTemplate`으로 commit한 뒤 HTTP 요청했다. 운영 DB 및 외부 API를 호출하지 않았다.
- 결과: HTTP 요청 **104건**, 상태·응답·정책 비교 판정 **216건**, 실패 **0건**, `BUILD SUCCESSFUL in 30s`.
- 두 추천 API 및 `/api/v2/events/{id}`를 함께 호출해 추천 배너의 이벤트 ID와 상세 조회 허용 ID를 비교했다.
실행 명령은 아래와 같다. QA_ROOT는 작업 소유 임시 디렉터리였으며 최종 자원 정리 대상이다.
```bash
./gradlew --init-script "$QA_ROOT/socket-qa.init.gradle" test \
--tests kr.co.vividnext.sodalive.v2.qa.RecommendationSocketQaTest
```
원본 `evidence-second/summary.json`, `responses.json`을 직접 읽고 아래 결과를 발췌했다. 토큰·인증 헤더·Auth 개인정보는 보존하지 않는다. 아래 ID는 테스트 fixture ID이지 운영 이벤트 ID가 아니다.
## 실제 결과
| 국가 / Accept-Language | 조회자 | 홈 이벤트 ID 순서 | 오디오 이벤트 ID 순서 | 상세 허용 ID 집합 | 판정 |
|---|---|---|---|---|---|
| KR / ja | 익명 | 1, 2 | 1, 2 | 1, 2 | PASS |
| KR / ja | 미인증 | 1, 2 | 1, 2 | 1, 2 | PASS |
| KR / ja | 인증 | 1, 3 | 1, 3 | 1, 3 | PASS |
| KR / ja | ADMIN | 1, 2, 3 | 1, 2, 3 | 1, 2, 3 | PASS |
| JP / ko | 익명 | 8, 9 | 8, 9 | 8, 9 | PASS |
| JP / ko | 미인증 | 8, 9, 8 | 8, 9, 8 | 8, 9 | PASS |
| JP / ko | 인증 | 8, 10 | 8, 10 | 8, 10 | PASS |
| JP / ko | ADMIN | 8, 9, 10 | 8, 9, 10 | 8, 9, 10 | PASS |
JP 미인증의 마지막 ID 8은 동일한 전체 대상 이벤트에 연결된 별도 성인 배너다. 해당 회원은 성인 표시 설정이 켜져 있어 배너 자체 조건은 통과하지만, 인증 전용 이벤트 ID 10은 노출되지 않는다. 이벤트 중복 제거를 추가하지 않고 기존 배너 단위 계약을 유지한다.
모든 행에서 성공 envelope, 기존 top-level 필드 집합, 정확한 배너 이미지 순서, LINK 유지, `subBanners: []`를 확인했다. 홈은 `aiCharacters: []`, 오디오는 해당 필드가 없다. 인증 회원의 성인 표시 설정은 꺼져 있었어도 인증 대상 이벤트 배너 자체가 비성인이면 정상 노출됐다.
## HTTP 응답 발췌
익명 KR 요청의 홈 응답에서 관찰한 부분이다. 전체 응답 예시가 아닌 실제 필드 발췌다.
```json
{
"success": true,
"data": {
"aiCharacters": [],
"subBanners": [],
"banners": [
{ "imageUrl": "https://cdn.test/qa/KR/HOME/all.png", "eventItem": { "id": 1 } },
{ "imageUrl": "https://cdn.test/qa/KR/HOME/false.png", "eventItem": { "id": 2 } },
{ "imageUrl": "https://cdn.test/qa/KR/HOME/retained-link.png", "eventItem": null },
{ "imageUrl": "https://cdn.test/qa/KR/HOME/creator-block.png", "creatorId": 4 }
]
}
}
```
## 경계 및 인접 표면
- 비활성·시작 전·종료 후·이벤트 언어 불일치·연결 이벤트 없는 EVENT 배너는 제외됐다. ADMIN도 이를 우회하지 못했다.
- 배너 자체의 비활성·탭·언어·예약·성인 조건을 유지했고, 로그인 회원에게 차단된 CREATOR 배너는 제외됐다.
- LINK 문자열에 이벤트처럼 보이는 URL이 있어도 임의 파싱하지 않고 기존 배너로 유지했다.
- fixture 이벤트 전체를 비활성화하고 비EVENT 배너도 비활성화한 후 4종 조회자 × KR/JP × 두 API에서 `banners: []`를 확인했다.
- 이 HTTP 검증은 넓은 시간창을 사용했다. 정확한 나노초 경계·limit·random tie·AI 조회 비호출·전용 AI endpoint 보존은 계획의 focused 테스트 증거로 별도 검증한다.
## 실행 중 보정 및 한계
- 첫 QA 실행은 HTTP 요청 이전에 Spring Redis `setConfig("save", "")`의 빈 값 검증에서 실패했다. 제품 결함이 아니며 임시 harness만 raw `CONFIG SET save` 호출로 바꾼 뒤 재실행했다.
- 두 번째 실행이 위 PASS 결과다. 임시 Redis의 snapshot 저장만 끄고 테스트 토큰을 종료 시 삭제하도록 구성했다.
- 운영 MySQL 데이터·Event 컬럼 저장 정밀도는 검증하지 않았다. LSP 도구가 없어 LSP 진단을 실행했다고 주장하지 않는다.
- 최종 전체 빌드·리뷰·임시 자원 정리 결과는 `plan-task.md`의 Gate 기록을 따른다.