test(music-lab): 풀 파이프라인 통합 테스트 (mock)
This commit is contained in:
113
music-lab/tests/test_pipeline_flow.py
Normal file
113
music-lab/tests/test_pipeline_flow.py
Normal file
@@ -0,0 +1,113 @@
|
||||
"""풀 파이프라인 happy-path 통합 테스트 — 모든 외부 호출 mock."""
|
||||
import pytest
|
||||
import sqlite3
|
||||
from unittest.mock import AsyncMock, patch
|
||||
from fastapi.testclient import TestClient
|
||||
from app.main import app
|
||||
from app import db
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client(monkeypatch, tmp_path):
|
||||
monkeypatch.setattr(db, "DB_PATH", str(tmp_path / "music.db"))
|
||||
db.init_db()
|
||||
# 기본 트랙 1개 등록 (music_library 테이블)
|
||||
conn = sqlite3.connect(db.DB_PATH)
|
||||
cur = conn.cursor()
|
||||
try:
|
||||
cur.execute(
|
||||
"""INSERT INTO music_library
|
||||
(id, title, genre, moods, instruments, duration_sec, bpm, key, scale,
|
||||
prompt, audio_url, file_path, task_id, tags)
|
||||
VALUES (1, 'Integration Test', 'lo-fi', '["chill"]', '["piano"]',
|
||||
120, 85, 'C', 'maj', 'p',
|
||||
'/media/music/x.mp3', '/app/data/music/x.mp3', NULL, '[]')""",
|
||||
)
|
||||
conn.commit()
|
||||
except sqlite3.OperationalError as e:
|
||||
# 스키마가 다르면 테스트 스킵 가이드 표시
|
||||
pytest.skip(f"music_library schema mismatch: {e}")
|
||||
conn.close()
|
||||
return TestClient(app)
|
||||
|
||||
|
||||
# `new=...`로 patch한 항목은 mock 인자를 함수에 주입하지 않음.
|
||||
# return_value로 patch한 두 개(cover/video는 new=AsyncMock 사용 → 미주입,
|
||||
# thumb/video는 return_value → 주입, youtube.upload_video도 return_value → 주입).
|
||||
# 데코레이터는 아래에서 위 순서로 인자에 주입됨:
|
||||
# 1) cover (new=AsyncMock) → 미주입
|
||||
# 2) video (return_value) → 주입 → mock_video
|
||||
# 3) thumb (return_value) → 주입 → mock_thumb
|
||||
# 4) metadata (new=AsyncMock) → 미주입
|
||||
# 5) review (new=AsyncMock) → 미주입
|
||||
# 6) youtube.upload_video (return_value) → 주입 → mock_yt
|
||||
@patch("app.pipeline.youtube.upload_video", return_value={"video_id": "VID999"})
|
||||
@patch("app.pipeline.review.run_4_axis", new=AsyncMock(return_value={
|
||||
"metadata_quality": {"score": 80, "notes": "ok"},
|
||||
"policy_compliance": {"score": 90, "issues": []},
|
||||
"viewer_experience": {"score": 80, "notes": "ok"},
|
||||
"trend_alignment": {"score": 70, "matched_keywords": ["lofi"]},
|
||||
"weighted_total": 80.0, "verdict": "pass", "summary": "good", "used_fallback": False,
|
||||
}))
|
||||
@patch("app.pipeline.metadata.generate", new=AsyncMock(return_value={
|
||||
"title": "Integration Test", "description": "Test desc",
|
||||
"tags": ["lofi"], "category_id": 10, "used_fallback": False, "error": None,
|
||||
}))
|
||||
@patch("app.pipeline.thumb.generate", return_value={
|
||||
"url": "/media/videos/1/thumbnail.jpg", "used_fallback": False,
|
||||
})
|
||||
@patch("app.pipeline.video.generate", return_value={
|
||||
"url": "/media/videos/1/video.mp4", "used_fallback": False, "duration_sec": 120,
|
||||
})
|
||||
@patch("app.pipeline.cover.generate", new=AsyncMock(return_value={
|
||||
"url": "/media/videos/1/cover.jpg", "used_fallback": False, "error": None,
|
||||
}))
|
||||
def test_full_pipeline_happy_path(mock_video, mock_thumb, mock_yt, client):
|
||||
# 1. 파이프라인 생성
|
||||
pid = client.post("/api/music/pipeline", json={"track_id": 1}).json()["id"]
|
||||
assert db.get_pipeline(pid)["state"] == "created"
|
||||
|
||||
# 2. 시작 → cover 자동 생성 → cover_pending
|
||||
r = client.post(f"/api/music/pipeline/{pid}/start")
|
||||
assert r.status_code == 202
|
||||
assert db.get_pipeline(pid)["state"] == "cover_pending"
|
||||
|
||||
# 3. 4단계 사용자 승인 (cover, video, thumb, meta)
|
||||
for step in ["cover", "video", "thumb", "meta"]:
|
||||
r = client.post(f"/api/music/pipeline/{pid}/feedback",
|
||||
json={"step": step, "intent": "approve"})
|
||||
assert r.status_code == 202
|
||||
|
||||
# 4. ai_review 자동 진행 후 publish_pending
|
||||
p = db.get_pipeline(pid)
|
||||
assert p["state"] == "publish_pending"
|
||||
assert p["review"]["verdict"] == "pass"
|
||||
|
||||
# 5. 발행 트리거
|
||||
r = client.post(f"/api/music/pipeline/{pid}/publish")
|
||||
assert r.status_code == 202
|
||||
|
||||
# 6. 최종 published
|
||||
p = db.get_pipeline(pid)
|
||||
assert p["state"] == "published"
|
||||
assert p["youtube_video_id"] == "VID999"
|
||||
|
||||
|
||||
# cover.generate는 new=AsyncMock이므로 함수 인자에 주입되지 않음.
|
||||
@patch("app.pipeline.cover.generate", new=AsyncMock(return_value={
|
||||
"url": "/media/videos/2/cover.jpg", "used_fallback": False, "error": None,
|
||||
}))
|
||||
def test_pipeline_reject_and_regenerate(client):
|
||||
pid = client.post("/api/music/pipeline", json={"track_id": 1}).json()["id"]
|
||||
client.post(f"/api/music/pipeline/{pid}/start")
|
||||
assert db.get_pipeline(pid)["state"] == "cover_pending"
|
||||
|
||||
# 반려 + 피드백 → 같은 단계 재진입
|
||||
r = client.post(f"/api/music/pipeline/{pid}/feedback",
|
||||
json={"step": "cover", "intent": "reject", "feedback_text": "더 어둡게"})
|
||||
assert r.status_code == 202
|
||||
p = db.get_pipeline(pid)
|
||||
assert p["state"] == "cover_pending" # 같은 단계 유지
|
||||
assert p["feedback_count_per_step"]["cover"] == 1
|
||||
history = db.get_feedback_history(pid)
|
||||
assert history[0]["feedback_text"] == "더 어둡게"
|
||||
Reference in New Issue
Block a user