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