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"])