First commit
This commit is contained in:
140
tests/etape2_dal_repositories.py
Normal file
140
tests/etape2_dal_repositories.py
Normal file
@@ -0,0 +1,140 @@
|
||||
"""
|
||||
Tests de validation pour l'Étape 2 : Couche d'accès aux données (Repositories)
|
||||
|
||||
[Version Corrigée]
|
||||
Cette version corrige les assertions pour accepter les arguments positionnels (args)
|
||||
au lieu de forcer les arguments mots-clés (kwargs) lors de l'appel à _request.
|
||||
Elle corrige également la simulation de DALException pour qu'elle corresponde
|
||||
à la logique de _base_client.py.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
from unittest.mock import MagicMock, AsyncMock
|
||||
from app.core.exceptions import DALException
|
||||
from app.models.movie import Movie
|
||||
from app.models.genre import Genre
|
||||
|
||||
# --- Fixtures de données (simule les réponses de l'API) ---
|
||||
|
||||
@pytest.fixture
|
||||
def mock_movie_data():
|
||||
"""Données JSON brutes pour un film, simulées depuis l'API."""
|
||||
return {
|
||||
"id": 1,
|
||||
"title": "Inception",
|
||||
"year": 2010,
|
||||
"duration": 148,
|
||||
"synopsis": "Un voleur...",
|
||||
"genre": {"id": 1, "label": "Science-Fiction"},
|
||||
"director": {"id": 1, "last_name": "Nolan", "first_name": "Christopher"},
|
||||
"actors": [{"id": 2, "last_name": "DiCaprio", "first_name": "Leonardo"}],
|
||||
"opinions": []
|
||||
}
|
||||
|
||||
@pytest.fixture
|
||||
def mock_genre_list_data():
|
||||
"""Données JSON brutes pour une liste de genres."""
|
||||
return [
|
||||
{"id": 1, "label": "Science-Fiction"},
|
||||
{"id": 2, "label": "Drame"}
|
||||
]
|
||||
|
||||
# --- Tests ---
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_movie_repo_find_by_id_success(mocker, mock_movie_data):
|
||||
"""
|
||||
Vérifie que movie_repository.find_by_id retourne un objet Movie
|
||||
en cas de succès.
|
||||
"""
|
||||
# 1. Mock de l'api_client
|
||||
mock_response = MagicMock()
|
||||
mock_response.json.return_value = mock_movie_data
|
||||
mock_api_client = AsyncMock()
|
||||
mock_api_client._request.return_value = mock_response
|
||||
|
||||
# On "patch" l'instance importée dans le module du repository
|
||||
mocker.patch('app.repositories.movie_repository.api_client', mock_api_client)
|
||||
|
||||
# 2. Appel de la méthode à tester
|
||||
from app.repositories.movie_repository import movie_repository
|
||||
movie = await movie_repository.find_by_id(1)
|
||||
|
||||
# 3. Assertions
|
||||
mock_api_client._request.assert_called_once_with("GET", "/movies/1")
|
||||
assert movie is not None
|
||||
assert isinstance(movie, Movie)
|
||||
assert movie.title == "Inception"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_movie_repo_find_by_id_not_found(mocker):
|
||||
"""
|
||||
Vérifie que movie_repository.find_by_id retourne None si l'API
|
||||
lève une DALException avec un status 404.
|
||||
"""
|
||||
# 1. Mock de l'api_client pour qu'il lève une erreur 404
|
||||
mock_api_client = AsyncMock()
|
||||
|
||||
# 1. Crée l'exception
|
||||
mock_exception = DALException("Not Found")
|
||||
# 2. Attache le status_code
|
||||
mock_exception.status_code = 404
|
||||
# 3. La définit comme side_effect
|
||||
mock_api_client._request.side_effect = mock_exception
|
||||
|
||||
mocker.patch('app.repositories.movie_repository.api_client', mock_api_client)
|
||||
|
||||
# 2. Appel de la méthode à tester
|
||||
from app.repositories.movie_repository import movie_repository
|
||||
movie = await movie_repository.find_by_id(999)
|
||||
|
||||
# 3. Assertions
|
||||
mock_api_client._request.assert_called_once_with("GET", "/movies/999")
|
||||
assert movie is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_movie_repo_list(mocker, mock_movie_data):
|
||||
"""
|
||||
Vérifie que movie_repository.list retourne une liste de Movies
|
||||
et passe correctement les paramètres skip/limit.
|
||||
"""
|
||||
# 1. Mock de l'api_client
|
||||
mock_response = MagicMock()
|
||||
mock_response.json.return_value = [mock_movie_data] # L'API retourne une liste
|
||||
mock_api_client = AsyncMock()
|
||||
mock_api_client._request.return_value = mock_response
|
||||
mocker.patch('app.repositories.movie_repository.api_client', mock_api_client)
|
||||
|
||||
# 2. Appel de la méthode à tester
|
||||
from app.repositories.movie_repository import movie_repository
|
||||
movies = await movie_repository.list(skip=5, limit=10)
|
||||
|
||||
# 3. Assertions
|
||||
expected_params = {"skip": 5, "limit": 10}
|
||||
mock_api_client._request.assert_called_once_with("GET", "/movies/", params=expected_params)
|
||||
assert isinstance(movies, list)
|
||||
assert isinstance(movies[0], Movie)
|
||||
assert movies[0].id == 1
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_genre_repo_list(mocker, mock_genre_list_data):
|
||||
"""
|
||||
Vérifie que genre_repository.list retourne une liste de Genres.
|
||||
"""
|
||||
# 1. Mock de l'api_client
|
||||
mock_response = MagicMock()
|
||||
mock_response.json.return_value = mock_genre_list_data
|
||||
mock_api_client = AsyncMock()
|
||||
mock_api_client._request.return_value = mock_response
|
||||
mocker.patch('app.repositories.genre_repository.api_client', mock_api_client)
|
||||
|
||||
# 2. Appel de la méthode à tester
|
||||
from app.repositories.genre_repository import genre_repository
|
||||
genres = await genre_repository.list()
|
||||
|
||||
# 3. Assertions
|
||||
mock_api_client._request.assert_called_once_with("GET", "/genres/")
|
||||
assert isinstance(genres, list)
|
||||
assert isinstance(genres[0], Genre)
|
||||
assert genres[0].label == "Science-Fiction"
|
||||
70
tests/etape3_graphql_v1.py
Normal file
70
tests/etape3_graphql_v1.py
Normal file
@@ -0,0 +1,70 @@
|
||||
"""
|
||||
Tests de validation pour l'Étape 3 : Mise en place GraphQL (Version 1 - Statique)
|
||||
|
||||
Objectif :
|
||||
1. Vérifier que le service `movie_analyzer_v1` retourne bien un dict "mock".
|
||||
2. Vérifier que le resolver `analyze_movie_v1` appelle ce service et
|
||||
construit correctement l'objet `MovieAnalysis`.
|
||||
|
||||
Prérequis :
|
||||
- Les TODOs de `app/services/movie_analyzer_v1.py` sont complétés.
|
||||
- Les TODOs de `app/graphql/resolvers/analyze_movie_v1.py` sont complétés.
|
||||
- Le champ `analyzeMovie` est bien ajouté à `app/graphql/queries.py`
|
||||
(en utilisant le resolver V1).
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import strawberry
|
||||
from unittest.mock import AsyncMock
|
||||
from app.graphql.types.movie_analysis import MovieAnalysis
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_service_v1_analyze_movie():
|
||||
"""
|
||||
Teste le service V1. Il doit retourner un dictionnaire
|
||||
contenant les clés requises et l'ID correct.
|
||||
"""
|
||||
from app.services.movie_analyzer_v1 import analyze_movie
|
||||
|
||||
movie_id = "123"
|
||||
analysis_data = await analyze_movie(movie_id)
|
||||
|
||||
assert isinstance(analysis_data, dict)
|
||||
assert analysis_data["id"] == movie_id
|
||||
assert "aiSummary" in analysis_data
|
||||
assert "aiOpinionSummary" in analysis_data
|
||||
assert "aiBestGenre" in analysis_data
|
||||
assert "aiTags" in analysis_data
|
||||
assert isinstance(analysis_data["aiTags"], list)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_resolver_v1_analyze_movie_by_id(mocker):
|
||||
"""
|
||||
Teste le resolver V1. Il doit appeler le service V1 et
|
||||
retourner un objet `MovieAnalysis` typé.
|
||||
"""
|
||||
# 1. Mock du service V1 que le resolver est censé appeler
|
||||
mock_service_data = {
|
||||
"id": "1",
|
||||
"aiSummary": "Mock summary",
|
||||
"aiOpinionSummary": "Mock opinion",
|
||||
"aiBestGenre": "Mock genre",
|
||||
"aiTags": ["mock", "test"]
|
||||
}
|
||||
mock_analyze_service = AsyncMock(return_value=mock_service_data)
|
||||
mocker.patch('app.graphql.resolvers.analyze_movie_v1.analyze_movie', mock_analyze_service)
|
||||
|
||||
# 2. Appel du resolver
|
||||
from app.graphql.resolvers.analyze_movie_v1 import analyze_movie_by_id
|
||||
|
||||
result = await analyze_movie_by_id(movie_id=strawberry.ID("1"))
|
||||
|
||||
# 3. Assertions
|
||||
# Vérifie que le service a bien été appelé
|
||||
mock_analyze_service.assert_called_once_with(movie_id="1")
|
||||
|
||||
# Vérifie que le résultat est du bon type
|
||||
assert isinstance(result, MovieAnalysis)
|
||||
assert result.id == strawberry.ID("1")
|
||||
assert result.aiSummary == "Mock summary"
|
||||
assert result.aiTags == ["mock", "test"]
|
||||
188
tests/etape5_service_v2.py
Normal file
188
tests/etape5_service_v2.py
Normal file
@@ -0,0 +1,188 @@
|
||||
"""
|
||||
Tests de validation pour l'Étape 5 : Service d'analyse avancé (Version 2 - Dynamique)
|
||||
|
||||
Objectif :
|
||||
1. Vérifier que les fonctions `get_ai_...` appellent bien le LLM.
|
||||
2. Vérifier que la fonction principale `analyze_movie` (V2)
|
||||
n'appelle QUE les fonctions nécessaires (en fonction des booléens).
|
||||
3. Vérifier que les appels aux repositories sont corrects.
|
||||
|
||||
Prérequis :
|
||||
- Les TODOs de `app/services/movie_analyzer_v2.py` sont complétés.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import strawberry
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
from app.models.movie import Movie
|
||||
from app.models.genre import Genre
|
||||
from app.models.person import Person
|
||||
from app.models.opinion import Opinion
|
||||
from app.models.member import Member
|
||||
|
||||
|
||||
# --- Fixtures ---
|
||||
|
||||
@pytest.fixture
|
||||
def mock_llm():
|
||||
"""Fixture pour un LLM mocké."""
|
||||
llm = MagicMock()
|
||||
# Simule la réponse de llm.ainvoke(...)
|
||||
mock_response = MagicMock()
|
||||
mock_response.content = "Réponse du LLM"
|
||||
llm.ainvoke = AsyncMock(return_value=mock_response)
|
||||
return llm
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_movie():
|
||||
"""Fixture pour un objet Movie Pydantic complet."""
|
||||
return Movie(
|
||||
id=1,
|
||||
title="Inception",
|
||||
year=2010,
|
||||
synopsis="Un voleur qui vole des secrets...",
|
||||
genre=Genre(id=1, label="Science-Fiction"),
|
||||
director=Person(id=1, last_name="Nolan", first_name="Christopher"),
|
||||
actors=[Person(id=2, last_name="DiCaprio", first_name="Leonardo")],
|
||||
opinions=[
|
||||
Opinion(id=1, note=5, comment="Génial!", movie_id=1, member=Member(id=1, login="user1"))
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_genres():
|
||||
"""Fixture pour une liste d'objets Genre Pydantic."""
|
||||
return [
|
||||
Genre(id=1, label="Science-Fiction"),
|
||||
Genre(id=2, label="Drame")
|
||||
]
|
||||
|
||||
|
||||
# --- Tests des helpers LLM ---
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_ai_summary(mock_llm):
|
||||
"""Teste le prompt de résumé."""
|
||||
from app.services.movie_analyzer_v2 import get_ai_summary
|
||||
synopsis = "Un long synopsis..."
|
||||
result = await get_ai_summary(mock_llm, synopsis)
|
||||
|
||||
assert result == "Réponse du LLM"
|
||||
mock_llm.ainvoke.assert_called_once()
|
||||
prompt_call = mock_llm.ainvoke.call_args[0][0]
|
||||
assert synopsis in prompt_call # Vérifie que le synopsis est dans le prompt
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_ai_summary_no_synopsis(mock_llm):
|
||||
"""Teste que le LLM n'est pas appelé si le synopsis est vide."""
|
||||
from app.services.movie_analyzer_v2 import get_ai_summary
|
||||
result = await get_ai_summary(mock_llm, None)
|
||||
|
||||
assert result is None
|
||||
mock_llm.ainvoke.assert_not_called()
|
||||
|
||||
|
||||
# ... (Des tests similaires pourraient être écrits pour get_ai_opinion_summary,
|
||||
# get_ai_best_genre, et get_ai_tags)
|
||||
|
||||
# --- Test du service principal (V2) ---
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_service_v2_analyze_movie_partial_request(mocker, mock_llm, mock_movie):
|
||||
"""
|
||||
Teste analyze_movie V2 avec une demande partielle (juste aiSummary).
|
||||
Vérifie que SEULS les appels nécessaires sont faits.
|
||||
"""
|
||||
# 1. Mocker les dépendances (repositories)
|
||||
mock_movie_repo = AsyncMock()
|
||||
mock_movie_repo.find_by_id.return_value = mock_movie
|
||||
mocker.patch('app.services.movie_analyzer_v2.movie_repository', mock_movie_repo)
|
||||
|
||||
mock_genre_repo = AsyncMock()
|
||||
mocker.patch('app.services.movie_analyzer_v2.genre_repository', mock_genre_repo)
|
||||
|
||||
# 2. Mocker les helpers LLM (pour les espionner)
|
||||
mock_summary = AsyncMock(return_value="Résumé IA")
|
||||
mock_opinion = AsyncMock()
|
||||
mock_genre = AsyncMock()
|
||||
mock_tags = AsyncMock()
|
||||
|
||||
mocker.patch('app.services.movie_analyzer_v2.get_ai_summary', mock_summary)
|
||||
mocker.patch('app.services.movie_analyzer_v2.get_ai_opinion_summary', mock_opinion)
|
||||
mocker.patch('app.services.movie_analyzer_v2.get_ai_best_genre', mock_genre)
|
||||
mocker.patch('app.services.movie_analyzer_v2.get_ai_tags', mock_tags)
|
||||
|
||||
# 3. Appel du service
|
||||
from app.services.movie_analyzer_v2 import analyze_movie
|
||||
result = await analyze_movie(
|
||||
movie_id="1",
|
||||
ai_summary=True,
|
||||
ai_opinion_summary=False,
|
||||
ai_best_genre=False,
|
||||
ai_tags=False,
|
||||
llm=mock_llm
|
||||
)
|
||||
|
||||
# 4. Assertions
|
||||
# Le repo de film a été appelé
|
||||
mock_movie_repo.find_by_id.assert_called_once_with("1")
|
||||
# Le repo de genre NE DOIT PAS être appelé
|
||||
mock_genre_repo.list.assert_not_called()
|
||||
|
||||
# Seul le helper de résumé a été appelé
|
||||
mock_summary.assert_called_once()
|
||||
mock_opinion.assert_not_called()
|
||||
mock_genre.assert_not_called()
|
||||
mock_tags.assert_not_called()
|
||||
|
||||
# Le résultat est correct
|
||||
assert result['id'] == strawberry.ID("1")
|
||||
assert result['aiSummary'] == "Résumé IA"
|
||||
assert result['aiOpinionSummary'] is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_service_v2_analyze_movie_full_request(mocker, mock_llm, mock_movie, mock_genres):
|
||||
"""
|
||||
Teste analyze_movie V2 avec une demande complète.
|
||||
Vérifie que tout est appelé (y compris asyncio.gather).
|
||||
"""
|
||||
# 1. Mocker les dépendances (repositories)
|
||||
mock_movie_repo = AsyncMock()
|
||||
mock_movie_repo.find_by_id.return_value = mock_movie
|
||||
mocker.patch('app.services.movie_analyzer_v2.movie_repository', mock_movie_repo)
|
||||
|
||||
mock_genre_repo = AsyncMock()
|
||||
mock_genre_repo.list.return_value = mock_genres
|
||||
mocker.patch('app.services.movie_analyzer_v2.genre_repository', mock_genre_repo)
|
||||
|
||||
# 2. Mocker les helpers LLM
|
||||
mocker.patch('app.services.movie_analyzer_v2.get_ai_summary', AsyncMock(return_value="Résumé IA"))
|
||||
mocker.patch('app.services.movie_analyzer_v2.get_ai_opinion_summary', AsyncMock(return_value="Opinion IA"))
|
||||
mocker.patch('app.services.movie_analyzer_v2.get_ai_best_genre', AsyncMock(return_value="Genre IA"))
|
||||
mocker.patch('app.services.movie_analyzer_v2.get_ai_tags', AsyncMock(return_value=["tag1", "tag2"]))
|
||||
|
||||
# 3. Appel du service
|
||||
from app.services.movie_analyzer_v2 import analyze_movie
|
||||
result = await analyze_movie(
|
||||
movie_id="1",
|
||||
ai_summary=True,
|
||||
ai_opinion_summary=True,
|
||||
ai_best_genre=True,
|
||||
ai_tags=True,
|
||||
llm=mock_llm
|
||||
)
|
||||
|
||||
# 4. Assertions
|
||||
# Les deux repos ont été appelés
|
||||
mock_movie_repo.find_by_id.assert_called_once_with("1")
|
||||
mock_genre_repo.list.assert_called_once() # Appelé car ai_best_genre=True
|
||||
|
||||
# Le résultat est complet
|
||||
assert result['aiSummary'] == "Résumé IA"
|
||||
assert result['aiOpinionSummary'] == "Opinion IA"
|
||||
assert result['aiBestGenre'] == "Genre IA"
|
||||
assert result['aiTags'] == ["tag1", "tag2"]
|
||||
110
tests/etape6_resolver_v2.py
Normal file
110
tests/etape6_resolver_v2.py
Normal file
@@ -0,0 +1,110 @@
|
||||
"""
|
||||
Tests de validation pour l'Étape 6 : Optimisation du Resolver GraphQL (Version 2)
|
||||
|
||||
Objectif :
|
||||
1. Vérifier que le resolver V2 récupère bien le LLM du contexte.
|
||||
2. Vérifier que le resolver V2 utilise `is_field_requested` pour
|
||||
passer les bons booléens au service V2.
|
||||
|
||||
Prérequis :
|
||||
- Les TODOs de `app/graphql/resolvers/analyze_movie_v2.py` sont complétés.
|
||||
- Le champ `analyzeMovie` de `app/graphql/queries.py` est mis à jour
|
||||
pour utiliser le resolver V2.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import strawberry
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
@pytest.fixture
|
||||
def mock_info():
|
||||
"""Fixture pour un objet Info de Strawberry."""
|
||||
mock_llm_instance = MagicMock(name="MockLLM")
|
||||
info = MagicMock()
|
||||
info.context = {"llm": mock_llm_instance}
|
||||
return info
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_resolver_v2_partial_request(mocker, mock_info):
|
||||
"""
|
||||
Teste le resolver V2 avec une requête partielle.
|
||||
Vérifie qu'il passe les bons drapeaux au service.
|
||||
"""
|
||||
# 1. Mocker les dépendances (le service V2 et le helper is_field_requested)
|
||||
|
||||
# On mock le service V2 pour espionner ses arguments
|
||||
mock_service = AsyncMock(return_value={
|
||||
"id": "1",
|
||||
"aiSummary": "Service Result",
|
||||
"aiOpinionSummary": None,
|
||||
"aiBestGenre": None,
|
||||
"aiTags": None
|
||||
})
|
||||
mocker.patch('app.graphql.resolvers.analyze_movie_v2.analyze_movie', mock_service)
|
||||
|
||||
# On mock 'is_field_requested' pour simuler une requête partielle
|
||||
def mock_is_field_requested(info, field_name):
|
||||
if field_name == "aiSummary":
|
||||
return True
|
||||
return False
|
||||
|
||||
mocker.patch('app.graphql.resolvers.analyze_movie_v2.is_field_requested', mock_is_field_requested)
|
||||
|
||||
# 2. Appel du resolver
|
||||
from app.graphql.resolvers.analyze_movie_v2 import analyze_movie_by_id
|
||||
|
||||
await analyze_movie_by_id(
|
||||
movie_id=strawberry.ID("1"),
|
||||
info=mock_info
|
||||
)
|
||||
|
||||
# 3. Assertions
|
||||
# Vérifie que le service a été appelé avec les bons drapeaux
|
||||
mock_service.assert_called_once_with(
|
||||
movie_id="1",
|
||||
ai_summary=True,
|
||||
ai_opinion_summary=False,
|
||||
ai_best_genre=False,
|
||||
ai_tags=False,
|
||||
llm=mock_info.context["llm"] # Vérifie que le LLM du contexte est bien passé
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_resolver_v2_full_request(mocker, mock_info):
|
||||
"""
|
||||
Teste le resolver V2 avec une requête complète.
|
||||
"""
|
||||
# 1. Mocker les dépendances
|
||||
# [CORRECTION] Le mock doit retourner un dictionnaire complet
|
||||
# pour que MovieAnalysis(**analysis_data) fonctionne.
|
||||
mock_service_return = {
|
||||
"id": "1",
|
||||
"aiSummary": "Mock summary",
|
||||
"aiOpinionSummary": "Mock opinion",
|
||||
"aiBestGenre": "Mock genre",
|
||||
"aiTags": ["mock", "tag"]
|
||||
}
|
||||
mock_service = AsyncMock(return_value=mock_service_return)
|
||||
mocker.patch('app.graphql.resolvers.analyze_movie_v2.analyze_movie', mock_service)
|
||||
|
||||
# Simule une requête complète
|
||||
mocker.patch('app.graphql.resolvers.analyze_movie_v2.is_field_requested', return_value=True)
|
||||
|
||||
# 2. Appel du resolver
|
||||
from app.graphql.resolvers.analyze_movie_v2 import analyze_movie_by_id
|
||||
|
||||
await analyze_movie_by_id(
|
||||
movie_id=strawberry.ID("1"),
|
||||
info=mock_info
|
||||
)
|
||||
|
||||
# 3. Assertions
|
||||
# Vérifie que le service a été appelé avec TOUS les drapeaux à True
|
||||
mock_service.assert_called_once_with(
|
||||
movie_id="1",
|
||||
ai_summary=True,
|
||||
ai_opinion_summary=True,
|
||||
ai_best_genre=True,
|
||||
ai_tags=True,
|
||||
llm=mock_info.context["llm"]
|
||||
)
|
||||
51
tests/etape7_erreurs.py
Normal file
51
tests/etape7_erreurs.py
Normal file
@@ -0,0 +1,51 @@
|
||||
"""
|
||||
Tests de validation pour l'Étape 7 (Bonus) : Gestion fine des erreurs métier
|
||||
|
||||
Objectif :
|
||||
1. Vérifier que le service V2 lève bien une `NotFoundBLLException`
|
||||
si le `movie_repository` retourne `None`.
|
||||
|
||||
Prérequis :
|
||||
- La logique de gestion d'erreur est en place dans
|
||||
`app/services/movie_analyzer_v2.py`.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
from app.core.exceptions import NotFoundBLLException
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_service_v2_raises_not_found(mocker):
|
||||
"""
|
||||
Vérifie que le service `analyze_movie` lève une `NotFoundBLLException`
|
||||
si le film n'est pas trouvé dans le repository.
|
||||
"""
|
||||
# 1. Mocker les dépendances (repositories)
|
||||
mock_movie_repo = AsyncMock()
|
||||
mock_movie_repo.find_by_id.return_value = None # Simule un film non trouvé
|
||||
mocker.patch('app.services.movie_analyzer_v2.movie_repository', mock_movie_repo)
|
||||
|
||||
mock_llm = MagicMock()
|
||||
|
||||
# 2. Appel du service en s'attendant à une exception
|
||||
from app.services.movie_analyzer_v2 import analyze_movie
|
||||
|
||||
with pytest.raises(NotFoundBLLException) as exc_info:
|
||||
await analyze_movie(
|
||||
movie_id="999",
|
||||
ai_summary=True, # Peu importe les drapeaux
|
||||
ai_opinion_summary=False,
|
||||
ai_best_genre=False,
|
||||
ai_tags=False,
|
||||
llm=mock_llm
|
||||
)
|
||||
|
||||
# 3. Assertions
|
||||
# Vérifie que le repo a bien été appelé
|
||||
mock_movie_repo.find_by_id.assert_called_once_with("999")
|
||||
# Vérifie que le type d'exception est correct
|
||||
assert exc_info.type is NotFoundBLLException
|
||||
# Vérifie que le message d'erreur contient l'ID
|
||||
assert "999" in str(exc_info.value)
|
||||
assert "Movie" in str(exc_info.value)
|
||||
Reference in New Issue
Block a user