Skip to main content
Glama
test_base_auto_pagination.py12.2 kB
"""Tests for BaseClient.auto_paginate method.""" import os from typing import TypedDict from unittest.mock import AsyncMock, MagicMock, patch import pytest from client.base import BaseClient class TestResponse(TypedDict): """Test response type for auto-pagination tests.""" title: str id: int class TestableClient(BaseClient): """Testable subclass of BaseClient for direct testing.""" pass @pytest.mark.asyncio async def test_auto_paginate_single_page(): """Test auto-pagination with a single page of results.""" mock_response = MagicMock() mock_response.json.return_value = [ {"title": "Item 1", "id": 1}, {"title": "Item 2", "id": 2}, ] mock_response.headers = { "X-Pagination-Page": "1", "X-Pagination-Limit": "2", "X-Pagination-Page-Count": "1", "X-Pagination-Item-Count": "2", } mock_response.raise_for_status = MagicMock() with ( patch("httpx.AsyncClient") as mock_client_class, patch.dict( os.environ, {"TRAKT_CLIENT_ID": "test_id", "TRAKT_CLIENT_SECRET": "test_secret"}, ), ): mock_instance = MagicMock() mock_instance.get = AsyncMock(return_value=mock_response) mock_instance.aclose = AsyncMock() mock_client_class.return_value = mock_instance client = TestableClient() result = await client.auto_paginate( "/test/endpoint", response_type=TestResponse, params={"limit": 2} ) # Should return all items from single page assert isinstance(result, list) assert len(result) == 2 assert result[0]["title"] == "Item 1" assert result[1]["title"] == "Item 2" # Should only make one request assert mock_instance.get.call_count == 1 @pytest.mark.asyncio async def test_auto_paginate_multiple_pages(): """Test auto-pagination across multiple pages.""" # Mock first page mock_response_page1 = MagicMock() mock_response_page1.json.return_value = [ {"title": "Item 1", "id": 1}, {"title": "Item 2", "id": 2}, ] mock_response_page1.headers = { "X-Pagination-Page": "1", "X-Pagination-Limit": "2", "X-Pagination-Page-Count": "3", "X-Pagination-Item-Count": "5", } mock_response_page1.raise_for_status = MagicMock() # Mock second page mock_response_page2 = MagicMock() mock_response_page2.json.return_value = [ {"title": "Item 3", "id": 3}, {"title": "Item 4", "id": 4}, ] mock_response_page2.headers = { "X-Pagination-Page": "2", "X-Pagination-Limit": "2", "X-Pagination-Page-Count": "3", "X-Pagination-Item-Count": "5", } mock_response_page2.raise_for_status = MagicMock() # Mock third page (last page with 1 item) mock_response_page3 = MagicMock() mock_response_page3.json.return_value = [ {"title": "Item 5", "id": 5}, ] mock_response_page3.headers = { "X-Pagination-Page": "3", "X-Pagination-Limit": "2", "X-Pagination-Page-Count": "3", "X-Pagination-Item-Count": "5", } mock_response_page3.raise_for_status = MagicMock() with ( patch("httpx.AsyncClient") as mock_client_class, patch.dict( os.environ, {"TRAKT_CLIENT_ID": "test_id", "TRAKT_CLIENT_SECRET": "test_secret"}, ), ): mock_instance = MagicMock() mock_instance.get = AsyncMock( side_effect=[mock_response_page1, mock_response_page2, mock_response_page3] ) mock_instance.aclose = AsyncMock() mock_client_class.return_value = mock_instance client = TestableClient() result = await client.auto_paginate( "/test/endpoint", response_type=TestResponse, params={"limit": 2} ) # Should return all items from all pages assert isinstance(result, list) assert len(result) == 5 assert result[0]["title"] == "Item 1" assert result[2]["title"] == "Item 3" assert result[4]["title"] == "Item 5" # Should make exactly 3 requests (one per page) assert mock_instance.get.call_count == 3 @pytest.mark.asyncio async def test_auto_paginate_empty_results(): """Test auto-pagination with empty results.""" mock_response = MagicMock() mock_response.json.return_value = [] mock_response.headers = { "X-Pagination-Page": "1", "X-Pagination-Limit": "10", "X-Pagination-Page-Count": "1", "X-Pagination-Item-Count": "0", } mock_response.raise_for_status = MagicMock() with ( patch("httpx.AsyncClient") as mock_client_class, patch.dict( os.environ, {"TRAKT_CLIENT_ID": "test_id", "TRAKT_CLIENT_SECRET": "test_secret"}, ), ): mock_instance = MagicMock() mock_instance.get = AsyncMock(return_value=mock_response) mock_instance.aclose = AsyncMock() mock_client_class.return_value = mock_instance client = TestableClient() result = await client.auto_paginate( "/test/endpoint", response_type=TestResponse, params={"limit": 10} ) # Should return empty list assert isinstance(result, list) assert len(result) == 0 # Should only make one request assert mock_instance.get.call_count == 1 @pytest.mark.asyncio async def test_auto_paginate_uses_server_next_page(): """Test that auto-pagination uses server-provided next_page property.""" # Mock first page - server indicates page 5 as next mock_response_page1 = MagicMock() mock_response_page1.json.return_value = [{"title": "Item 1", "id": 1}] mock_response_page1.headers = { "X-Pagination-Page": "3", "X-Pagination-Limit": "1", "X-Pagination-Page-Count": "5", "X-Pagination-Item-Count": "5", } mock_response_page1.raise_for_status = MagicMock() # Mock second page - last page mock_response_page2 = MagicMock() mock_response_page2.json.return_value = [{"title": "Item 2", "id": 2}] mock_response_page2.headers = { "X-Pagination-Page": "5", "X-Pagination-Limit": "1", "X-Pagination-Page-Count": "5", "X-Pagination-Item-Count": "5", } mock_response_page2.raise_for_status = MagicMock() with ( patch("httpx.AsyncClient") as mock_client_class, patch.dict( os.environ, {"TRAKT_CLIENT_ID": "test_id", "TRAKT_CLIENT_SECRET": "test_secret"}, ), ): mock_instance = MagicMock() mock_instance.get = AsyncMock( side_effect=[mock_response_page1, mock_response_page2] ) mock_instance.aclose = AsyncMock() mock_client_class.return_value = mock_instance client = TestableClient() result = await client.auto_paginate( "/test/endpoint", response_type=TestResponse, params={"limit": 1} ) # Should successfully fetch both pages assert len(result) == 2 # Verify the page parameters sent in requests calls = mock_instance.get.call_args_list assert len(calls) == 2 # First call should be page 1 (initial page) first_call_params = calls[0].kwargs.get("params", {}) assert first_call_params["page"] == 1 # Second call should be page 4 (server's next_page from page 3) second_call_params = calls[1].kwargs.get("params", {}) assert second_call_params["page"] == 4 @pytest.mark.asyncio async def test_auto_paginate_preserves_params(): """Test that auto-pagination preserves additional query parameters.""" mock_response = MagicMock() mock_response.json.return_value = [{"title": "Item 1", "id": 1}] mock_response.headers = { "X-Pagination-Page": "1", "X-Pagination-Limit": "1", "X-Pagination-Page-Count": "1", "X-Pagination-Item-Count": "1", } mock_response.raise_for_status = MagicMock() with ( patch("httpx.AsyncClient") as mock_client_class, patch.dict( os.environ, {"TRAKT_CLIENT_ID": "test_id", "TRAKT_CLIENT_SECRET": "test_secret"}, ), ): mock_instance = MagicMock() mock_instance.get = AsyncMock(return_value=mock_response) mock_instance.aclose = AsyncMock() mock_client_class.return_value = mock_instance client = TestableClient() result = await client.auto_paginate( "/test/endpoint", response_type=TestResponse, params={"limit": 1, "period": "weekly", "sort": "newest"}, ) # Should return results assert len(result) == 1 # Verify params were preserved call_params = mock_instance.get.call_args.kwargs.get("params", {}) assert call_params["limit"] == 1 assert call_params["period"] == "weekly" assert call_params["sort"] == "newest" assert call_params["page"] == 1 @pytest.mark.asyncio async def test_auto_paginate_max_pages_safety_guard(): """Test that auto-pagination stops at max_pages safety limit.""" # Create responses that suggest infinite pagination def create_mock_response(page_num: int) -> MagicMock: mock_response = MagicMock() mock_response.json.return_value = [ {"title": f"Item {page_num}", "id": page_num} ] mock_response.headers = { "X-Pagination-Page": str(page_num), "X-Pagination-Limit": "1", "X-Pagination-Page-Count": "999", # Very large page count "X-Pagination-Item-Count": "999", } mock_response.raise_for_status = MagicMock() return mock_response with ( patch("httpx.AsyncClient") as mock_client_class, patch.dict( os.environ, {"TRAKT_CLIENT_ID": "test_id", "TRAKT_CLIENT_SECRET": "test_secret"}, ), ): mock_instance = MagicMock() # Create infinite sequence of responses mock_instance.get = AsyncMock( side_effect=[create_mock_response(i) for i in range(1, 200)] ) mock_instance.aclose = AsyncMock() mock_client_class.return_value = mock_instance client = TestableClient() # Should raise RuntimeError when hitting max_pages limit with pytest.raises( RuntimeError, match="Pagination safety limit reached: 5 pages fetched", ): await client.auto_paginate( "/test/endpoint", response_type=TestResponse, params={"limit": 1}, max_pages=5, ) # Should have made exactly 5 requests (max_pages) assert mock_instance.get.call_count == 5 @pytest.mark.asyncio async def test_auto_paginate_with_none_params(): """Test auto-pagination with None params (should use empty dict).""" mock_response = MagicMock() mock_response.json.return_value = [{"title": "Item 1", "id": 1}] mock_response.headers = { "X-Pagination-Page": "1", "X-Pagination-Limit": "10", "X-Pagination-Page-Count": "1", "X-Pagination-Item-Count": "1", } mock_response.raise_for_status = MagicMock() with ( patch("httpx.AsyncClient") as mock_client_class, patch.dict( os.environ, {"TRAKT_CLIENT_ID": "test_id", "TRAKT_CLIENT_SECRET": "test_secret"}, ), ): mock_instance = MagicMock() mock_instance.get = AsyncMock(return_value=mock_response) mock_instance.aclose = AsyncMock() mock_client_class.return_value = mock_instance client = TestableClient() result = await client.auto_paginate( "/test/endpoint", response_type=TestResponse, params=None ) # Should return results assert len(result) == 1 # Verify params include page even when None was passed call_params = mock_instance.get.call_args.kwargs.get("params", {}) assert call_params["page"] == 1

Latest Blog Posts

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/wwiens/trakt_mcpserver'

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