Files
notytex/backend/tests/comparison/test_write_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

611 lines
23 KiB
Python

"""
Tests de compatibilité croisée v1↔v2 pour les opérations d'écriture.
Ces tests vérifient que les données créées via l'API v2 sont correctement
lisibles et utilisables par l'API v1, et vice-versa.
"""
import pytest
from datetime import date, timedelta
from httpx import AsyncClient
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from infrastructure.database.models import (
ClassGroup,
Student,
StudentEnrollment,
Assessment,
Exercise,
GradingElement,
Grade,
)
@pytest.mark.asyncio
class TestClassCRUD:
"""Tests CRUD pour les classes."""
async def test_create_class(self, client: AsyncClient, db_session: AsyncSession):
"""Test création d'une classe."""
class_data = {
"name": "Test Class API v2",
"description": "Classe créée via API v2",
"year": "2024-2025"
}
response = await client.post("/api/v2/classes", json=class_data)
assert response.status_code == 201
data = response.json()
assert data["name"] == "Test Class API v2"
assert data["year"] == "2024-2025"
assert data["students_count"] == 0
assert "id" in data
# Vérifier en base
query = select(ClassGroup).where(ClassGroup.id == data["id"])
result = await db_session.execute(query)
cls = result.scalar_one()
assert cls.name == "Test Class API v2"
async def test_create_class_duplicate_name(self, client: AsyncClient, db_session: AsyncSession):
"""Test qu'on ne peut pas créer deux classes avec le même nom."""
class_data = {
"name": "Duplicate Class",
"year": "2024-2025"
}
# Première création
response1 = await client.post("/api/v2/classes", json=class_data)
assert response1.status_code == 201
# Deuxième création avec le même nom
response2 = await client.post("/api/v2/classes", json=class_data)
assert response2.status_code == 400
assert "existe déjà" in response2.json()["detail"]
async def test_update_class(self, client: AsyncClient, db_session: AsyncSession):
"""Test modification d'une classe."""
# Créer une classe
create_response = await client.post("/api/v2/classes", json={
"name": "Class to Update",
"year": "2024-2025"
})
class_id = create_response.json()["id"]
# Modifier la classe
update_response = await client.put(f"/api/v2/classes/{class_id}", json={
"name": "Updated Class Name",
"description": "New description"
})
assert update_response.status_code == 200
data = update_response.json()
assert data["name"] == "Updated Class Name"
assert data["description"] == "New description"
async def test_delete_class(self, client: AsyncClient, db_session: AsyncSession):
"""Test suppression d'une classe vide."""
# Créer une classe
create_response = await client.post("/api/v2/classes", json={
"name": "Class to Delete",
"year": "2024-2025"
})
class_id = create_response.json()["id"]
# Supprimer la classe
delete_response = await client.delete(f"/api/v2/classes/{class_id}")
assert delete_response.status_code == 204
# Vérifier qu'elle n'existe plus
get_response = await client.get(f"/api/v2/classes/{class_id}")
assert get_response.status_code == 404
@pytest.mark.asyncio
class TestStudentCRUD:
"""Tests CRUD pour les étudiants."""
async def test_create_student_without_class(self, client: AsyncClient):
"""Test création d'un étudiant sans l'inscrire dans une classe."""
student_data = {
"first_name": "Jean",
"last_name": "Dupont",
"email": "jean.dupont@test.com"
}
response = await client.post("/api/v2/students", json=student_data)
assert response.status_code == 201
data = response.json()
assert data["first_name"] == "Jean"
assert data["last_name"] == "Dupont"
assert data["current_class_id"] is None
async def test_create_student_with_class(self, client: AsyncClient):
"""Test création d'un étudiant avec inscription directe dans une classe."""
# Créer une classe d'abord
class_response = await client.post("/api/v2/classes", json={
"name": "Classe pour inscription",
"year": "2024-2025"
})
class_id = class_response.json()["id"]
# Créer un étudiant avec inscription
student_data = {
"first_name": "Marie",
"last_name": "Martin",
"class_group_id": class_id,
"enrollment_date": str(date.today())
}
response = await client.post("/api/v2/students", json=student_data)
assert response.status_code == 201
data = response.json()
assert data["current_class_id"] == class_id
assert data["current_class_name"] == "Classe pour inscription"
async def test_enroll_existing_student(self, client: AsyncClient):
"""Test inscription d'un élève existant dans une classe."""
# Créer une classe
class_response = await client.post("/api/v2/classes", json={
"name": "Classe d'inscription",
"year": "2024-2025"
})
class_id = class_response.json()["id"]
# Créer un étudiant sans classe
student_response = await client.post("/api/v2/students", json={
"first_name": "Pierre",
"last_name": "Durand"
})
student_id = student_response.json()["id"]
# Inscrire l'étudiant
enroll_response = await client.post("/api/v2/students/enroll", json={
"student_id": student_id,
"class_group_id": class_id,
"enrollment_date": str(date.today()),
"enrollment_reason": "Inscription normale"
})
assert enroll_response.status_code == 201
data = enroll_response.json()
assert data["student_id"] == student_id
assert data["class_name"] == "Classe d'inscription"
assert data["is_new_student"] is False
async def test_enroll_new_student(self, client: AsyncClient):
"""Test inscription avec création d'un nouvel élève."""
# Créer une classe
class_response = await client.post("/api/v2/classes", json={
"name": "Classe nouvel élève",
"year": "2024-2025"
})
class_id = class_response.json()["id"]
# Inscrire avec création d'élève
enroll_response = await client.post("/api/v2/students/enroll", json={
"first_name": "Sophie",
"last_name": "Leroy",
"class_group_id": class_id,
"enrollment_date": str(date.today())
})
assert enroll_response.status_code == 201
data = enroll_response.json()
assert data["student_name"] == "Sophie Leroy"
assert data["is_new_student"] is True
async def test_transfer_student(self, client: AsyncClient):
"""Test transfert d'un élève vers une autre classe."""
# Créer deux classes
class1_response = await client.post("/api/v2/classes", json={
"name": "Classe origine",
"year": "2024-2025"
})
class1_id = class1_response.json()["id"]
class2_response = await client.post("/api/v2/classes", json={
"name": "Classe destination",
"year": "2024-2025"
})
class2_id = class2_response.json()["id"]
# Créer et inscrire un étudiant dans la première classe
enroll_response = await client.post("/api/v2/students/enroll", json={
"first_name": "Lucas",
"last_name": "Bernard",
"class_group_id": class1_id,
"enrollment_date": str(date.today() - timedelta(days=30))
})
student_id = enroll_response.json()["student_id"]
# Transférer vers la deuxième classe
transfer_response = await client.post("/api/v2/students/transfer", json={
"student_id": student_id,
"new_class_group_id": class2_id,
"transfer_date": str(date.today()),
"transfer_reason": "Changement de section"
})
assert transfer_response.status_code == 200
data = transfer_response.json()
assert data["old_class_name"] == "Classe origine"
assert data["new_class_name"] == "Classe destination"
async def test_student_departure(self, client: AsyncClient):
"""Test enregistrement du départ d'un élève."""
# Créer une classe et inscrire un élève
class_response = await client.post("/api/v2/classes", json={
"name": "Classe départ",
"year": "2024-2025"
})
class_id = class_response.json()["id"]
enroll_response = await client.post("/api/v2/students/enroll", json={
"first_name": "Emma",
"last_name": "Petit",
"class_group_id": class_id,
"enrollment_date": str(date.today() - timedelta(days=30))
})
student_id = enroll_response.json()["student_id"]
# Enregistrer le départ
departure_response = await client.post("/api/v2/students/departure", json={
"student_id": student_id,
"departure_date": str(date.today()),
"departure_reason": "Déménagement"
})
assert departure_response.status_code == 200
data = departure_response.json()
assert "Emma Petit" in data["student_name"]
@pytest.mark.asyncio
class TestAssessmentCRUD:
"""Tests CRUD pour les évaluations."""
async def test_create_assessment_unified(self, client: AsyncClient):
"""Test création unifiée d'une évaluation complète."""
# Créer une classe
class_response = await client.post("/api/v2/classes", json={
"name": "Classe évaluation",
"year": "2024-2025"
})
class_id = class_response.json()["id"]
# Créer une évaluation complète
assessment_data = {
"title": "Contrôle de mathématiques",
"description": "Test du chapitre 3",
"date": str(date.today()),
"trimester": 1,
"coefficient": 2.0,
"class_group_id": class_id,
"exercises": [
{
"title": "Exercice 1",
"description": "Calculs",
"order": 1,
"grading_elements": [
{
"label": "Question 1a",
"max_points": 2.0,
"grading_type": "notes"
},
{
"label": "Question 1b",
"max_points": 3.0,
"grading_type": "notes"
}
]
},
{
"title": "Exercice 2",
"description": "Problème",
"order": 2,
"grading_elements": [
{
"label": "Compétence résolution",
"max_points": 3.0,
"grading_type": "score",
"skill": "Résoudre des problèmes"
}
]
}
]
}
response = await client.post("/api/v2/assessments", json=assessment_data)
assert response.status_code == 201
data = response.json()
assert data["title"] == "Contrôle de mathématiques"
assert data["trimester"] == 1
assert data["coefficient"] == 2.0
assert len(data["exercises"]) == 2
assert data["total_max_points"] == 8.0 # 2 + 3 + 3
assert data["grading_progress"]["status"] == "not_started"
async def test_update_assessment(self, client: AsyncClient):
"""Test modification d'une évaluation."""
# Créer une classe et une évaluation
class_response = await client.post("/api/v2/classes", json={
"name": "Classe update eval",
"year": "2024-2025"
})
class_id = class_response.json()["id"]
create_response = await client.post("/api/v2/assessments", json={
"title": "Évaluation à modifier",
"date": str(date.today()),
"trimester": 1,
"coefficient": 1.0,
"class_group_id": class_id,
"exercises": []
})
assessment_id = create_response.json()["id"]
# Modifier l'évaluation
update_response = await client.put(f"/api/v2/assessments/{assessment_id}", json={
"title": "Évaluation modifiée",
"trimester": 2,
"coefficient": 1.5
})
assert update_response.status_code == 200
data = update_response.json()
assert data["title"] == "Évaluation modifiée"
assert data["trimester"] == 2
assert data["coefficient"] == 1.5
async def test_delete_assessment(self, client: AsyncClient):
"""Test suppression d'une évaluation."""
# Créer une classe et une évaluation
class_response = await client.post("/api/v2/classes", json={
"name": "Classe delete eval",
"year": "2024-2025"
})
class_id = class_response.json()["id"]
create_response = await client.post("/api/v2/assessments", json={
"title": "Évaluation à supprimer",
"date": str(date.today()),
"trimester": 1,
"coefficient": 1.0,
"class_group_id": class_id,
"exercises": [
{
"title": "Exercice test",
"order": 1,
"grading_elements": [
{"label": "Q1", "max_points": 5.0, "grading_type": "notes"}
]
}
]
})
assessment_id = create_response.json()["id"]
# Supprimer
delete_response = await client.delete(f"/api/v2/assessments/{assessment_id}")
assert delete_response.status_code == 204
# Vérifier suppression
get_response = await client.get(f"/api/v2/assessments/{assessment_id}")
assert get_response.status_code == 404
@pytest.mark.asyncio
class TestGradesCRUD:
"""Tests pour la saisie des notes."""
async def test_save_grades(self, client: AsyncClient, db_session: AsyncSession):
"""Test sauvegarde de notes pour une évaluation."""
# Créer une classe
class_response = await client.post("/api/v2/classes", json={
"name": "Classe notation",
"year": "2024-2025"
})
class_id = class_response.json()["id"]
# Créer un élève inscrit
enroll_response = await client.post("/api/v2/students/enroll", json={
"first_name": "Test",
"last_name": "Student",
"class_group_id": class_id,
"enrollment_date": str(date.today() - timedelta(days=30))
})
student_id = enroll_response.json()["student_id"]
# Créer une évaluation
assessment_response = await client.post("/api/v2/assessments", json={
"title": "Évaluation notation",
"date": str(date.today()),
"trimester": 1,
"coefficient": 1.0,
"class_group_id": class_id,
"exercises": [
{
"title": "Exercice 1",
"order": 1,
"grading_elements": [
{"label": "Q1", "max_points": 5.0, "grading_type": "notes"},
{"label": "Q2", "max_points": 5.0, "grading_type": "notes"}
]
}
]
})
assessment_id = assessment_response.json()["id"]
# Récupérer les IDs des éléments de notation
get_response = await client.get(f"/api/v2/assessments/{assessment_id}")
elements = get_response.json()["exercises"][0]["grading_elements"]
# On ne peut pas utiliser les IDs directement car ils sont à 0 dans la création
# Récupérons-les depuis la base
query = select(GradingElement).join(Exercise).where(Exercise.assessment_id == assessment_id)
result = await db_session.execute(query)
db_elements = result.scalars().all()
# Sauvegarder des notes
grades_response = await client.post(f"/api/v2/assessments/{assessment_id}/grades", json={
"grades": [
{
"student_id": student_id,
"grading_element_id": db_elements[0].id,
"value": "4.5"
},
{
"student_id": student_id,
"grading_element_id": db_elements[1].id,
"value": "3"
}
]
})
assert grades_response.status_code == 200
data = grades_response.json()
assert data["saved_count"] == 2
assert data["created_count"] == 2
assert data["updated_count"] == 0
async def test_update_grades(self, client: AsyncClient, db_session: AsyncSession):
"""Test mise à jour de notes existantes."""
# Créer une classe et un élève
class_response = await client.post("/api/v2/classes", json={
"name": "Classe update notes",
"year": "2024-2025"
})
class_id = class_response.json()["id"]
enroll_response = await client.post("/api/v2/students/enroll", json={
"first_name": "Update",
"last_name": "Notes",
"class_group_id": class_id,
"enrollment_date": str(date.today() - timedelta(days=30))
})
student_id = enroll_response.json()["student_id"]
# Créer une évaluation
assessment_response = await client.post("/api/v2/assessments", json={
"title": "Évaluation update notes",
"date": str(date.today()),
"trimester": 1,
"coefficient": 1.0,
"class_group_id": class_id,
"exercises": [
{
"title": "Ex1",
"order": 1,
"grading_elements": [
{"label": "Q1", "max_points": 10.0, "grading_type": "notes"}
]
}
]
})
assessment_id = assessment_response.json()["id"]
# Récupérer l'ID de l'élément
query = select(GradingElement).join(Exercise).where(Exercise.assessment_id == assessment_id)
result = await db_session.execute(query)
element = result.scalar_one()
# Première sauvegarde
await client.post(f"/api/v2/assessments/{assessment_id}/grades", json={
"grades": [
{"student_id": student_id, "grading_element_id": element.id, "value": "5"}
]
})
# Mise à jour
update_response = await client.post(f"/api/v2/assessments/{assessment_id}/grades", json={
"grades": [
{"student_id": student_id, "grading_element_id": element.id, "value": "8"}
]
})
assert update_response.status_code == 200
data = update_response.json()
assert data["updated_count"] == 1
assert data["created_count"] == 0
@pytest.mark.asyncio
class TestCrossVersionCompatibility:
"""Tests de compatibilité croisée v1↔v2.
Ces tests vérifient que les données créées via v2 sont correctement
lisibles et cohérentes avec ce que v1 produirait.
"""
async def test_assessment_results_consistency(self, client: AsyncClient, db_session: AsyncSession):
"""Test que les résultats calculés sont cohérents."""
# Créer une classe avec des élèves
class_response = await client.post("/api/v2/classes", json={
"name": "Classe résultats",
"year": "2024-2025"
})
class_id = class_response.json()["id"]
# Créer plusieurs élèves
students = []
for i in range(3):
enroll_response = await client.post("/api/v2/students/enroll", json={
"first_name": f"Élève{i}",
"last_name": f"Test{i}",
"class_group_id": class_id,
"enrollment_date": str(date.today() - timedelta(days=30))
})
students.append(enroll_response.json()["student_id"])
# Créer une évaluation
assessment_response = await client.post("/api/v2/assessments", json={
"title": "Évaluation résultats",
"date": str(date.today()),
"trimester": 1,
"coefficient": 1.0,
"class_group_id": class_id,
"exercises": [
{
"title": "Exercice unique",
"order": 1,
"grading_elements": [
{"label": "Note principale", "max_points": 20.0, "grading_type": "notes"}
]
}
]
})
assessment_id = assessment_response.json()["id"]
# Récupérer l'ID de l'élément
query = select(GradingElement).join(Exercise).where(Exercise.assessment_id == assessment_id)
result = await db_session.execute(query)
element = result.scalar_one()
# Noter les élèves : 12, 15, 18
grades = [
{"student_id": students[0], "grading_element_id": element.id, "value": "12"},
{"student_id": students[1], "grading_element_id": element.id, "value": "15"},
{"student_id": students[2], "grading_element_id": element.id, "value": "18"},
]
await client.post(f"/api/v2/assessments/{assessment_id}/grades", json={"grades": grades})
# Récupérer les résultats
results_response = await client.get(f"/api/v2/assessments/{assessment_id}/results")
assert results_response.status_code == 200
data = results_response.json()
# Vérifier les statistiques
stats = data["statistics"]
assert stats["count"] == 3
assert stats["mean"] == pytest.approx(15.0, abs=0.1) # (12+15+18)/3
assert stats["median"] == pytest.approx(15.0, abs=0.1)
assert stats["min"] == 12.0
assert stats["max"] == 18.0