Spaces:
Running
Running
fix: update analysis and commend endpoints with additional tests and default values
Browse files- app/routes/analyze.py +3 -3
- tests/test_analyze.py +104 -0
- tests/test_commend.py +36 -1
- tests/test_data_processing.py +40 -0
app/routes/analyze.py
CHANGED
|
@@ -32,7 +32,7 @@ class AnalysisFeatures(BaseModel):
|
|
| 32 |
spectralRegularity: float = 0.5
|
| 33 |
temporalPatterns: float = 0.5
|
| 34 |
harmonicStructure: float = 0.5
|
| 35 |
-
artificialIndicators: List[str] =
|
| 36 |
|
| 37 |
|
| 38 |
class AudioInfo(BaseModel):
|
|
@@ -62,8 +62,8 @@ class AnalyzeResponse(BaseModel):
|
|
| 62 |
"""Response model for analysis endpoint."""
|
| 63 |
|
| 64 |
result: Optional[AnalysisResult] = None
|
| 65 |
-
warnings: List[str] =
|
| 66 |
-
errors: List[str] =
|
| 67 |
|
| 68 |
|
| 69 |
@router.post("/api/analyze", response_model=AnalyzeResponse)
|
|
|
|
| 32 |
spectralRegularity: float = 0.5
|
| 33 |
temporalPatterns: float = 0.5
|
| 34 |
harmonicStructure: float = 0.5
|
| 35 |
+
artificialIndicators: List[str] = Field(default_factory=list)
|
| 36 |
|
| 37 |
|
| 38 |
class AudioInfo(BaseModel):
|
|
|
|
| 62 |
"""Response model for analysis endpoint."""
|
| 63 |
|
| 64 |
result: Optional[AnalysisResult] = None
|
| 65 |
+
warnings: List[str] = Field(default_factory=list)
|
| 66 |
+
errors: List[str] = Field(default_factory=list)
|
| 67 |
|
| 68 |
|
| 69 |
@router.post("/api/analyze", response_model=AnalyzeResponse)
|
tests/test_analyze.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Tests for /api/analyze endpoint."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import io
|
| 6 |
+
|
| 7 |
+
from fastapi.testclient import TestClient
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def test_analyze_missing_source_type(client: TestClient) -> None:
|
| 11 |
+
"""Analyze should reject a request without sourceType."""
|
| 12 |
+
response = client.post("/api/analyze")
|
| 13 |
+
assert response.status_code == 422
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def test_analyze_invalid_source_type(client: TestClient) -> None:
|
| 17 |
+
"""Analyze should return error for unknown sourceType."""
|
| 18 |
+
response = client.post(
|
| 19 |
+
"/api/analyze",
|
| 20 |
+
data={"sourceType": "unknown"},
|
| 21 |
+
)
|
| 22 |
+
assert response.status_code == 200
|
| 23 |
+
data = response.json()
|
| 24 |
+
assert "invalid_source_type" in data["errors"]
|
| 25 |
+
assert data["result"] is None
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def test_analyze_unsupported_source(client: TestClient) -> None:
|
| 29 |
+
"""Analyze should return unsupported_source for spotify/apple."""
|
| 30 |
+
for source in ("spotify", "apple"):
|
| 31 |
+
response = client.post(
|
| 32 |
+
"/api/analyze",
|
| 33 |
+
data={"sourceType": source},
|
| 34 |
+
)
|
| 35 |
+
assert response.status_code == 200
|
| 36 |
+
data = response.json()
|
| 37 |
+
assert "unsupported_source" in data["errors"]
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def test_analyze_youtube_missing_url(client: TestClient) -> None:
|
| 41 |
+
"""Analyze youtube without url should return missing_url error."""
|
| 42 |
+
response = client.post(
|
| 43 |
+
"/api/analyze",
|
| 44 |
+
data={"sourceType": "youtube"},
|
| 45 |
+
)
|
| 46 |
+
assert response.status_code == 200
|
| 47 |
+
data = response.json()
|
| 48 |
+
assert "missing_url" in data["errors"]
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def test_analyze_file_missing_file(client: TestClient) -> None:
|
| 52 |
+
"""Analyze file without upload should return missing_file error."""
|
| 53 |
+
response = client.post(
|
| 54 |
+
"/api/analyze",
|
| 55 |
+
data={"sourceType": "file"},
|
| 56 |
+
)
|
| 57 |
+
assert response.status_code == 200
|
| 58 |
+
data = response.json()
|
| 59 |
+
assert "missing_file" in data["errors"]
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def test_analyze_file_invalid_type(client: TestClient) -> None:
|
| 63 |
+
"""Analyze should reject non-audio file."""
|
| 64 |
+
fake_file = io.BytesIO(b"not audio content " * 100)
|
| 65 |
+
response = client.post(
|
| 66 |
+
"/api/analyze",
|
| 67 |
+
data={"sourceType": "file"},
|
| 68 |
+
files={"file": ("test.txt", fake_file, "text/plain")},
|
| 69 |
+
)
|
| 70 |
+
assert response.status_code == 200
|
| 71 |
+
data = response.json()
|
| 72 |
+
assert "invalid_file_type" in data["errors"]
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def test_analyze_file_too_small(client: TestClient) -> None:
|
| 76 |
+
"""Analyze should reject files smaller than 1KB."""
|
| 77 |
+
tiny_file = io.BytesIO(b"\x00" * 100)
|
| 78 |
+
response = client.post(
|
| 79 |
+
"/api/analyze",
|
| 80 |
+
data={"sourceType": "file"},
|
| 81 |
+
files={"file": ("tiny.wav", tiny_file, "audio/wav")},
|
| 82 |
+
)
|
| 83 |
+
assert response.status_code == 200
|
| 84 |
+
data = response.json()
|
| 85 |
+
assert "file_too_small" in data["errors"]
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def test_analyze_file_happy_path(client: TestClient) -> None:
|
| 89 |
+
"""Analyze with a valid audio file should return a preview result."""
|
| 90 |
+
audio_content = b"\x00" * 2048
|
| 91 |
+
fake_audio = io.BytesIO(audio_content)
|
| 92 |
+
response = client.post(
|
| 93 |
+
"/api/analyze",
|
| 94 |
+
data={"sourceType": "file"},
|
| 95 |
+
files={"file": ("test.wav", fake_audio, "audio/wav")},
|
| 96 |
+
)
|
| 97 |
+
assert response.status_code == 200
|
| 98 |
+
data = response.json()
|
| 99 |
+
assert data["result"] is not None
|
| 100 |
+
result = data["result"]
|
| 101 |
+
assert "isAIGenerated" in result
|
| 102 |
+
assert "confidence" in result
|
| 103 |
+
assert result["analysisMode"] == "preview"
|
| 104 |
+
assert result["decisionSource"] == "preview"
|
tests/test_commend.py
CHANGED
|
@@ -15,6 +15,15 @@ def test_commend_health(client: TestClient) -> None:
|
|
| 15 |
assert "youtubeConfigured" in data
|
| 16 |
|
| 17 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
def test_commend_generate_rejects_invalid_url(client: TestClient) -> None:
|
| 19 |
"""Generate endpoint should reject a non-YouTube URL."""
|
| 20 |
response = client.post(
|
|
@@ -25,4 +34,30 @@ def test_commend_generate_rejects_invalid_url(client: TestClient) -> None:
|
|
| 25 |
"style": "supportive",
|
| 26 |
},
|
| 27 |
)
|
| 28 |
-
assert response.status_code in (400, 422)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
assert "youtubeConfigured" in data
|
| 16 |
|
| 17 |
|
| 18 |
+
def test_commend_styles(client: TestClient) -> None:
|
| 19 |
+
"""Styles endpoint should return available comment styles."""
|
| 20 |
+
response = client.get("/api/commend/styles")
|
| 21 |
+
assert response.status_code == 200
|
| 22 |
+
data = response.json()
|
| 23 |
+
assert "styles" in data
|
| 24 |
+
assert len(data["styles"]) > 0
|
| 25 |
+
|
| 26 |
+
|
| 27 |
def test_commend_generate_rejects_invalid_url(client: TestClient) -> None:
|
| 28 |
"""Generate endpoint should reject a non-YouTube URL."""
|
| 29 |
response = client.post(
|
|
|
|
| 34 |
"style": "supportive",
|
| 35 |
},
|
| 36 |
)
|
| 37 |
+
assert response.status_code in (400, 401, 422)
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def test_commend_generate_rejects_empty_url(client: TestClient) -> None:
|
| 41 |
+
"""Generate endpoint should reject empty URL."""
|
| 42 |
+
response = client.post(
|
| 43 |
+
"/api/commend/generate",
|
| 44 |
+
json={
|
| 45 |
+
"url": "",
|
| 46 |
+
"language": "en",
|
| 47 |
+
"style": "supportive",
|
| 48 |
+
},
|
| 49 |
+
)
|
| 50 |
+
assert response.status_code in (400, 401, 422)
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def test_commend_post_disabled_by_default(client: TestClient) -> None:
|
| 54 |
+
"""Post endpoint should be disabled by default (feature flag)."""
|
| 55 |
+
response = client.post(
|
| 56 |
+
"/api/commend/post",
|
| 57 |
+
json={
|
| 58 |
+
"videoId": "dQw4w9WgXcQ",
|
| 59 |
+
"comment": "Test comment",
|
| 60 |
+
},
|
| 61 |
+
)
|
| 62 |
+
# Either 401 (auth) or 403 (feature flag disabled)
|
| 63 |
+
assert response.status_code in (401, 403, 422)
|
tests/test_data_processing.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Tests for /api/process/audio endpoint."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import io
|
| 6 |
+
|
| 7 |
+
from fastapi.testclient import TestClient
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def test_audio_rejects_non_audio(client: TestClient) -> None:
|
| 11 |
+
"""Should reject non-audio content type."""
|
| 12 |
+
fake = io.BytesIO(b"not audio")
|
| 13 |
+
response = client.post(
|
| 14 |
+
"/api/process/audio",
|
| 15 |
+
data={"options": '{"pitch_shift": 0}'},
|
| 16 |
+
files={"file": ("test.txt", fake, "text/plain")},
|
| 17 |
+
)
|
| 18 |
+
assert response.status_code == 400
|
| 19 |
+
assert "audio" in response.json()["detail"].lower()
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def test_audio_rejects_missing_content_type(client: TestClient) -> None:
|
| 23 |
+
"""Should reject file with no content type."""
|
| 24 |
+
fake = io.BytesIO(b"some bytes")
|
| 25 |
+
response = client.post(
|
| 26 |
+
"/api/process/audio",
|
| 27 |
+
data={"options": '{"pitch_shift": 0}'},
|
| 28 |
+
files={"file": ("test.bin", fake, "")},
|
| 29 |
+
)
|
| 30 |
+
# Empty content_type should be treated as invalid
|
| 31 |
+
assert response.status_code == 400
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def test_audio_rejects_missing_file(client: TestClient) -> None:
|
| 35 |
+
"""Should reject request without file."""
|
| 36 |
+
response = client.post(
|
| 37 |
+
"/api/process/audio",
|
| 38 |
+
data={"options": '{"pitch_shift": 0}'},
|
| 39 |
+
)
|
| 40 |
+
assert response.status_code == 422
|