Files
notytex/backend/tests/comparison/test_v1_v2_parity.py
Bertrand Benjamin 2b08eb534a Migration v1 (Flask) -> v2 (FastAPI + Vue.js) complétée
 Changements majeurs:
- Suppression complète du code Flask legacy
- Migration backend FastAPI vers racine /backend
- Migration frontend Vue.js vers racine /frontend
- Suppression de notytex-v2/ (code monté à la racine)

 Validations:
- Backend démarre correctement (port 8000)
- API /api/v2/health répond healthy
- 99/99 tests unitaires passent
- Frontend configuré avec proxy Vite

📝 Documentation:
- README.md réécrit pour v2
- Instructions de démarrage mises à jour
- .gitignore adapté pour backend/frontend/

🎯 Architecture finale:
notytex/
├── backend/     # FastAPI + SQLAlchemy + Pydantic
├── frontend/    # Vue 3 + Vite + TailwindCSS
├── docs/        # Documentation
└── school_management.db  # Base de données (inchangée)

Jalon 6 complété: Application v2 prête pour utilisation!
2025-11-25 21:09:47 +01:00

334 lines
13 KiB
Python

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