Spaces:
Running
Running
| import io | |
| import uuid | |
| import pytest | |
| pytestmark = pytest.mark.asyncio | |
| async def test_health(client): | |
| response = await client.get("/health") | |
| assert response.status_code == 200 | |
| assert response.json() == {"status": "ok"} | |
| async def test_status_unknown_job(client): | |
| response = await client.get("/status/nonexistent-job-id") | |
| assert response.status_code == 404 | |
| async def test_status_known_job(client, monkeypatch): | |
| import processor | |
| from models import JobState, JobStatus | |
| fake_status = JobStatus(job_id="abc", state=JobState.pending, progress=0) | |
| monkeypatch.setattr(processor, "_jobs", {"abc": fake_status}) | |
| response = await client.get("/status/abc") | |
| assert response.status_code == 200 | |
| data = response.json() | |
| assert data["job_id"] == "abc" | |
| assert data["state"] == "pending" | |
| assert data["progress"] == 0 | |
| assert data["error"] is None | |
| async def test_upload_mp3(client): | |
| fake_mp3 = io.BytesIO(b"\xff\xfb" + b"\x00" * 100) | |
| response = await client.post( | |
| "/upload", | |
| files={"file": ("song.mp3", fake_mp3, "audio/mpeg")}, | |
| ) | |
| assert response.status_code == 200 | |
| data = response.json() | |
| assert "job_id" in data | |
| uuid.UUID(data["job_id"]) | |
| async def test_upload_with_trim_params(client): | |
| fake_mp3 = io.BytesIO(b"\xff\xfb" + b"\x00" * 100) | |
| response = await client.post( | |
| "/upload", | |
| files={"file": ("song.mp3", fake_mp3, "audio/mpeg")}, | |
| data={"trim_start_sec": "4.5", "trim_end_sec": "22.75"}, | |
| ) | |
| assert response.status_code == 200 | |
| async def test_upload_rejects_invalid_trim_range(client): | |
| fake_mp3 = io.BytesIO(b"\xff\xfb" + b"\x00" * 100) | |
| response = await client.post( | |
| "/upload", | |
| files={"file": ("song.mp3", fake_mp3, "audio/mpeg")}, | |
| data={"trim_start_sec": "12", "trim_end_sec": "10"}, | |
| ) | |
| assert response.status_code == 400 | |
| assert "greater" in response.json()["detail"].lower() | |
| async def test_upload_rejects_negative_trim_start(client): | |
| fake_mp3 = io.BytesIO(b"\xff\xfb" + b"\x00" * 100) | |
| response = await client.post( | |
| "/upload", | |
| files={"file": ("song.mp3", fake_mp3, "audio/mpeg")}, | |
| data={"trim_start_sec": "-1"}, | |
| ) | |
| assert response.status_code == 400 | |
| async def test_upload_rejects_non_mp3(client): | |
| fake_file = io.BytesIO(b"fake content") | |
| response = await client.post( | |
| "/upload", | |
| files={"file": ("doc.pdf", fake_file, "application/pdf")}, | |
| ) | |
| assert response.status_code == 400 | |
| assert "MP3" in response.json()["detail"] | |
| async def test_upload_accepts_audio_mp3_content_type(client): | |
| fake_mp3 = io.BytesIO(b"\xff\xfb" + b"\x00" * 100) | |
| response = await client.post( | |
| "/upload", | |
| files={"file": ("song.mp3", fake_mp3, "audio/mp3")}, | |
| ) | |
| assert response.status_code == 200 | |
| async def test_upload_rejects_oversized_file(client, monkeypatch): | |
| import main | |
| monkeypatch.setattr(main, "MAX_FILE_SIZE", 8) | |
| oversized = io.BytesIO(b"\xff\xfb" + b"\x00" * 20) | |
| response = await client.post( | |
| "/upload", | |
| files={"file": ("big.mp3", oversized, "audio/mpeg")}, | |
| ) | |
| assert response.status_code == 400 | |
| assert "large" in response.json()["detail"].lower() | |
| async def test_upload_rejects_non_mp3_bytes(client): | |
| fake_zip = io.BytesIO(b"PK\x03\x04" + b"\x00" * 100) | |
| response = await client.post( | |
| "/upload", | |
| files={"file": ("trap.mp3", fake_zip, "audio/mpeg")}, | |
| ) | |
| assert response.status_code == 400 | |
| assert "valid MP3" in response.json()["detail"] | |
| async def test_download_done_job(client, tmp_path, monkeypatch): | |
| import main | |
| import processor | |
| from models import JobState, JobStatus | |
| job_id = "done-job-123" | |
| job_dir = tmp_path / job_id | |
| job_dir.mkdir(parents=True, exist_ok=True) | |
| fake_output = job_dir / "instrumental.mp3" | |
| fake_output.write_bytes(b"\xff\xfb" + b"\x00" * 200) | |
| fake_status = JobStatus( | |
| job_id=job_id, | |
| state=JobState.done, | |
| progress=100, | |
| stems={"instrumental": "instrumental.mp3"}, | |
| ) | |
| monkeypatch.setitem(processor._jobs, job_id, fake_status) | |
| monkeypatch.setattr(main, "OUTPUT_DIR", tmp_path) | |
| monkeypatch.setattr(main, "STATS_FILE", tmp_path / "stats.json") | |
| monkeypatch.setattr(main, "DOWNLOAD_LOG_FILE", tmp_path / "download_log.json") | |
| response = await client.get(f"/download/{job_id}") | |
| assert response.status_code == 200 | |
| assert response.headers["content-type"].startswith("audio/mpeg") | |
| assert response.content == b"\xff\xfb" + b"\x00" * 200 | |
| async def test_download_specific_stem(client, tmp_path, monkeypatch): | |
| import main | |
| import processor | |
| from models import JobState, JobStatus | |
| job_id = "stem-job" | |
| job_dir = tmp_path / job_id | |
| job_dir.mkdir(parents=True, exist_ok=True) | |
| vocal_file = job_dir / "vocals.mp3" | |
| vocal_file.write_bytes(b"\xff\xfb" + b"\x00" * 60) | |
| fake_status = JobStatus( | |
| job_id=job_id, | |
| state=JobState.done, | |
| progress=100, | |
| stems={"vocals": "vocals.mp3"}, | |
| ) | |
| monkeypatch.setitem(processor._jobs, job_id, fake_status) | |
| monkeypatch.setattr(main, "OUTPUT_DIR", tmp_path) | |
| monkeypatch.setattr(main, "STATS_FILE", tmp_path / "stats.json") | |
| monkeypatch.setattr(main, "DOWNLOAD_LOG_FILE", tmp_path / "download_log.json") | |
| response = await client.get(f"/download/{job_id}/vocals") | |
| assert response.status_code == 200 | |
| assert response.content == b"\xff\xfb" + b"\x00" * 60 | |
| async def test_download_original_stem(client, tmp_path, monkeypatch): | |
| import main | |
| import processor | |
| from models import JobState, JobStatus | |
| job_id = "original-job" | |
| job_dir = tmp_path / job_id | |
| job_dir.mkdir(parents=True, exist_ok=True) | |
| original_file = job_dir / "original.mp3" | |
| original_file.write_bytes(b"\xff\xfb" + b"\x00" * 70) | |
| fake_status = JobStatus( | |
| job_id=job_id, | |
| state=JobState.done, | |
| progress=100, | |
| stems={"instrumental": "instrumental.mp3"}, | |
| ) | |
| monkeypatch.setitem(processor._jobs, job_id, fake_status) | |
| monkeypatch.setattr(main, "UPLOAD_DIR", tmp_path / "uploads") | |
| monkeypatch.setattr(main, "OUTPUT_DIR", tmp_path) | |
| response = await client.get(f"/download/{job_id}/original") | |
| assert response.status_code == 200 | |
| assert response.content == b"\xff\xfb" + b"\x00" * 70 | |
| async def test_download_original_stem_falls_back_to_upload_dir(client, tmp_path, monkeypatch): | |
| import main | |
| import processor | |
| from models import JobState, JobStatus | |
| job_id = "original-fallback-job" | |
| upload_dir = tmp_path / "uploads" | |
| upload_dir.mkdir(parents=True, exist_ok=True) | |
| upload_file = upload_dir / f"{job_id}.mp3" | |
| upload_file.write_bytes(b"\xff\xfb" + b"\x00" * 70) | |
| fake_status = JobStatus( | |
| job_id=job_id, | |
| state=JobState.done, | |
| progress=100, | |
| stems={"instrumental": "instrumental.mp3"}, | |
| ) | |
| monkeypatch.setitem(processor._jobs, job_id, fake_status) | |
| monkeypatch.setattr(main, "UPLOAD_DIR", upload_dir) | |
| monkeypatch.setattr(main, "OUTPUT_DIR", tmp_path) | |
| response = await client.get(f"/download/{job_id}/original") | |
| assert response.status_code == 200 | |
| assert response.content == b"\xff\xfb" + b"\x00" * 70 | |
| async def test_download_unknown_stem_returns_404(client, tmp_path, monkeypatch): | |
| import main | |
| import processor | |
| from models import JobState, JobStatus | |
| job_id = "unknown-stem-job" | |
| fake_status = JobStatus( | |
| job_id=job_id, | |
| state=JobState.done, | |
| progress=100, | |
| stems={"vocals": "vocals.mp3"}, | |
| ) | |
| monkeypatch.setitem(processor._jobs, job_id, fake_status) | |
| monkeypatch.setattr(main, "OUTPUT_DIR", tmp_path) | |
| response = await client.get(f"/download/{job_id}/drums") | |
| assert response.status_code == 404 | |
| async def test_download_processing_job_returns_409(client, monkeypatch): | |
| import processor | |
| from models import JobState, JobStatus | |
| job_id = "processing-job" | |
| fake_status = JobStatus(job_id=job_id, state=JobState.processing, progress=40) | |
| monkeypatch.setitem(processor._jobs, job_id, fake_status) | |
| response = await client.get(f"/download/{job_id}") | |
| assert response.status_code == 409 | |
| assert "complete" in response.json()["detail"].lower() | |
| async def test_download_unknown_job_returns_404(client): | |
| response = await client.get("/download/does-not-exist") | |
| assert response.status_code == 404 | |
| async def test_get_stats_returns_zeros_when_no_file(client, tmp_path, monkeypatch): | |
| import main | |
| monkeypatch.setattr(main, "STATS_FILE", tmp_path / "stats.json") | |
| response = await client.get("/stats") | |
| assert response.status_code == 200 | |
| data = response.json() | |
| assert data["total_visits"] == 100 | |
| assert data["total_downloads"] == 5 | |
| async def test_record_visit_increments_total_visits(client, tmp_path, monkeypatch): | |
| import main | |
| monkeypatch.setattr(main, "STATS_FILE", tmp_path / "stats.json") | |
| response = await client.post("/stats/visit") | |
| assert response.status_code == 204 | |
| stats = (await client.get("/stats")).json() | |
| assert stats["total_visits"] == 101 | |
| async def test_record_visit_is_cumulative(client, tmp_path, monkeypatch): | |
| import main | |
| monkeypatch.setattr(main, "STATS_FILE", tmp_path / "stats.json") | |
| await client.post("/stats/visit") | |
| await client.post("/stats/visit") | |
| stats = (await client.get("/stats")).json() | |
| assert stats["total_visits"] == 102 | |
| async def test_download_rejects_out_of_range_pitch(client, tmp_path, monkeypatch): | |
| import main | |
| import processor | |
| from models import JobState, JobStatus | |
| job_id = "pitch-range-job" | |
| job_dir = tmp_path / job_id | |
| job_dir.mkdir(parents=True, exist_ok=True) | |
| (job_dir / "instrumental.mp3").write_bytes(b"\xff\xfb" + b"\x00" * 100) | |
| monkeypatch.setattr(main, "OUTPUT_DIR", tmp_path) | |
| monkeypatch.setitem( | |
| processor._jobs, | |
| job_id, | |
| JobStatus( | |
| job_id=job_id, | |
| state=JobState.done, | |
| progress=100, | |
| stems={"instrumental": "instrumental.mp3"}, | |
| ), | |
| ) | |
| response = await client.get(f"/download/{job_id}?pitch_semitones=20") | |
| assert response.status_code == 400 | |
| assert "pitch" in response.json()["detail"].lower() | |
| async def test_download_rejects_out_of_range_tempo(client, tmp_path, monkeypatch): | |
| import main | |
| import processor | |
| from models import JobState, JobStatus | |
| job_id = "tempo-range-job" | |
| job_dir = tmp_path / job_id | |
| job_dir.mkdir(parents=True, exist_ok=True) | |
| (job_dir / "instrumental.mp3").write_bytes(b"\xff\xfb" + b"\x00" * 100) | |
| monkeypatch.setattr(main, "OUTPUT_DIR", tmp_path) | |
| monkeypatch.setitem( | |
| processor._jobs, | |
| job_id, | |
| JobStatus( | |
| job_id=job_id, | |
| state=JobState.done, | |
| progress=100, | |
| stems={"instrumental": "instrumental.mp3"}, | |
| ), | |
| ) | |
| response = await client.get(f"/download/{job_id}?tempo_pct=25") | |
| assert response.status_code == 400 | |
| assert "tempo" in response.json()["detail"].lower() | |
| async def test_download_default_params_serve_raw_file(client, tmp_path, monkeypatch): | |
| import main | |
| import processor | |
| from models import JobState, JobStatus | |
| job_id = "raw-serve-job" | |
| job_dir = tmp_path / job_id | |
| job_dir.mkdir(parents=True, exist_ok=True) | |
| raw_bytes = b"\xff\xfb" + b"\x00" * 123 | |
| (job_dir / "instrumental.mp3").write_bytes(raw_bytes) | |
| monkeypatch.setattr(main, "STATS_FILE", tmp_path / "stats.json") | |
| monkeypatch.setattr(main, "DOWNLOAD_LOG_FILE", tmp_path / "download_log.json") | |
| monkeypatch.setattr(main, "OUTPUT_DIR", tmp_path) | |
| monkeypatch.setitem( | |
| processor._jobs, | |
| job_id, | |
| JobStatus( | |
| job_id=job_id, | |
| state=JobState.done, | |
| progress=100, | |
| stems={"instrumental": "instrumental.mp3"}, | |
| ), | |
| ) | |
| response = await client.get( | |
| f"/download/{job_id}?pitch_semitones=0&tempo_pct=100" | |
| ) | |
| assert response.status_code == 200 | |
| assert response.content == raw_bytes | |
| async def test_download_increments_total_downloads(client, tmp_path, monkeypatch): | |
| import main | |
| import processor | |
| from models import JobState, JobStatus | |
| job_id = "dl-stat-job" | |
| job_dir = tmp_path / job_id | |
| job_dir.mkdir(parents=True, exist_ok=True) | |
| fake_output = job_dir / "instrumental.mp3" | |
| fake_output.write_bytes(b"\xff\xfb" + b"\x00" * 100) | |
| monkeypatch.setattr(main, "STATS_FILE", tmp_path / "stats.json") | |
| monkeypatch.setattr(main, "DOWNLOAD_LOG_FILE", tmp_path / "download_log.json") | |
| monkeypatch.setattr(main, "OUTPUT_DIR", tmp_path) | |
| monkeypatch.setitem( | |
| processor._jobs, | |
| job_id, | |
| JobStatus( | |
| job_id=job_id, | |
| state=JobState.done, | |
| progress=100, | |
| stems={"instrumental": "instrumental.mp3"}, | |
| ), | |
| ) | |
| await client.get(f"/download/{job_id}") | |
| stats = (await client.get("/stats")).json() | |
| assert stats["total_downloads"] == 6 | |