Spaces:
Runtime error
Runtime error
File size: 13,237 Bytes
94ddfa7 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 | """
Comprehensive tests for the Deepfake Detection Service.
Tests cover: text analysis, rate limiting, response validation, and Redis integration.
"""
import asyncio
import pytest
from fastapi.testclient import TestClient
from unittest.mock import patch, AsyncMock, MagicMock
from app import app
from app.services.text_analyzer import analyze_text
from app.services.queue import get_queue_service
from app.models.schemas import TextAnalysisRequest, AnalysisResponse
from app.core.limiter import limiter
client = TestClient(app)
@pytest.fixture(autouse=True)
def reset_rate_limits():
"""
Automatyczny fixture, który przed KAŻDYM testem
czyści pamięć limitera zapytań SlowAPI.
Dzięki temu testy nie blokują się nawzajem błędem 429.
"""
limiter._storage.reset()
class TestTextAnalysis:
"""Test text deepfake analysis functionality."""
def test_health_check(self):
"""Test health check endpoint returns correct status."""
response = client.get("/")
assert response.status_code == 200
data = response.json()
assert data["status"] in ["ok", "degraded"]
assert data["service"] == "Deepfake Detection Service"
assert "available_models" in data
assert "text" in data["supported_types"]
assert "image" in data["supported_types"]
def test_text_analysis_valid_input(self):
"""Test text analysis with valid AI-generated text."""
payload = {
"content_type": "text",
"text": "This is an AI-generated text that demonstrates the capabilities of modern language models in creating coherent and contextually appropriate content without human intervention."
}
response = client.post("/analyze", json=payload)
assert response.status_code == 200
data = response.json()
# Validate response structure
assert "is_deepfake" in data
assert isinstance(data["is_deepfake"], bool)
assert "confidence" in data
assert 0.0 <= data["confidence"] <= 1.0
assert "analysis_time" in data
assert data["analysis_time"] > 0
assert "used_model" in data
assert data["content_type"] == "text"
assert "yaya36095/xlm-roberta-text-detector" in data["used_model"]
def test_text_analysis_human_written(self):
"""Test text analysis with human-written text."""
payload = {
"content_type": "text",
"text": "I went to the store yesterday and bought some groceries. The weather was nice, and I enjoyed the walk. I also met an old friend who I haven't seen in years. We talked about our lives and made plans to meet again soon."
}
response = client.post("/analyze", json=payload)
assert response.status_code == 200
data = response.json()
assert isinstance(data["is_deepfake"], bool)
assert 0.0 <= data["confidence"] <= 1.0
def test_text_analysis_too_short(self):
"""Test text analysis with text that's too short (< 50 chars)."""
payload = {
"content_type": "text",
"text": "Short text"
}
response = client.post("/analyze", json=payload)
assert response.status_code == 400
data = response.json()
assert "at least 50 characters" in data["detail"]
def test_text_analysis_too_long(self):
"""Test text analysis with text that exceeds max length."""
payload = {
"content_type": "text",
"text": "A" * 5001 # Exceeds 5000 character limit
}
response = client.post("/analyze", json=payload)
assert response.status_code == 400
data = response.json()
assert "exceeds maximum length" in data["detail"]
def test_text_analysis_exactly_50_chars(self):
"""Test text analysis with exactly 50 characters (minimum valid)."""
text_50_chars = "A" * 50
payload = {
"content_type": "text",
"text": text_50_chars
}
response = client.post("/analyze", json=payload)
# Should either succeed or fail based on model behavior
# but not because of length validation
assert response.status_code in [200, 500] # Success or model error, not validation error
def test_text_analysis_empty_text(self):
"""Test text analysis with empty text."""
payload = {
"content_type": "text",
"text": ""
}
response = client.post("/analyze", json=payload)
assert response.status_code == 400
def test_text_analysis_missing_field(self):
"""Test text analysis with missing text field."""
payload = {
"content_type": "text"
}
response = client.post("/analyze", json=payload)
assert response.status_code == 422 # Validation error
class TestRateLimiting:
"""Test rate limiting (slowapi) functionality."""
def test_rate_limit_single_request(self):
"""Test that a single request is allowed."""
payload = {
"content_type": "text",
"text": "This is a test text with sufficient length to pass validation and be analyzed by the deepfake detector model."
}
response = client.post("/analyze", json=payload)
assert response.status_code in [200, 500] # Should not be rate limited
assert response.status_code != 429
def test_rate_limit_multiple_rapid_requests(self):
"""Test that rapid requests are rate limited (1 per 5 seconds)."""
payload = {
"content_type": "text",
"text": "This is a test text with sufficient length to pass validation and be analyzed by the deepfake detector model."
}
# First request should succeed
response1 = client.post("/analyze", json=payload)
assert response1.status_code != 429
# Immediate second request should be rate limited
response2 = client.post("/analyze", json=payload)
assert response2.status_code == 429
assert "rate limit" in response2.text.lower()
def test_rate_limit_recovery_after_delay(self):
"""Test that rate limit recovers after 5 seconds."""
payload = {
"content_type": "text",
"text": "This is a test text with sufficient length to pass validation and be analyzed by the deepfake detector model."
}
# First request
response1 = client.post("/analyze", json=payload)
first_status = response1.status_code
# Wait for rate limit to reset (5+ seconds)
import time
time.sleep(5.1)
# Second request should now be allowed
response2 = client.post("/analyze", json=payload)
assert response2.status_code != 429
class TestResponseValidation:
"""Test response structure and validation."""
def test_response_includes_all_fields(self):
"""Test that response includes all required fields."""
payload = {
"content_type": "text",
"text": "This is a comprehensive test to ensure the response includes all necessary fields for proper API usage and data handling requirements."
}
response = client.post("/analyze", json=payload)
if response.status_code == 200:
data = response.json()
required_fields = ["is_deepfake", "confidence", "analysis_time", "used_model", "content_type"]
for field in required_fields:
assert field in data, f"Missing required field: {field}"
def test_response_confidence_range(self):
"""Test that confidence score is between 0.0 and 1.0."""
payload = {
"content_type": "text",
"text": "This is another test to verify that the confidence score is properly normalized between zero and one for consistent API behavior."
}
response = client.post("/analyze", json=payload)
if response.status_code == 200:
data = response.json()
assert 0.0 <= data["confidence"] <= 1.0
def test_response_analysis_time_positive(self):
"""Test that analysis_time is positive."""
payload = {
"content_type": "text",
"text": "Testing the analysis time tracking to ensure it records valid positive durations for performance monitoring purposes."
}
response = client.post("/analyze", json=payload)
if response.status_code == 200:
data = response.json()
assert data["analysis_time"] > 0
class TestRedisIntegration:
"""Test Redis queue integration."""
def test_queue_service_initialization(self):
"""Test that queue service initializes correctly."""
queue_service = get_queue_service()
assert queue_service is not None
def test_queue_service_singleton(self):
"""Test that queue service is a singleton."""
queue_service1 = get_queue_service()
queue_service2 = get_queue_service()
assert queue_service1 is queue_service2
@pytest.mark.asyncio
async def test_enqueue_analysis_task(self):
"""Test enqueuing an analysis task."""
queue_service = get_queue_service()
result = await queue_service.enqueue_analysis(
file_url="https://example.com/text.txt",
model="yaya36095/xlm-roberta-text-detector",
task_id="test_task_001"
)
assert result is True
@pytest.mark.asyncio
async def test_get_task_result(self):
"""Test retrieving task result from queue."""
queue_service = get_queue_service()
# Try to get a non-existent result
result = await queue_service.get_task_result("non_existent_task")
# Should return None for non-existent task
assert result is None
def test_redis_config_available(self):
"""Test that Redis config is available."""
from app.core.config import get_settings
settings = get_settings()
assert hasattr(settings, "REDIS_ENABLED")
assert hasattr(settings, "REDIS_URL")
assert hasattr(settings, "REDIS_QUEUE_NAME")
class TestAsyncTextAnalyzer:
"""Test async text analyzer directly."""
@pytest.mark.asyncio
async def test_analyze_text_valid_input(self):
"""Test analyze_text function with valid input."""
text = "This is a comprehensive test of the async text analyzer to ensure it properly processes input and returns valid results."
result = await analyze_text(text)
assert isinstance(result, dict)
assert "is_deepfake" in result
assert "confidence" in result
assert "analysis_time" in result
assert isinstance(result["is_deepfake"], bool)
assert isinstance(result["confidence"], float)
assert 0.0 <= result["confidence"] <= 1.0
@pytest.mark.asyncio
async def test_analyze_text_multiple_calls(self):
"""Test that analyze_text can be called multiple times (model caching)."""
text1 = "First test text that should be analyzed by the model to verify it works correctly on multiple invocations."
text2 = "Second test text to ensure the model remains loaded in memory for subsequent analysis operations."
result1 = await analyze_text(text1)
result2 = await analyze_text(text2)
assert result1 is not None
assert result2 is not None
assert "confidence" in result1
assert "confidence" in result2
class TestErrorHandling:
"""Test error handling in endpoints."""
def test_unsupported_content_type(self):
"""Test handling of unsupported content type."""
payload = {
"content_type": "unsupported_type",
"data": "some data"
}
response = client.post("/analyze", json=payload)
assert response.status_code in [415, 422] # Unsupported media type or validation error
def test_malformed_json(self):
"""Test handling of malformed JSON."""
response = client.post(
"/analyze",
content="not valid json",
headers={"Content-Type": "application/json"}
)
assert response.status_code == 422
def test_invalid_content_type_header(self):
"""Test handling of invalid Content-Type header."""
payload = {
"content_type": "text",
"text": "Valid test text with sufficient length to be properly analyzed and validated by the system."
}
response = client.post(
"/analyze",
json=payload,
headers={"Content-Type": "text/plain"}
)
# Should still work as FastAPI is lenient
assert response.status_code in [200, 422, 400, 415, 500]
if __name__ == "__main__":
pytest.main([__file__, "-v", "--tb=short"])
|