✨ 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!
334 lines
13 KiB
Python
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"])
|