import os import pytest from unittest.mock import patch, MagicMock from app.pipeline import video, thumb, storage import respx import httpx from httpx import Response @pytest.fixture def tmp_storage(monkeypatch, tmp_path): monkeypatch.setattr(storage, "VIDEO_DATA_DIR", str(tmp_path)) # 더미 입력 파일들 audio = tmp_path / "audio.mp3" audio.write_bytes(b"\x00" * 100) cover_dir = tmp_path / "50" cover_dir.mkdir() cover = cover_dir / "cover.jpg" cover.write_bytes(b"\x00" * 100) return tmp_path @patch("subprocess.run") def test_thumb_extracts_frame(mock_run, tmp_storage): mock_run.return_value = MagicMock(returncode=0, stderr="") video_path = tmp_storage / "60" / "video.mp4" video_path.parent.mkdir() video_path.write_bytes(b"\x00" * 100) out = thumb.generate(pipeline_id=60, video_path=str(video_path), track_title="Midnight Drive", overlay_text=False) assert out["url"].endswith("/60/thumbnail.jpg") args = mock_run.call_args[0][0] assert args[0] == "ffmpeg" @patch("subprocess.run") def test_thumb_failure_raises(mock_run, tmp_storage): mock_run.return_value = MagicMock(returncode=1, stderr="bad input") video_path = tmp_storage / "61" / "video.mp4" video_path.parent.mkdir() video_path.write_bytes(b"\x00" * 100) with pytest.raises(thumb.ThumbGenerationError): thumb.generate(pipeline_id=61, video_path=str(video_path), track_title="X", overlay_text=False) # ===== Video tests (replacing the FFmpeg-based tests) ===== @pytest.fixture def encoder_env(monkeypatch): monkeypatch.setattr(video, "ENCODER_URL", "http://192.168.45.59:8765") monkeypatch.setattr(video, "NAS_VIDEOS_ROOT", "/volume1/docker/webpage/data/videos") monkeypatch.setattr(video, "NAS_MUSIC_ROOT", "/volume1/docker/webpage/data/music") @respx.mock def test_generate_video_calls_remote_encoder(encoder_env, tmp_path, monkeypatch): monkeypatch.setattr(storage, "VIDEO_DATA_DIR", str(tmp_path)) respx.post("http://192.168.45.59:8765/encode_video").mock( return_value=Response(200, json={ "ok": True, "duration_ms": 12000, "output_path_nas": "/volume1/docker/webpage/data/videos/3/video.mp4", "output_bytes": 28000000, "encoder": "h264_nvenc", "preset": "p4", }) ) out = video.generate( pipeline_id=3, audio_path="/app/data/1c695df3.mp3", cover_path="/app/data/videos/3/cover.jpg", genre="lo-fi", duration_sec=120, resolution="1920x1080", style="visualizer", ) assert out["url"].endswith("/3/video.mp4") assert out["used_fallback"] is False assert out["encode_duration_ms"] == 12000 @respx.mock def test_generate_video_raises_on_connection_error(encoder_env, monkeypatch, tmp_path): monkeypatch.setattr(storage, "VIDEO_DATA_DIR", str(tmp_path)) respx.post("http://192.168.45.59:8765/encode_video").mock( side_effect=httpx.ConnectError("Connection refused") ) with pytest.raises(video.VideoGenerationError) as exc: video.generate( pipeline_id=4, audio_path="/app/data/x.mp3", cover_path="/app/data/videos/4/cover.jpg", genre="lo-fi", duration_sec=120, resolution="1920x1080", ) assert "연결 실패" in str(exc.value) or "Connection" in str(exc.value) @respx.mock def test_generate_video_raises_on_500(encoder_env, monkeypatch, tmp_path): monkeypatch.setattr(storage, "VIDEO_DATA_DIR", str(tmp_path)) respx.post("http://192.168.45.59:8765/encode_video").mock( return_value=Response(500, json={"detail": {"ok": False, "stage": "ffmpeg", "error": "bad codec"}}) ) with pytest.raises(video.VideoGenerationError) as exc: video.generate( pipeline_id=5, audio_path="/app/data/x.mp3", cover_path="/app/data/videos/5/cover.jpg", genre="lo-fi", duration_sec=120, resolution="1920x1080", ) assert "Windows 인코더 오류" in str(exc.value) assert "ffmpeg" in str(exc.value) def test_generate_video_no_url_configured(monkeypatch, tmp_path): monkeypatch.setattr(storage, "VIDEO_DATA_DIR", str(tmp_path)) monkeypatch.setattr(video, "ENCODER_URL", "") with pytest.raises(video.VideoGenerationError) as exc: video.generate( pipeline_id=6, audio_path="/app/data/x.mp3", cover_path="/app/data/videos/6/cover.jpg", genre="lo-fi", duration_sec=120, resolution="1920x1080", ) assert "WINDOWS_VIDEO_ENCODER_URL" in str(exc.value) def test_container_to_nas_videos_path(monkeypatch): monkeypatch.setattr(video, "NAS_VIDEOS_ROOT", "/volume1/docker/webpage/data/videos") monkeypatch.setattr(video, "NAS_MUSIC_ROOT", "/volume1/docker/webpage/data/music") assert video._container_to_nas("/app/data/videos/3/cover.jpg") == "/volume1/docker/webpage/data/videos/3/cover.jpg" def test_container_to_nas_music_path(monkeypatch): monkeypatch.setattr(video, "NAS_VIDEOS_ROOT", "/volume1/docker/webpage/data/videos") monkeypatch.setattr(video, "NAS_MUSIC_ROOT", "/volume1/docker/webpage/data/music") assert video._container_to_nas("/app/data/abc.mp3") == "/volume1/docker/webpage/data/music/abc.mp3" def test_container_to_nas_strips_cache_buster(monkeypatch): monkeypatch.setattr(video, "NAS_VIDEOS_ROOT", "/volume1/docker/webpage/data/videos") monkeypatch.setattr(video, "NAS_MUSIC_ROOT", "/volume1/docker/webpage/data/music") # cache-busted path → strip ?v=... before NAS conversion assert video._container_to_nas("/app/data/videos/3/cover.jpg?v=20260510065642") == "/volume1/docker/webpage/data/videos/3/cover.jpg" @respx.mock def test_generate_video_passes_essential_params(encoder_env, tmp_path, monkeypatch): monkeypatch.setattr(storage, "VIDEO_DATA_DIR", str(tmp_path)) captured = {} def hook(req): import json as _json captured["body"] = _json.loads(req.content) return Response(200, json={"ok": True, "duration_ms": 5000, "output_path_nas": "/v/3/video.mp4", "output_bytes": 10_000_000, "encoder": "h264_nvenc", "preset": "p4"}) respx.post("http://192.168.45.59:8765/encode_video").mock(side_effect=hook) out = video.generate( pipeline_id=3, audio_path="/app/data/x.mp3", cover_path="/app/data/videos/3/cover.jpg", genre="mix", duration_sec=3600, resolution="1920x1080", style="essential", background_mode="video_loop", background_path="/app/data/videos/3/loop.mp4", tracks=[{"id": 1, "title": "T1", "start_offset_sec": 0}], ) body = captured["body"] assert body["style"] == "essential" assert body["background_mode"] == "video_loop" assert body["background_path_nas"] == "/volume1/docker/webpage/data/videos/3/loop.mp4" assert body["tracks"][0]["title"] == "T1" assert out["url"].endswith("/3/video.mp4")