MusicUtils / tests /test_main.py
sathvik77's picture
Add pitch/tempo shifting on download
fd9421b
Raw
History Blame Contribute Delete
13.1 kB
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