""" 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 # Add project root to path for imports 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") # Mock the app state 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) # Check specific 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.""" # Create app without vector service 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 # Create app with uninitialized service 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 with vector service mock_request = Mock() mock_request.app.state.vector_service = mock_vector_service with patch('src.routes.api.generator', None): # Reset global generator 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.""" # Test missing required fields response = client.post("/api/generate", json={}) assert response.status_code == 422 # Validation error # Test invalid field types response = client.post("/api/generate", json={ "topics": "not_a_list", "difficulty": "medium", }) assert response.status_code == 422 # Validation error 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"] # difficulty should use defaults }) assert response.status_code == 200 # Check that defaults were used mock_crossword_generator.generate_puzzle.assert_called_once_with( topics=["Animals"], difficulty="medium", # Default value ) if __name__ == "__main__": pytest.main([__file__, "-v"])