Skip to main content
Glama

OpenEdu MCP Server

test_openlibrary_tools.pyโ€ข19.8 kB
""" Unit tests for Open Library tools. This module contains tests for the Open Library API client and MCP tools, including mocked API responses for reliable testing. """ import pytest import asyncio from unittest.mock import AsyncMock, MagicMock, patch from datetime import datetime, date from typing import Dict, Any, List import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src")) from api.openlibrary import OpenLibraryClient from tools.openlibrary_tools import OpenLibraryTool from models.book import Book from models.base import GradeLevel, EducationalMetadata from config import Config from exceptions import ValidationError, APIError, ToolError class TestOpenLibraryClient: """Test cases for OpenLibraryClient.""" @pytest.fixture def mock_config(self): """Create mock configuration.""" config = MagicMock() config.apis.open_library.base_url = "https://openlibrary.org" config.apis.open_library.timeout = 30 config.apis.open_library.retry_attempts = 3 config.apis.open_library.backoff_factor = 2.0 config.server.name = "test-server" config.server.version = "1.0.0" return config @pytest.fixture def client(self, mock_config): """Create OpenLibraryClient instance.""" return OpenLibraryClient(mock_config) @pytest.fixture def mock_book_data(self): """Mock book data from Open Library API.""" return { "key": "/works/OL123456W", "title": "Test Educational Book", "author_name": ["Test Author"], "first_publish_year": 2020, "isbn": ["1234567890", "9781234567890"], "publisher": ["Test Publisher"], "subject": ["Mathematics", "Education", "Elementary"], "cover_i": 12345, "number_of_pages_median": 150, "language": ["eng"], "description": "A test book for educational purposes" } def test_validate_isbn_valid(self, client): """Test ISBN validation with valid ISBNs.""" # Valid ISBN-10 assert client._validate_isbn("1234567890") == "1234567890" assert client._validate_isbn("123456789X") == "123456789X" # Valid ISBN-13 assert client._validate_isbn("9781234567890") == "9781234567890" # With hyphens and spaces assert client._validate_isbn("978-1-234-56789-0") == "9781234567890" assert client._validate_isbn("1 234 567890") == "1234567890" def test_validate_isbn_invalid(self, client): """Test ISBN validation with invalid ISBNs.""" with pytest.raises(ValidationError): client._validate_isbn("") with pytest.raises(ValidationError): client._validate_isbn("123") # Too short with pytest.raises(ValidationError): client._validate_isbn("12345678901234") # Too long with pytest.raises(ValidationError): client._validate_isbn("123456789a") # Invalid character def test_validate_search_params_valid(self, client): """Test search parameter validation with valid inputs.""" # Should not raise any exceptions client._validate_search_params("test query", 10) client._validate_search_params("mathematics", 1) client._validate_search_params("science education", 100) def test_validate_search_params_invalid(self, client): """Test search parameter validation with invalid inputs.""" with pytest.raises(ValidationError): client._validate_search_params("", 10) # Empty query with pytest.raises(ValidationError): client._validate_search_params(" ", 10) # Whitespace only with pytest.raises(ValidationError): client._validate_search_params("test", 0) # Invalid limit with pytest.raises(ValidationError): client._validate_search_params("test", 101) # Limit too high @pytest.mark.asyncio async def test_search_books_success(self, client, mock_book_data): """Test successful book search.""" mock_response = { "docs": [mock_book_data], "numFound": 1, "start": 0 } with patch.object(client, '_make_request', return_value=mock_response): results = await client.search_books("test query", limit=10) assert len(results) == 1 assert results[0]["title"] == "Test Educational Book" assert results[0]["author_name"] == ["Test Author"] @pytest.mark.asyncio async def test_search_books_validation_error(self, client): """Test book search with invalid parameters.""" with pytest.raises(ValidationError): await client.search_books("", limit=10) @pytest.mark.asyncio async def test_get_book_details_success(self, client, mock_book_data): """Test successful book details retrieval.""" with patch.object(client, '_make_request', return_value=mock_book_data): result = await client.get_book_details("9781234567890") assert result["title"] == "Test Educational Book" assert result["author_name"] == ["Test Author"] @pytest.mark.asyncio async def test_get_book_details_not_found(self, client): """Test book details retrieval when book not found.""" with patch.object(client, '_make_request', return_value={}): with patch.object(client, 'search_books', return_value=[]): result = await client.get_book_details("9781234567890") assert result == {} @pytest.mark.asyncio async def test_check_book_availability_success(self, client, mock_book_data): """Test book availability checking.""" with patch.object(client, 'get_book_details', return_value=mock_book_data): result = await client.check_book_availability("9781234567890") assert result["available"] is True assert result["status"] == "available" assert result["isbn"] == "9781234567890" @pytest.mark.asyncio async def test_check_book_availability_not_found(self, client): """Test book availability checking when book not found.""" with patch.object(client, 'get_book_details', return_value={}): result = await client.check_book_availability("9781234567890") assert result["available"] is False assert result["status"] == "not_found" @pytest.mark.asyncio async def test_health_check_success(self, client): """Test successful health check.""" with patch.object(client, 'search_books', return_value=[{"title": "test"}]): result = await client.health_check() assert result["status"] == "healthy" assert "response_time_seconds" in result assert "timestamp" in result @pytest.mark.asyncio async def test_health_check_failure(self, client): """Test health check failure.""" with patch.object(client, 'search_books', side_effect=APIError("API Error", "open_library")): result = await client.health_check() assert result["status"] == "unhealthy" assert "error" in result class TestOpenLibraryTool: """Test cases for OpenLibraryTool.""" @pytest.fixture def mock_config(self): """Create mock configuration.""" config = MagicMock() config.education.content_filters.min_educational_relevance = 0.7 config.education.content_filters.enable_age_appropriate = True config.education.content_filters.enable_curriculum_alignment = True return config @pytest.fixture def mock_services(self): """Create mock services.""" cache_service = AsyncMock() rate_limiting_service = AsyncMock() usage_service = AsyncMock() return cache_service, rate_limiting_service, usage_service @pytest.fixture def tool(self, mock_config, mock_services): """Create OpenLibraryTool instance.""" cache_service, rate_limiting_service, usage_service = mock_services return OpenLibraryTool(mock_config, cache_service, rate_limiting_service, usage_service) @pytest.fixture def sample_book(self): """Create sample Book instance.""" educational_metadata = EducationalMetadata( grade_levels=[GradeLevel.GRADES_3_5], educational_subjects=["Mathematics"], educational_relevance_score=0.8 ) return Book( id="OL123456W", title="Elementary Mathematics", authors=["Test Author"], isbn="9781234567890", publication_date=date(2020, 1, 1), subjects=["Mathematics", "Education"], educational_metadata=educational_metadata, source="open_library" ) def test_api_name(self, tool): """Test API name property.""" assert tool.api_name == "open_library" def test_calculate_educational_relevance(self, tool, sample_book): """Test educational relevance calculation.""" # Test with matching subject and grade level score = tool._calculate_educational_relevance( sample_book, target_subject="Mathematics", target_grade_level="3-5" ) assert score > 0.5 # Should have high relevance # Test with non-matching criteria score = tool._calculate_educational_relevance( sample_book, target_subject="Science", target_grade_level="9-12" ) assert score < 0.5 # Should have lower relevance def test_infer_reading_level(self, tool, sample_book): """Test reading level inference.""" reading_level = tool._infer_reading_level(sample_book) assert reading_level == "Elementary" # Test with college-level book sample_book.educational_metadata.grade_levels = [GradeLevel.COLLEGE] reading_level = tool._infer_reading_level(sample_book) assert reading_level == "College" def test_infer_difficulty_level(self, tool, sample_book): """Test difficulty level inference.""" # Test with page count sample_book.page_count = 30 difficulty = tool._infer_difficulty_level(sample_book) assert difficulty == "Beginner" sample_book.page_count = 150 difficulty = tool._infer_difficulty_level(sample_book) assert difficulty == "Intermediate" sample_book.page_count = 300 difficulty = tool._infer_difficulty_level(sample_book) assert difficulty == "Advanced" def test_enhance_subject_classification(self, tool): """Test subject classification enhancement.""" subjects = ["Mathematics", "Algebra"] enhanced = tool._enhance_subject_classification(subjects) assert "Mathematics" in enhanced assert "Algebra" in enhanced assert len(enhanced) >= len(subjects) def test_get_grade_level_search_terms(self, tool): """Test grade level search terms generation.""" terms = tool._get_grade_level_search_terms(GradeLevel.K_2) assert "kindergarten" in terms assert "elementary" in terms terms = tool._get_grade_level_search_terms(GradeLevel.COLLEGE) assert "college" in terms assert "university" in terms @pytest.mark.asyncio async def test_enrich_educational_metadata(self, tool, sample_book): """Test educational metadata enrichment.""" enriched_book = await tool._enrich_educational_metadata( sample_book, subject="Mathematics", grade_level="3-5" ) assert enriched_book.educational_metadata.educational_relevance_score > 0 assert enriched_book.educational_metadata.reading_level is not None assert enriched_book.educational_metadata.difficulty_level is not None def test_filter_age_appropriate(self, tool, sample_book): """Test age-appropriate content filtering.""" # Test with appropriate content books = [sample_book] filtered = tool._filter_age_appropriate(books, GradeLevel.GRADES_3_5) assert len(filtered) == 1 # Test with inappropriate content sample_book.title = "Violence and War Stories" filtered = tool._filter_age_appropriate(books, GradeLevel.K_2) assert len(filtered) == 0 @pytest.mark.asyncio async def test_search_educational_books_success(self, tool): """Test successful educational book search.""" mock_book_data = { "key": "/works/OL123456W", "title": "Test Book", "author_name": ["Test Author"], "subject": ["Mathematics"] } with patch.object(tool.client, 'search_books', return_value=[mock_book_data]): with patch.object(tool, 'execute_with_monitoring') as mock_execute: mock_execute.return_value = [{"title": "Test Book"}] result = await tool.search_educational_books( query="mathematics", subject="Mathematics", grade_level="3-5", limit=10 ) mock_execute.assert_called_once() assert isinstance(result, list) @pytest.mark.asyncio async def test_get_book_details_by_isbn_success(self, tool): """Test successful book details retrieval by ISBN.""" mock_book_data = { "key": "/works/OL123456W", "title": "Test Book", "author_name": ["Test Author"] } with patch.object(tool.client, 'get_book_details', return_value=mock_book_data): with patch.object(tool.client, 'get_book_cover', return_value="http://example.com/cover.jpg"): with patch.object(tool.client, 'check_book_availability', return_value={"available": True}): with patch.object(tool, 'execute_with_monitoring') as mock_execute: mock_execute.return_value = {"title": "Test Book"} result = await tool.get_book_details_by_isbn( isbn="9781234567890", include_cover=True ) mock_execute.assert_called_once() assert isinstance(result, dict) @pytest.mark.asyncio async def test_health_check_success(self, tool): """Test successful tool health check.""" with patch.object(tool.client, 'health_check', return_value={"status": "healthy"}): with patch.object(tool.client, 'search_books', return_value=[{"title": "test"}]): result = await tool.health_check() assert result["status"] == "healthy" assert "api_health" in result assert "test_search_results" in result @pytest.mark.asyncio async def test_health_check_failure(self, tool): """Test tool health check failure.""" with patch.object(tool.client, 'health_check', side_effect=Exception("Test error")): result = await tool.health_check() assert result["status"] == "unhealthy" assert "error" in result class TestBookModel: """Test cases for Book model Open Library integration.""" @pytest.fixture def mock_ol_data(self): """Mock Open Library API response data.""" return { "key": "/works/OL123456W", "title": "Test Educational Book", "author_name": ["Test Author", "Another Author"], "first_publish_year": 2020, "isbn": ["1234567890", "9781234567890"], "publisher": ["Test Publisher"], "subject": ["Mathematics", "Education", "Elementary"], "cover_i": 12345, "number_of_pages_median": 150, "language": ["eng"], "description": "A comprehensive educational book for elementary mathematics." } def test_from_open_library_basic(self, mock_ol_data): """Test basic Book creation from Open Library data.""" book = Book.from_open_library(mock_ol_data) assert book.id == "OL123456W" assert book.title == "Test Educational Book" assert book.authors == ["Test Author", "Another Author"] assert book.isbn == "1234567890" assert book.isbn13 == "9781234567890" assert book.publication_date == date(2020, 1, 1) assert book.subjects == ["Mathematics", "Education", "Elementary"] assert book.source == "open_library" def test_from_open_library_educational_metadata(self, mock_ol_data): """Test educational metadata inference from Open Library data.""" book = Book.from_open_library(mock_ol_data) # Should infer grade levels from subjects assert len(book.educational_metadata.grade_levels) > 0 assert book.educational_metadata.educational_subjects == ["Mathematics", "Education", "Elementary"] def test_from_open_library_cover_url(self, mock_ol_data): """Test cover URL generation from Open Library data.""" book = Book.from_open_library(mock_ol_data) expected_cover_url = "https://covers.openlibrary.org/b/id/12345-L.jpg" assert book.cover_url == expected_cover_url def test_from_open_library_missing_data(self): """Test Book creation with minimal Open Library data.""" minimal_data = { "key": "/works/OL123456W", "title": "Minimal Book" } book = Book.from_open_library(minimal_data) assert book.id == "OL123456W" assert book.title == "Minimal Book" assert book.authors == [] assert book.isbn is None assert book.subjects == [] def test_is_suitable_for_grade_level(self, mock_ol_data): """Test grade level suitability checking.""" book = Book.from_open_library(mock_ol_data) # Add specific grade level book.educational_metadata.grade_levels = [GradeLevel.GRADES_3_5] assert book.is_suitable_for_grade_level(GradeLevel.GRADES_3_5) assert not book.is_suitable_for_grade_level(GradeLevel.COLLEGE) def test_has_subject(self, mock_ol_data): """Test subject checking.""" book = Book.from_open_library(mock_ol_data) assert book.has_subject("Mathematics") assert book.has_subject("math") # Case insensitive assert not book.has_subject("Science") def test_get_educational_score(self, mock_ol_data): """Test educational score calculation.""" book = Book.from_open_library(mock_ol_data) # Add some educational metadata book.educational_metadata.grade_levels = [GradeLevel.GRADES_3_5] book.educational_metadata.educational_relevance_score = 0.5 book.lexile_score = 600 score = book.get_educational_score() assert 0.0 <= score <= 1.0 assert score > 0.5 # Should be boosted by metadata if __name__ == "__main__": pytest.main([__file__])

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Cicatriiz/openedu-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server