1"""integration tests for track upload/delete using real API token. 2 3these tests require: 4- PLYRFM_API_TOKEN or PLYR_TOKEN env var 5- running backend (local or remote) 6- set PLYR_API_URL for non-local testing (default: http://localhost:8001) 7 8run with: uv run pytest tests/test_integration_upload.py -m integration -v 9""" 10 11import json 12import os 13import struct 14import tempfile 15from collections.abc import Generator 16from pathlib import Path 17 18import httpx 19import pytest 20 21API_URL = os.getenv("PLYR_API_URL", "http://localhost:8001") 22TOKEN = os.getenv("PLYR_TOKEN") or os.getenv("PLYRFM_API_TOKEN") 23 24 25def generate_wav_file(duration_seconds: float = 1.0, sample_rate: int = 44100) -> bytes: 26 """generate a minimal valid WAV file with silence.""" 27 num_channels = 1 28 bits_per_sample = 16 29 num_samples = int(sample_rate * duration_seconds) 30 data_size = num_samples * num_channels * (bits_per_sample // 8) 31 32 # WAV header 33 header = struct.pack( 34 "<4sI4s4sIHHIIHH4sI", 35 b"RIFF", 36 36 + data_size, # file size - 8 37 b"WAVE", 38 b"fmt ", 39 16, # fmt chunk size 40 1, # audio format (PCM) 41 num_channels, 42 sample_rate, 43 sample_rate * num_channels * (bits_per_sample // 8), # byte rate 44 num_channels * (bits_per_sample // 8), # block align 45 bits_per_sample, 46 b"data", 47 data_size, 48 ) 49 50 # silence (zeros) 51 audio_data = b"\x00" * data_size 52 53 return header + audio_data 54 55 56@pytest.fixture 57def test_audio_file() -> Generator[Path, None, None]: 58 """create a temporary test audio file.""" 59 wav_data = generate_wav_file(duration_seconds=1.0) 60 61 with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as f: 62 f.write(wav_data) 63 path = Path(f.name) 64 65 yield path 66 67 # cleanup 68 path.unlink(missing_ok=True) 69 70 71@pytest.mark.integration 72async def test_upload_and_delete_track(test_audio_file: Path): 73 """integration test: upload a track, wait for processing, then delete it.""" 74 if not TOKEN: 75 pytest.skip("PLYR_TOKEN or PLYRFM_API_TOKEN not set") 76 77 async with httpx.AsyncClient(timeout=120.0) as client: 78 # 1. verify auth works 79 auth_response = await client.get( 80 f"{API_URL}/auth/me", 81 headers={"Authorization": f"Bearer {TOKEN}"}, 82 ) 83 if auth_response.status_code == 401: 84 pytest.skip("token is invalid or expired") 85 assert auth_response.status_code == 200, f"auth failed: {auth_response.text}" 86 user = auth_response.json() 87 print(f"authenticated as: {user['handle']}") 88 89 # 2. upload track 90 with open(test_audio_file, "rb") as f: 91 files = {"file": ("test_integration.wav", f, "audio/wav")} 92 data = {"title": "Integration Test Track (DELETE ME)"} 93 94 upload_response = await client.post( 95 f"{API_URL}/tracks/", 96 headers={"Authorization": f"Bearer {TOKEN}"}, 97 files=files, 98 data=data, 99 ) 100 101 if upload_response.status_code == 403: 102 detail = upload_response.json().get("detail", "") 103 if "artist_profile_required" in detail: 104 pytest.skip("user needs artist profile setup") 105 if "scope_upgrade_required" in detail: 106 pytest.skip("token needs re-authorization with new scopes") 107 108 assert upload_response.status_code == 200, ( 109 f"upload failed: {upload_response.text}" 110 ) 111 112 upload_data = upload_response.json() 113 upload_id = upload_data["upload_id"] 114 print(f"upload started: {upload_id}") 115 116 # 3. poll for completion via SSE 117 track_id = None 118 async with client.stream( 119 "GET", 120 f"{API_URL}/tracks/uploads/{upload_id}/progress", 121 headers={"Authorization": f"Bearer {TOKEN}"}, 122 ) as response: 123 async for line in response.aiter_lines(): 124 if line.startswith("data: "): 125 data = json.loads(line[6:]) 126 status = data.get("status") 127 print(f" status: {status} - {data.get('message', '')}") 128 129 if status == "completed": 130 track_id = data.get("track_id") 131 print(f"upload complete! track_id: {track_id}") 132 break 133 elif status == "failed": 134 error = data.get("error", "unknown error") 135 pytest.fail(f"upload failed: {error}") 136 137 assert track_id is not None, "upload completed but no track_id returned" 138 139 # 4. verify track exists 140 track_response = await client.get(f"{API_URL}/tracks/{track_id}") 141 assert track_response.status_code == 200, ( 142 f"track not found: {track_response.text}" 143 ) 144 track = track_response.json() 145 print(f"track created: {track['title']} by {track['artist']['handle']}") 146 147 # 5. delete track 148 delete_response = await client.delete( 149 f"{API_URL}/tracks/{track_id}", 150 headers={"Authorization": f"Bearer {TOKEN}"}, 151 ) 152 assert delete_response.status_code == 200, ( 153 f"delete failed: {delete_response.text}" 154 ) 155 print(f"track {track_id} deleted successfully") 156 157 # 6. verify track is gone 158 verify_response = await client.get(f"{API_URL}/tracks/{track_id}") 159 assert verify_response.status_code == 404, "track should be deleted" 160 print("verified track no longer exists")