|
|
""" |
|
|
Unit tests for API routes. |
|
|
""" |
|
|
|
|
|
import pytest |
|
|
import asyncio |
|
|
from unittest.mock import Mock, patch, AsyncMock |
|
|
from fastapi.testclient import TestClient |
|
|
from fastapi import FastAPI |
|
|
import sys |
|
|
from pathlib import Path |
|
|
|
|
|
|
|
|
project_root = Path(__file__).parent.parent |
|
|
sys.path.insert(0, str(project_root)) |
|
|
|
|
|
from src.routes.api import router, get_crossword_generator |
|
|
from src.services.crossword_generator_wrapper import CrosswordGenerator |
|
|
|
|
|
|
|
|
@pytest.fixture |
|
|
def mock_vector_service(): |
|
|
"""Mock vector search service.""" |
|
|
mock_service = Mock() |
|
|
mock_service.is_initialized = True |
|
|
mock_service.find_similar_words = AsyncMock(return_value=[ |
|
|
{"word": "ELEPHANT", "clue": "Large mammal", "similarity": 0.8, "source": "vector_search"}, |
|
|
{"word": "TIGER", "clue": "Striped cat", "similarity": 0.7, "source": "vector_search"}, |
|
|
]) |
|
|
return mock_service |
|
|
|
|
|
|
|
|
@pytest.fixture |
|
|
def mock_crossword_generator(): |
|
|
"""Mock crossword generator.""" |
|
|
mock_gen = Mock(spec=CrosswordGenerator) |
|
|
mock_gen.generate_puzzle = AsyncMock(return_value={ |
|
|
"grid": [["T", "E", "S", "T"], [".", ".", ".", "."]], |
|
|
"clues": [ |
|
|
{ |
|
|
"number": 1, |
|
|
"word": "TEST", |
|
|
"text": "A test word", |
|
|
"direction": "across", |
|
|
"position": {"row": 0, "col": 0} |
|
|
} |
|
|
], |
|
|
"metadata": { |
|
|
"topics": ["Animals"], |
|
|
"difficulty": "medium", |
|
|
"wordCount": 1, |
|
|
"size": 2, |
|
|
"aiGenerated": True |
|
|
} |
|
|
}) |
|
|
mock_gen.generate_words_for_topics = AsyncMock(return_value=[ |
|
|
{"word": "ELEPHANT", "clue": "Large mammal", "similarity": 0.8, "source": "vector_search"}, |
|
|
{"word": "TIGER", "clue": "Striped cat", "similarity": 0.7, "source": "vector_search"}, |
|
|
]) |
|
|
return mock_gen |
|
|
|
|
|
|
|
|
@pytest.fixture |
|
|
def test_app(mock_vector_service): |
|
|
"""Create test FastAPI app.""" |
|
|
app = FastAPI() |
|
|
app.include_router(router, prefix="/api") |
|
|
|
|
|
|
|
|
app.state.vector_service = mock_vector_service |
|
|
|
|
|
return app |
|
|
|
|
|
|
|
|
@pytest.fixture |
|
|
def client(test_app): |
|
|
"""Create test client.""" |
|
|
return TestClient(test_app) |
|
|
|
|
|
|
|
|
class TestAPIRoutes: |
|
|
"""Test cases for API routes.""" |
|
|
|
|
|
def test_get_topics(self, client): |
|
|
"""Test GET /api/topics endpoint.""" |
|
|
response = client.get("/api/topics") |
|
|
|
|
|
assert response.status_code == 200 |
|
|
topics = response.json() |
|
|
|
|
|
assert len(topics) == 4 |
|
|
assert all("id" in topic and "name" in topic for topic in topics) |
|
|
|
|
|
|
|
|
topic_ids = [topic["id"] for topic in topics] |
|
|
assert "animals" in topic_ids |
|
|
assert "geography" in topic_ids |
|
|
assert "science" in topic_ids |
|
|
assert "technology" in topic_ids |
|
|
|
|
|
def test_generate_puzzle_success(self, client, mock_crossword_generator): |
|
|
"""Test successful puzzle generation.""" |
|
|
with patch('src.routes.api.generator', mock_crossword_generator): |
|
|
response = client.post("/api/generate", json={ |
|
|
"topics": ["Animals"], |
|
|
"difficulty": "medium" |
|
|
}) |
|
|
|
|
|
assert response.status_code == 200 |
|
|
puzzle = response.json() |
|
|
|
|
|
assert "grid" in puzzle |
|
|
assert "clues" in puzzle |
|
|
assert "metadata" in puzzle |
|
|
assert puzzle["metadata"]["topics"] == ["Animals"] |
|
|
assert puzzle["metadata"]["difficulty"] == "medium" |
|
|
assert puzzle["metadata"]["aiGenerated"] is True |
|
|
|
|
|
def test_generate_puzzle_no_topics(self, client): |
|
|
"""Test puzzle generation with no topics.""" |
|
|
response = client.post("/api/generate", json={ |
|
|
"topics": [], |
|
|
"difficulty": "medium", |
|
|
}) |
|
|
|
|
|
assert response.status_code == 400 |
|
|
assert "At least one topic is required" in response.json()["detail"] |
|
|
|
|
|
def test_generate_puzzle_invalid_difficulty(self, client): |
|
|
"""Test puzzle generation with invalid difficulty.""" |
|
|
response = client.post("/api/generate", json={ |
|
|
"topics": ["Animals"], |
|
|
"difficulty": "impossible", |
|
|
}) |
|
|
|
|
|
assert response.status_code == 400 |
|
|
assert "Invalid difficulty" in response.json()["detail"] |
|
|
|
|
|
def test_generate_puzzle_generator_failure(self, client): |
|
|
"""Test puzzle generation when generator fails.""" |
|
|
mock_gen = Mock(spec=CrosswordGenerator) |
|
|
mock_gen.generate_puzzle = AsyncMock(return_value=None) |
|
|
|
|
|
with patch('src.routes.api.generator', mock_gen): |
|
|
response = client.post("/api/generate", json={ |
|
|
"topics": ["Animals"], |
|
|
"difficulty": "medium" |
|
|
}) |
|
|
|
|
|
assert response.status_code == 500 |
|
|
assert "Failed to generate puzzle" in response.json()["detail"] |
|
|
|
|
|
def test_generate_puzzle_generator_exception(self, client): |
|
|
"""Test puzzle generation when generator raises exception.""" |
|
|
mock_gen = Mock(spec=CrosswordGenerator) |
|
|
mock_gen.generate_puzzle = AsyncMock(side_effect=Exception("Test error")) |
|
|
|
|
|
with patch('src.routes.api.generator', mock_gen): |
|
|
response = client.post("/api/generate", json={ |
|
|
"topics": ["Animals"], |
|
|
"difficulty": "medium" |
|
|
}) |
|
|
|
|
|
assert response.status_code == 500 |
|
|
assert "Test error" in response.json()["detail"] |
|
|
|
|
|
def test_generate_words_success(self, client, mock_crossword_generator): |
|
|
"""Test successful word generation.""" |
|
|
with patch('src.routes.api.generator', mock_crossword_generator): |
|
|
response = client.post("/api/words", json={ |
|
|
"topics": ["Animals"], |
|
|
"difficulty": "medium" |
|
|
}) |
|
|
|
|
|
assert response.status_code == 200 |
|
|
result = response.json() |
|
|
|
|
|
assert result["topics"] == ["Animals"] |
|
|
assert result["difficulty"] == "medium" |
|
|
assert "wordCount" in result |
|
|
assert "words" in result |
|
|
assert len(result["words"]) > 0 |
|
|
|
|
|
def test_generate_words_failure(self, client): |
|
|
"""Test word generation failure.""" |
|
|
mock_gen = Mock(spec=CrosswordGenerator) |
|
|
mock_gen.generate_words_for_topics = AsyncMock(side_effect=Exception("Word generation failed")) |
|
|
|
|
|
with patch('src.routes.api.generator', mock_gen): |
|
|
response = client.post("/api/words", json={ |
|
|
"topics": ["Animals"], |
|
|
"difficulty": "medium" |
|
|
}) |
|
|
|
|
|
assert response.status_code == 500 |
|
|
assert "Word generation failed" in response.json()["detail"] |
|
|
|
|
|
def test_api_health(self, client): |
|
|
"""Test API health check.""" |
|
|
response = client.get("/api/health") |
|
|
|
|
|
assert response.status_code == 200 |
|
|
health = response.json() |
|
|
|
|
|
assert health["status"] == "healthy" |
|
|
assert health["backend"] == "python" |
|
|
assert health["version"] == "2.0.0" |
|
|
assert "timestamp" in health |
|
|
|
|
|
def test_debug_vector_search_success(self, client, mock_vector_service): |
|
|
"""Test debug vector search endpoint.""" |
|
|
response = client.get("/api/debug/vector-search?topic=Animals&difficulty=medium&max_words=5") |
|
|
|
|
|
assert response.status_code == 200 |
|
|
result = response.json() |
|
|
|
|
|
assert result["topic"] == "Animals" |
|
|
assert result["difficulty"] == "medium" |
|
|
assert result["max_words"] == 5 |
|
|
assert "found_words" in result |
|
|
assert "words" in result |
|
|
|
|
|
def test_debug_vector_search_service_unavailable(self, client): |
|
|
"""Test debug vector search when service unavailable.""" |
|
|
|
|
|
app = FastAPI() |
|
|
app.include_router(router, prefix="/api") |
|
|
app.state.vector_service = None |
|
|
|
|
|
with TestClient(app) as test_client: |
|
|
response = test_client.get("/api/debug/vector-search?topic=Animals") |
|
|
|
|
|
assert response.status_code == 503 |
|
|
assert "Vector search service not available" in response.json()["detail"] |
|
|
|
|
|
def test_debug_vector_search_not_initialized(self, client): |
|
|
"""Test debug vector search when service not initialized.""" |
|
|
mock_service = Mock() |
|
|
mock_service.is_initialized = False |
|
|
|
|
|
|
|
|
app = FastAPI() |
|
|
app.include_router(router, prefix="/api") |
|
|
app.state.vector_service = mock_service |
|
|
|
|
|
with TestClient(app) as test_client: |
|
|
response = test_client.get("/api/debug/vector-search?topic=Animals") |
|
|
|
|
|
assert response.status_code == 503 |
|
|
assert "Vector search service not available" in response.json()["detail"] |
|
|
|
|
|
def test_debug_vector_search_failure(self, client, mock_vector_service): |
|
|
"""Test debug vector search when search fails.""" |
|
|
mock_vector_service.find_similar_words = AsyncMock(side_effect=Exception("Search failed")) |
|
|
|
|
|
response = client.get("/api/debug/vector-search?topic=Animals") |
|
|
|
|
|
assert response.status_code == 500 |
|
|
assert "Search failed" in response.json()["detail"] |
|
|
|
|
|
def test_get_crossword_generator_dependency(self, mock_vector_service): |
|
|
"""Test the crossword generator dependency.""" |
|
|
|
|
|
mock_request = Mock() |
|
|
mock_request.app.state.vector_service = mock_vector_service |
|
|
|
|
|
with patch('src.routes.api.generator', None): |
|
|
generator = get_crossword_generator(mock_request) |
|
|
|
|
|
assert isinstance(generator, CrosswordGenerator) |
|
|
assert generator.vector_service == mock_vector_service |
|
|
|
|
|
def test_request_validation(self, client): |
|
|
"""Test request model validation.""" |
|
|
|
|
|
response = client.post("/api/generate", json={}) |
|
|
assert response.status_code == 422 |
|
|
|
|
|
|
|
|
response = client.post("/api/generate", json={ |
|
|
"topics": "not_a_list", |
|
|
"difficulty": "medium", |
|
|
}) |
|
|
assert response.status_code == 422 |
|
|
|
|
|
def test_default_values(self, client, mock_crossword_generator): |
|
|
"""Test request model default values.""" |
|
|
with patch('src.routes.api.generator', mock_crossword_generator): |
|
|
response = client.post("/api/generate", json={ |
|
|
"topics": ["Animals"] |
|
|
|
|
|
}) |
|
|
|
|
|
assert response.status_code == 200 |
|
|
|
|
|
|
|
|
mock_crossword_generator.generate_puzzle.assert_called_once_with( |
|
|
topics=["Animals"], |
|
|
difficulty="medium", |
|
|
) |
|
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
|
pytest.main([__file__, "-v"]) |