import sqlite3 import pytest 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() 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, 'T', 'lo-fi', '["chill"]', '["piano"]', 120, 85, 'C', 'maj', 'p', '/media/music/x.mp3', '/app/data/music/x.mp3', NULL, '[]')""", ) conn.commit() conn.close() return TestClient(app) def test_create_pipeline(client): r = client.post("/api/music/pipeline", json={"track_id": 1}) assert r.status_code == 201 assert r.json()["state"] == "created" def test_create_duplicate_pipeline_returns_409(client): client.post("/api/music/pipeline", json={"track_id": 1}) r = client.post("/api/music/pipeline", json={"track_id": 1}) assert r.status_code == 409 def test_get_pipeline_returns_jobs_and_feedback(client): pid = client.post("/api/music/pipeline", json={"track_id": 1}).json()["id"] r = client.get(f"/api/music/pipeline/{pid}") assert "jobs" in r.json() assert "feedback" in r.json() def test_list_pipelines_active_filter(client): pid = client.post("/api/music/pipeline", json={"track_id": 1}).json()["id"] db.update_pipeline_state(pid, "published") r = client.get("/api/music/pipeline?status=active") assert all(p["state"] != "published" for p in r.json()["pipelines"]) def test_feedback_reject_records_feedback_and_increments_count(client): pid = client.post("/api/music/pipeline", json={"track_id": 1}).json()["id"] db.update_pipeline_state(pid, "cover_pending") # orchestrator.run_step를 mock해서 백그라운드 작업이 cover_pending을 변경하지 않도록 with patch("app.main.orchestrator.run_step", new=AsyncMock()): 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["feedback_count_per_step"]["cover"] == 1 history = db.get_feedback_history(pid) assert history[0]["feedback_text"] == "더 어둡게" def test_feedback_after_5_rejects_marks_awaiting_manual(client): pid = client.post("/api/music/pipeline", json={"track_id": 1}).json()["id"] db.update_pipeline_state(pid, "cover_pending") with patch("app.main.orchestrator.run_step", new=AsyncMock()): for i in range(5): client.post( f"/api/music/pipeline/{pid}/feedback", json={"step": "cover", "intent": "reject", "feedback_text": f"again {i}"}, ) r = client.post( f"/api/music/pipeline/{pid}/feedback", json={"step": "cover", "intent": "reject", "feedback_text": "6th"}, ) assert r.status_code == 409 assert db.get_pipeline(pid)["state"] == "awaiting_manual" def test_cancel_pipeline(client): pid = client.post("/api/music/pipeline", json={"track_id": 1}).json()["id"] r = client.post(f"/api/music/pipeline/{pid}/cancel") assert r.status_code == 200 assert db.get_pipeline(pid)["state"] == "cancelled" def test_setup_get_returns_defaults(client): r = client.get("/api/music/setup") assert r.status_code == 200 assert r.json()["review_threshold"] == 60 def test_setup_put_updates(client): r = client.put("/api/music/setup", json={"review_threshold": 70}) assert r.status_code == 200 assert r.json()["review_threshold"] == 70 def test_youtube_status_when_disconnected(client): r = client.get("/api/music/youtube/status") assert r.status_code == 200 assert r.json() == {"connected": False}