""" Tests de comparaison v1/v2. Ces tests vérifient que les données retournées par l'API v2 sont cohérentes avec les calculs effectués par la v1. IMPORTANT: Les deux applications utilisent la même base de données SQLite. """ import pytest import httpx from sqlalchemy import text from sqlalchemy.orm import Session # URLs des APIs V2_BASE_URL = "http://localhost:8000/api/v2" # Import du moteur sync pour les requêtes directes import sys sys.path.insert(0, "/home/commun/scripts/notytex/notytex-v2/backend") from infrastructure.database.session import sync_engine class TestDataCounts: """Tests de cohérence des comptages.""" def test_classes_count(self): """Vérifie que le nombre de classes correspond.""" # Requête directe à la DB with sync_engine.connect() as conn: result = conn.execute(text("SELECT COUNT(*) FROM class_group")) db_count = result.scalar() # Requête v2 API response = httpx.get(f"{V2_BASE_URL}/classes") assert response.status_code == 200 api_data = response.json() assert api_data["total"] == db_count def test_students_count(self): """Vérifie que le nombre d'étudiants correspond.""" with sync_engine.connect() as conn: result = conn.execute(text("SELECT COUNT(*) FROM student")) db_count = result.scalar() response = httpx.get(f"{V2_BASE_URL}/students") assert response.status_code == 200 api_data = response.json() assert api_data["total"] == db_count def test_assessments_count(self): """Vérifie que le nombre d'évaluations correspond.""" with sync_engine.connect() as conn: result = conn.execute(text("SELECT COUNT(*) FROM assessment")) db_count = result.scalar() response = httpx.get(f"{V2_BASE_URL}/assessments") assert response.status_code == 200 api_data = response.json() assert api_data["total"] == db_count class TestStudentsPerClass: """Tests de cohérence des élèves par classe.""" def test_each_class_student_count(self): """Vérifie le nombre d'élèves dans chaque classe.""" # Récupérer toutes les classes response = httpx.get(f"{V2_BASE_URL}/classes") classes = response.json()["classes"] for cls in classes: # Compter les élèves via DB (inscriptions actives) with sync_engine.connect() as conn: result = conn.execute(text(""" SELECT COUNT(*) FROM student_enrollments WHERE class_group_id = :class_id AND departure_date IS NULL """), {"class_id": cls["id"]}) db_count = result.scalar() # Comparer avec l'API assert cls["students_count"] == db_count, \ f"Classe {cls['name']}: attendu {db_count}, reçu {cls['students_count']}" class TestAssessmentProgress: """Tests de cohérence du calcul de progression.""" def test_completed_assessment_progress(self): """Vérifie la progression d'une évaluation 100% corrigée.""" # Récupérer les évaluations complétées response = httpx.get(f"{V2_BASE_URL}/assessments") assessments = response.json()["assessments"] completed = [a for a in assessments if a["grading_progress"]["status"] == "completed"] if not completed: pytest.skip("Aucune évaluation complétée trouvée") for assessment in completed[:3]: # Tester les 3 premières # Vérifier via DB assessment_id = assessment["id"] with sync_engine.connect() as conn: # Compter les éléments de notation elements = conn.execute(text(""" SELECT COUNT(*) FROM grading_element ge JOIN exercise e ON ge.exercise_id = e.id WHERE e.assessment_id = :aid """), {"aid": assessment_id}).scalar() # Compter les notes saisies grades = conn.execute(text(""" SELECT COUNT(*) FROM grade g JOIN grading_element ge ON g.grading_element_id = ge.id JOIN exercise e ON ge.exercise_id = e.id WHERE e.assessment_id = :aid AND g.value IS NOT NULL """), {"aid": assessment_id}).scalar() # Compter les élèves éligibles assessment_info = conn.execute(text(""" SELECT class_group_id, date FROM assessment WHERE id = :aid """), {"aid": assessment_id}).fetchone() eligible = conn.execute(text(""" SELECT COUNT(*) FROM student_enrollments WHERE class_group_id = :cid AND enrollment_date <= :date AND (departure_date IS NULL OR departure_date >= :date) """), { "cid": assessment_info[0], "date": assessment_info[1] }).scalar() expected_total = elements * eligible expected_completed = grades progress = assessment["grading_progress"] assert progress["total"] == expected_total, \ f"Assessment {assessment_id}: total attendu {expected_total}, reçu {progress['total']}" assert progress["completed"] == expected_completed, \ f"Assessment {assessment_id}: complété attendu {expected_completed}, reçu {progress['completed']}" if expected_total > 0: expected_pct = round((expected_completed / expected_total) * 100) assert progress["percentage"] == expected_pct, \ f"Assessment {assessment_id}: % attendu {expected_pct}, reçu {progress['percentage']}" class TestAssessmentResults: """Tests de cohérence des résultats d'évaluation.""" def test_results_statistics(self): """Vérifie les statistiques des résultats.""" # Trouver une évaluation complétée response = httpx.get(f"{V2_BASE_URL}/assessments") assessments = response.json()["assessments"] completed = [a for a in assessments if a["grading_progress"]["status"] == "completed"] if not completed: pytest.skip("Aucune évaluation complétée trouvée") assessment_id = completed[0]["id"] # Récupérer les résultats response = httpx.get(f"{V2_BASE_URL}/assessments/{assessment_id}/results") assert response.status_code == 200 results = response.json() stats = results["statistics"] scores = [s["total_score"] for s in results["students_scores"] if s["total_max_points"] > 0] # Vérifier le nombre d'élèves assert stats["count"] == len(scores) # Vérifier les bornes if scores: assert stats["min"] == round(min(scores), 2) assert stats["max"] == round(max(scores), 2) # Vérifier la moyenne import statistics expected_mean = round(statistics.mean(scores), 2) assert abs(stats["mean"] - expected_mean) < 0.1, \ f"Moyenne attendue {expected_mean}, reçue {stats['mean']}" def test_student_scores_sorted_alphabetically(self): """Vérifie que les scores sont triés par ordre alphabétique.""" response = httpx.get(f"{V2_BASE_URL}/assessments") assessments = response.json()["assessments"] completed = [a for a in assessments if a["grading_progress"]["status"] == "completed"] if not completed: pytest.skip("Aucune évaluation complétée trouvée") assessment_id = completed[0]["id"] response = httpx.get(f"{V2_BASE_URL}/assessments/{assessment_id}/results") results = response.json() names = [s["student_name"].lower() for s in results["students_scores"]] assert names == sorted(names), "Les scores ne sont pas triés alphabétiquement" class TestConfigConsistency: """Tests de cohérence de la configuration.""" def test_competences_count(self): """Vérifie le nombre de compétences.""" with sync_engine.connect() as conn: result = conn.execute(text("SELECT COUNT(*) FROM competences")) db_count = result.scalar() response = httpx.get(f"{V2_BASE_URL}/config/competences") api_data = response.json() assert len(api_data["competences"]) == db_count def test_domains_count(self): """Vérifie le nombre de domaines.""" with sync_engine.connect() as conn: result = conn.execute(text("SELECT COUNT(*) FROM domains")) db_count = result.scalar() response = httpx.get(f"{V2_BASE_URL}/config/domains") api_data = response.json() assert len(api_data["domains"]) == db_count def test_scale_values_count(self): """Vérifie le nombre de valeurs d'échelle.""" with sync_engine.connect() as conn: result = conn.execute(text("SELECT COUNT(*) FROM competence_scale_values")) db_count = result.scalar() response = httpx.get(f"{V2_BASE_URL}/config/scale") api_data = response.json() assert len(api_data["values"]) == db_count class TestFilteringAndSorting: """Tests des fonctionnalités de filtrage et tri.""" def test_filter_assessments_by_trimester(self): """Vérifie le filtrage par trimestre.""" for trimester in [1, 2, 3]: # Compter via DB with sync_engine.connect() as conn: result = conn.execute(text( "SELECT COUNT(*) FROM assessment WHERE trimester = :t" ), {"t": trimester}) db_count = result.scalar() # Comparer avec l'API response = httpx.get(f"{V2_BASE_URL}/assessments?trimester={trimester}") api_data = response.json() assert api_data["total"] == db_count, \ f"Trimestre {trimester}: attendu {db_count}, reçu {api_data['total']}" def test_filter_assessments_by_class(self): """Vérifie le filtrage par classe.""" # Récupérer toutes les classes response = httpx.get(f"{V2_BASE_URL}/classes") classes = response.json()["classes"] for cls in classes[:3]: # Tester les 3 premières # Compter via DB with sync_engine.connect() as conn: result = conn.execute(text( "SELECT COUNT(*) FROM assessment WHERE class_group_id = :cid" ), {"cid": cls["id"]}) db_count = result.scalar() # Comparer avec l'API response = httpx.get(f"{V2_BASE_URL}/assessments?class_id={cls['id']}") api_data = response.json() assert api_data["total"] == db_count, \ f"Classe {cls['name']}: attendu {db_count}, reçu {api_data['total']}" def test_search_students(self): """Vérifie la recherche d'étudiants.""" search_term = "martin" # Rechercher via DB with sync_engine.connect() as conn: result = conn.execute(text(""" SELECT COUNT(*) FROM student WHERE LOWER(last_name) LIKE :term OR LOWER(first_name) LIKE :term """), {"term": f"%{search_term}%"}) db_count = result.scalar() # Comparer avec l'API response = httpx.get(f"{V2_BASE_URL}/students?search={search_term}") api_data = response.json() assert api_data["total"] == db_count, \ f"Recherche '{search_term}': attendu {db_count}, reçu {api_data['total']}" class TestErrorHandling: """Tests de gestion des erreurs.""" def test_class_not_found(self): """Vérifie le 404 pour une classe inexistante.""" response = httpx.get(f"{V2_BASE_URL}/classes/99999") assert response.status_code == 404 assert "non trouvée" in response.json()["detail"] def test_student_not_found(self): """Vérifie le 404 pour un étudiant inexistant.""" response = httpx.get(f"{V2_BASE_URL}/students/99999") assert response.status_code == 404 assert "non trouvé" in response.json()["detail"] def test_assessment_not_found(self): """Vérifie le 404 pour une évaluation inexistante.""" response = httpx.get(f"{V2_BASE_URL}/assessments/99999") assert response.status_code == 404 assert "non trouvée" in response.json()["detail"] if __name__ == "__main__": pytest.main([__file__, "-v"])