""" Testy pre AI Fact Checker aplikáciu Spustenie: python -m pytest test_app.py -v Alebo: python test_app.py """ import pytest import json import os import sys from datetime import datetime, timedelta # Nastavíme prostredie pre testovanie os.environ['TESTING'] = 'True' # Importujeme aplikáciu from app import app, load_model, MODELS from database import ( get_db_connection, get_cached_result, save_to_cache, get_history, get_stats, add_verified_fact, hash_claim, init_db ) # ============================================================================= # FIXTURES - Pomocné funkcie pre testy # ============================================================================= @pytest.fixture def client(): """Vytvorí testovacieho klienta pre Flask aplikáciu""" app.config['TESTING'] = True with app.test_client() as client: yield client @pytest.fixture def init_test_db(): """Inicializuje čistú databázu pre testy""" init_db() yield # Cleanup - vymažeme testovacie dáta conn = get_db_connection() cursor = conn.cursor() cursor.execute("DELETE FROM fact_checks") cursor.execute("DELETE FROM verified_facts") conn.commit() conn.close() # ============================================================================= # TESTY DATABÁZY # ============================================================================= class TestDatabase: """Testy pre databázové operácie""" def test_hash_claim_consistency(self): """Test: Hash toho istého výroku je vždy rovnaký""" claim = "Bratislava je hlavné mesto Slovenska" hash1 = hash_claim(claim) hash2 = hash_claim(claim) assert hash1 == hash2, "Hash by mal byť konzistentný" def test_hash_claim_case_insensitive(self): """Test: Hash je case-insensitive""" claim1 = "Bratislava je hlavné mesto Slovenska" claim2 = "bratislava je hlavné mesto slovenska" hash1 = hash_claim(claim1) hash2 = hash_claim(claim2) assert hash1 == hash2, "Hash by mal byť case-insensitive" def test_hash_claim_different_claims(self): """Test: Rôzne výroky majú rôzny hash""" claim1 = "Bratislava je hlavné mesto Slovenska" claim2 = "Košice sú druhé najväčšie mesto" hash1 = hash_claim(claim1) hash2 = hash_claim(claim2) assert hash1 != hash2, "Rôzne výroky by mali mať rôzny hash" def test_save_and_get_cached_result(self, init_test_db): """Test: Uloženie a získanie cachovaného výsledku""" claim = "Testovací výrok pre cache" result = { "verdict": "✅ Pravda", "confidence": 0.85, "nli_votes": {"entailment": 3, "contradiction": 1}, "evidence_for": ["Dôkaz 1"], "evidence_against": [], "sources": ["https://example.com"] } # Uložíme do cache saved = save_to_cache(claim, result, model_name="roberta") assert saved == True, "Uloženie do cache zlyhalo" # Získame z cache cached = get_cached_result(claim) assert cached is not None, "Cache by nemala byť prázdna" assert cached["verdict"] == "✅ Pravda" assert cached["cached"] == True assert cached["model_name"] == "roberta" def test_cache_miss(self, init_test_db): """Test: Získanie neexistujúceho cachovaného výsledku""" claim = "Tento výrok ešte nebol overený" cached = get_cached_result(claim) assert cached is None, "Cache by mala byť prázdna pre nový výrok" def test_get_history(self, init_test_db): """Test: Získanie histórie overení""" # Pridáme niekoľko záznamov for i in range(5): claim = f"Testovací výrok {i}" result = { "verdict": "✅ Pravda" if i % 2 == 0 else "❌ Nepravda", "sources": [f"https://example{i}.com"] } save_to_cache(claim, result, model_name="roberta") history = get_history(limit=10) assert len(history) == 5, "História by mala obsahovať 5 záznamov" def test_get_stats(self, init_test_db): """Test: Získanie štatistík""" # Pridáme testovacie dáta for i in range(3): claim = f"Štatistický výrok {i}" result = {"verdict": "✅ Pravda", "sources": []} save_to_cache(claim, result, model_name="roberta") # Pridáme manuálne overený fakt add_verified_fact("Manuálne overený fakt", "TRUE", "Vysvetlenie") stats = get_stats() assert stats["unique_claims"] == 3 assert stats["verified_facts"] == 1 assert stats["total_checks"] >= 3 def test_add_verified_fact_duplicate(self, init_test_db): """Test: Pridanie duplicitného manuálne overeného faktu""" claim = "Duplicitný fakt" result1 = add_verified_fact(claim, "TRUE", "Prvé vysvetlenie") result2 = add_verified_fact(claim, "FALSE", "Druhé vysvetlenie") assert result1 == True, "Prvý fakt by sa mal pridať" assert result2 == False, "Duplicitný fakt by sa nemal pridať" # ============================================================================= # TESTY API ENDPOINTOV # ============================================================================= class TestAPIEndpoints: """Testy pre REST API endpointy""" def test_empty_claim_validation(self, client): """Test: Validácia prázdneho výroku""" response = client.post('/api/check', data=json.dumps({"claim": ""}), content_type='application/json') assert response.status_code == 400 data = json.loads(response.data) assert "error" in data def test_whitespace_only_claim(self, client): """Test: Validácia výroku s iba medzerami""" response = client.post('/api/check', data=json.dumps({"claim": " "}), content_type='application/json') assert response.status_code == 400 def test_missing_claim_field(self, client): """Test: Chýbajúce pole claim""" response = client.post('/api/check', data=json.dumps({"language": "sk"}), content_type='application/json') assert response.status_code == 400 def test_history_endpoint_empty(self, client, init_test_db): """Test: História keď je prázdna""" response = client.get('/api/history') assert response.status_code == 200 data = json.loads(response.data) assert "history" in data assert "count" in data assert data["count"] == 0 def test_history_endpoint_with_data(self, client, init_test_db): """Test: História s dátami""" # Pridáme dáta cez cache for i in range(3): claim = f"História test {i}" result = {"verdict": "✅ Pravda", "sources": []} save_to_cache(claim, result, model_name="roberta") response = client.get('/api/history?limit=10') assert response.status_code == 200 data = json.loads(response.data) assert data["count"] == 3 def test_stats_endpoint(self, client): """Test: Štatistiky endpoint""" response = client.get('/api/stats') assert response.status_code == 200 data = json.loads(response.data) assert "unique_claims" in data assert "total_checks" in data assert "verified_facts" in data def test_admin_add_fact_success(self, client, init_test_db): """Test: Pridanie manuálne overeného faktu (admin)""" response = client.post('/api/admin/add-fact', data=json.dumps({ "claim": "Manuálne pridaný fakt", "verdict": "TRUE", "explanation": "Toto je vysvetlenie", "source_url": "https://overenie.sk" }), content_type='application/json') assert response.status_code == 200 data = json.loads(response.data) assert "message" in data assert data["message"] == "Overený fakt pridaný" def test_admin_add_fact_missing_fields(self, client): """Test: Chýbajúce povinné polia pre admin endpoint""" response = client.post('/api/admin/add-fact', data=json.dumps({ "claim": "Neúplný fakt" # Chýba verdict }), content_type='application/json') assert response.status_code == 400 def test_admin_add_fact_duplicate(self, client, init_test_db): """Test: Pridanie duplicitného faktu""" # Prvýkrát úspech response1 = client.post('/api/admin/add-fact', data=json.dumps({ "claim": "Duplicitný admin fakt", "verdict": "TRUE" }), content_type='application/json') assert response1.status_code == 200 # Druhýkrát chyba (duplicate) response2 = client.post('/api/admin/add-fact', data=json.dumps({ "claim": "Duplicitný admin fakt", "verdict": "FALSE" }), content_type='application/json') assert response2.status_code == 409 # ============================================================================= # TESTY VALIDÁCIE VSTUPU # ============================================================================= class TestInputValidation: """Testy pre validáciu vstupov a filtre""" def test_forbidden_word_politician(self, client): """Test: Zakázané slová - politici""" response = client.post('/api/check', data=json.dumps({ "claim": "Fico je politik" }), content_type='application/json') assert response.status_code == 400 data = json.loads(response.data) assert "forbidden_words" in data def test_forbidden_word_vulgar(self, client): """Test: Zakázané slová - vulgárnosti""" response = client.post('/api/check', data=json.dumps({ "claim": "Toto je kokotina" }), content_type='application/json') assert response.status_code == 400 def test_forbidden_word_covid(self, client): """Test: Zakázané slová - citlivé témy""" response = client.post('/api/check', data=json.dumps({ "claim": "Vakcína spôsobuje neplodnosť" }), content_type='application/json') assert response.status_code == 400 def test_forbidden_word_english(self, client): """Test: Zakázané slová - anglické výrazy""" response = client.post('/api/check', data=json.dumps({ "claim": "This is bullshit" }), content_type='application/json') assert response.status_code == 400 def test_clean_claim_passes(self, client): """Test: Čistý výrok prejde""" # Tento test vyžaduje funkčný SerpAPI kľúč # Ak nie je nastavený, vráti 500 if not os.getenv('SERPAPI_API_KEY'): pytest.skip("SERPAPI_API_KEY nie je nastavený") response = client.post('/api/check', data=json.dumps({ "claim": "Bratislava je hlavné mesto Slovenska" }), content_type='application/json') # Môže byť 200 (úspech) alebo 429 (limit API) assert response.status_code in [200, 429] # ============================================================================= # TESTY MODELOV # ============================================================================= class TestModels: """Testy pre AI modely""" def test_model_config_exists(self): """Test: Konfigurácia modelov existuje""" assert "roberta" in MODELS assert "mdeberta" in MODELS def test_roberta_config(self): """Test: RoBERTa konfigurácia""" config = MODELS["roberta"] assert config["needs_translation"] == True assert "roberta" in config["name"].lower() def test_mdeberta_config(self): """Test: mDeBERTa konfigurácia""" config = MODELS["mdeberta"] assert config["needs_translation"] == False assert "mdeberta" in config["name"].lower() or "DeBERTa" in config["name"] def test_model_loading(self): """Test: Načítanie modelu""" # Testujeme že funkcia load_model existuje a nespôsobí chybu try: load_model("roberta") assert True except Exception as e: pytest.fail(f"Načítanie modelu zlyhalo: {e}") # ============================================================================= # TESTY CACHE LOGIKY # ============================================================================= class TestCacheLogic: """Testy pre cachovaciu logiku""" def test_cache_increments_count(self, init_test_db): """Test: Cache inkrementuje počet overení""" claim = "Inkrementácia test" result = {"verdict": "✅ Pravda", "sources": []} # Prvé uloženie save_to_cache(claim, result, model_name="roberta") cached1 = get_cached_result(claim) count1 = cached1["check_count"] # Druhé získanie (inkrementuje) cached2 = get_cached_result(claim) count2 = cached2["check_count"] assert count2 == count1 + 1, "Počet overení by sa mal inkrementovať" def test_cache_updates_timestamp(self, init_test_db): """Test: Cache aktualizuje časovú pečiatku""" claim = "Timestamp test" result = {"verdict": "✅ Pravda", "sources": []} save_to_cache(claim, result, model_name="roberta") cached1 = get_cached_result(claim) timestamp1 = cached1["checked_at"] import time time.sleep(1) # Počkáme sekundu cached2 = get_cached_result(claim) timestamp2 = cached2["checked_at"] # Timestamp by mal byť novší assert timestamp2 >= timestamp1 def test_cache_serialization(self, init_test_db): """Test: Serializácia JSON polí v cache""" claim = "JSON serializácia test" result = { "verdict": "✅ Pravda", "nli_votes": {"entailment": 0.8, "contradiction": 0.2}, "evidence_for": [{"text": "Dôkaz 1", "confidence": 0.9}], "evidence_against": [], "sources": [{"url": "https://example.com", "label": "entailment"}] } save_to_cache(claim, result, model_name="mdeberta") cached = get_cached_result(claim) assert isinstance(cached["nli_votes"], dict) assert isinstance(cached["evidence_for"], list) assert isinstance(cached["evidence_against"], list) # ============================================================================= # INTEGRÁCNE TESTY # ============================================================================= class TestIntegration: """Integračné testy celého systému""" def test_full_request_cycle(self, client, init_test_db): """Test: Celý cyklus požiadavky (bez SerpAPI)""" # Testujeme validáciu a štruktúru odpovede # Bez SerpAPI kľúča očakávame chybu 500 response = client.post('/api/check', data=json.dumps({ "claim": "Testovací výrok", "language": "sk", "model": "roberta" }), content_type='application/json') # Bez SerpAPI: 500, so SerpAPI: 200 alebo 429 if os.getenv('SERPAPI_API_KEY'): assert response.status_code in [200, 429] else: assert response.status_code == 500 def test_model_switching(self, client): """Test: Prepínanie medzi modelmi""" # Overíme že endpoint akceptuje parameter model response = client.post('/api/check', data=json.dumps({ "claim": "", # Prázdne pre rýchly test "model": "mdeberta" }), content_type='application/json') # Očakávame 400 (validation error) nie 500 (model error) assert response.status_code == 400 def test_verified_fact_overrides_cache(self, client, init_test_db): """Test: Manuálne overený fakt má prioritu pred cache""" claim = "Prioritný fakt" # Najprv pridáme manuálne overený fakt add_verified_fact(claim, "TRUE", "Oficiálne overené", "https://overenie.sk") # Potom pridáme do cache iný výsledok cache_result = { "verdict": "❌ Nepravda", "sources": ["https://ine-zdroj.sk"] } save_to_cache(claim, cache_result, model_name="roberta") # Získanie by malo vrátiť manuálne overený fakt cached = get_cached_result(claim) assert cached is not None assert cached["verified"] == True assert "Overené" in cached["verdict"] # ============================================================================= # HRANIČNÉ PRÍPADY # ============================================================================= class TestEdgeCases: """Testy hraničných prípadov""" def test_very_long_claim(self, client): """Test: Veľmi dlhý výrok""" long_claim = "A" * 10000 # 10k znakov response = client.post('/api/check', data=json.dumps({"claim": long_claim}), content_type='application/json') # Aplikácia by mala zvládnuť dlhý vstup (vráti "nedostatok zdrojov" alebo spracuje) assert response.status_code == 200 # Očakávame úspešné spracovanie data = json.loads(response.data) assert "verdict" in data # Mala by vrátiť verdikt def test_unicode_claim(self, client): """Test: Výrok s Unicode znakmi""" unicode_claim = "🌍 je guľatá 🌍" response = client.post('/api/check', data=json.dumps({"claim": unicode_claim}), content_type='application/json') # Aplikácia by mala správne spracovať Unicode (emoji, diakritika) assert response.status_code == 200 data = json.loads(response.data) assert "verdict" in data # Mala by vrátiť verdikt def test_sql_injection_attempt(self, client): """Test: Pokus o SQL injection""" malicious_claim = "'; DROP TABLE fact_checks; --" response = client.post('/api/check', data=json.dumps({"claim": malicious_claim}), content_type='application/json') # Aplikácia by mala bezpečne spracovať vstup (parametrované dotazy) assert response.status_code == 200 # Nemalo by to zhodiť server # Overíme že databáza stále existuje from database import get_stats stats = get_stats() # Ak toto prejde, SQL injection zlyhal assert stats is not None def test_special_characters_claim(self, client): """Test: Výrok so špeciálnymi znakmi""" special_claim = "Cena je 100€ (zľava 50%!) " response = client.post('/api/check', data=json.dumps({"claim": special_claim}), content_type='application/json') assert response.status_code in [400, 500, 429] def test_empty_json_body(self, client): """Test: Prázdne JSON telo""" response = client.post('/api/check', data=json.dumps({}), content_type='application/json') assert response.status_code == 400 def test_invalid_json(self, client): """Test: Neplatné JSON""" response = client.post('/api/check', data="nie je json", content_type='application/json') assert response.status_code == 400 def test_history_limit_zero(self, client, init_test_db): """Test: História s limitom 0""" # Pridáme dáta save_to_cache("Test", {"verdict": "✅", "sources": []}, "roberta") response = client.get('/api/history?limit=0') data = json.loads(response.data) assert data["count"] == 0 assert len(data["history"]) == 0 def test_history_large_limit(self, client, init_test_db): """Test: História s veľkým limitom""" response = client.get('/api/history?limit=999999') assert response.status_code == 200 # ============================================================================= # SPUSTENIE TESTOV # ============================================================================= if __name__ == '__main__': # Spustenie priamym zavolaním: python test_app.py pytest.main([__file__, '-v', '--tb=short'])