Phase 2, Task 2.1, Step 4 Complete: Async Testing Suite
This commit adds a complete test suite for the async client and
async pages endpoint, achieving 100% pass rate for async functionality.
Test Coverage:
--------------
1. **AsyncWikiJSClient Tests** (test_async_client.py)
- Initialization tests (5 tests)
* API key string initialization
* Auth handler initialization
* Invalid auth parameter handling
* Custom settings configuration
* Pages endpoint availability
- HTTP Request tests (5 tests)
* Successful API requests
* 401 authentication errors
* API error handling (500 errors)
* Connection error handling
* Timeout error handling
- Connection Testing (4 tests)
* Successful connection test
* GraphQL error handling
* Invalid response format detection
* Missing configuration detection
- Context Manager tests (2 tests)
* Async context manager protocol
* Manual close handling
- Session Creation tests (3 tests)
* Session creation and configuration
* Lazy session initialization
* Session reuse
2. **AsyncPagesEndpoint Tests** (test_async_pages.py)
- Initialization test
- List operations (3 tests)
* Basic listing
* Parameterized filtering
* Validation errors
- Get operations (3 tests)
* Get by ID
* Validation errors
* Not found handling
- Get by path operation
- Create operations (2 tests)
* Successful creation
* Failed creation handling
- Update operation
- Delete operations (2 tests)
* Successful deletion
* Failed deletion handling
- Search operation
- Get by tags operation
- GraphQL error handling
- Data normalization tests (2 tests)
Bug Fixes:
----------
- Fixed exception handling order in AsyncWikiJSClient._request()
* ServerTimeoutError now caught before ClientConnectionError
* Prevents timeout errors being misclassified as connection errors
- Fixed test mocking for async context managers
* Properly mock __aenter__ and __aexit__ methods
* Fixed session creation in async context
Test Results:
-------------
✅ 37/37 tests passing (100% pass rate)
✅ Async client tests: 19/19 passing
✅ Async pages tests: 18/18 passing
✅ 53% overall code coverage (includes async code)
✅ Zero flake8 errors
✅ All imports successful
Quality Metrics:
----------------
- Test coverage for async module: >85%
- All edge cases covered (errors, validation, not found)
- Proper async/await usage throughout
- Mock objects properly configured
- Clean test structure and organization
Next Steps:
-----------
- Create async usage examples
- Write async documentation
- Performance benchmarks (async vs sync)
- Integration tests with real Wiki.js instance
This establishes a solid foundation for async development
with comprehensive test coverage ensuring reliability.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
308 lines
11 KiB
Python
308 lines
11 KiB
Python
"""Tests for AsyncWikiJSClient."""
|
|
|
|
import json
|
|
from unittest.mock import AsyncMock, Mock, patch
|
|
|
|
import aiohttp
|
|
import pytest
|
|
|
|
from wikijs.aio import AsyncWikiJSClient
|
|
from wikijs.auth import APIKeyAuth
|
|
from wikijs.exceptions import (
|
|
APIError,
|
|
AuthenticationError,
|
|
ConfigurationError,
|
|
ConnectionError,
|
|
TimeoutError,
|
|
)
|
|
|
|
|
|
class TestAsyncWikiJSClientInit:
|
|
"""Test AsyncWikiJSClient initialization."""
|
|
|
|
def test_init_with_api_key_string(self):
|
|
"""Test initialization with API key string."""
|
|
client = AsyncWikiJSClient("https://wiki.example.com", auth="test-key")
|
|
|
|
assert client.base_url == "https://wiki.example.com"
|
|
assert isinstance(client._auth_handler, APIKeyAuth)
|
|
assert client.timeout == 30
|
|
assert client.verify_ssl is True
|
|
assert "wikijs-python-sdk" in client.user_agent
|
|
|
|
def test_init_with_auth_handler(self):
|
|
"""Test initialization with auth handler."""
|
|
auth_handler = APIKeyAuth("test-key")
|
|
client = AsyncWikiJSClient("https://wiki.example.com", auth=auth_handler)
|
|
|
|
assert client._auth_handler is auth_handler
|
|
|
|
def test_init_invalid_auth(self):
|
|
"""Test initialization with invalid auth parameter."""
|
|
with pytest.raises(ConfigurationError, match="Invalid auth parameter"):
|
|
AsyncWikiJSClient("https://wiki.example.com", auth=123)
|
|
|
|
def test_init_with_custom_settings(self):
|
|
"""Test initialization with custom settings."""
|
|
client = AsyncWikiJSClient(
|
|
"https://wiki.example.com",
|
|
auth="test-key",
|
|
timeout=60,
|
|
verify_ssl=False,
|
|
user_agent="Custom Agent",
|
|
)
|
|
|
|
assert client.timeout == 60
|
|
assert client.verify_ssl is False
|
|
assert client.user_agent == "Custom Agent"
|
|
|
|
def test_has_pages_endpoint(self):
|
|
"""Test that client has pages endpoint."""
|
|
client = AsyncWikiJSClient("https://wiki.example.com", auth="test-key")
|
|
|
|
assert hasattr(client, "pages")
|
|
assert client.pages._client is client
|
|
|
|
|
|
class TestAsyncWikiJSClientRequest:
|
|
"""Test AsyncWikiJSClient HTTP request methods."""
|
|
|
|
@pytest.fixture
|
|
def client(self):
|
|
"""Create test client."""
|
|
return AsyncWikiJSClient("https://wiki.example.com", auth="test-key")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_successful_request(self, client):
|
|
"""Test successful API request."""
|
|
mock_response = AsyncMock()
|
|
mock_response.status = 200
|
|
# Response returns full data structure
|
|
mock_response.json = AsyncMock(return_value={"data": {"result": "success"}})
|
|
|
|
# Create a context manager mock
|
|
mock_ctx_manager = AsyncMock()
|
|
mock_ctx_manager.__aenter__.return_value = mock_response
|
|
mock_ctx_manager.__aexit__.return_value = False
|
|
|
|
with patch.object(client, "_get_session") as mock_get_session:
|
|
mock_session = Mock()
|
|
mock_session.request = Mock(return_value=mock_ctx_manager)
|
|
mock_get_session.return_value = mock_session
|
|
|
|
result = await client._request("GET", "/test")
|
|
|
|
# parse_wiki_response returns full response if no errors
|
|
assert result == {"data": {"result": "success"}}
|
|
mock_session.request.assert_called_once()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_authentication_error(self, client):
|
|
"""Test 401 authentication error."""
|
|
mock_response = AsyncMock()
|
|
mock_response.status = 401
|
|
|
|
# Create a context manager mock
|
|
mock_ctx_manager = AsyncMock()
|
|
mock_ctx_manager.__aenter__.return_value = mock_response
|
|
mock_ctx_manager.__aexit__.return_value = False
|
|
|
|
with patch.object(client, "_get_session") as mock_get_session:
|
|
mock_session = Mock()
|
|
mock_session.request = Mock(return_value=mock_ctx_manager)
|
|
mock_get_session.return_value = mock_session
|
|
|
|
with pytest.raises(AuthenticationError, match="Authentication failed"):
|
|
await client._request("GET", "/test")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_api_error(self, client):
|
|
"""Test API error handling."""
|
|
mock_response = AsyncMock()
|
|
mock_response.status = 500
|
|
mock_response.text = AsyncMock(return_value="Internal Server Error")
|
|
|
|
# Create a context manager mock
|
|
mock_ctx_manager = AsyncMock()
|
|
mock_ctx_manager.__aenter__.return_value = mock_response
|
|
mock_ctx_manager.__aexit__.return_value = False
|
|
|
|
with patch.object(client, "_get_session") as mock_get_session:
|
|
mock_session = Mock()
|
|
mock_session.request = Mock(return_value=mock_ctx_manager)
|
|
mock_get_session.return_value = mock_session
|
|
|
|
with pytest.raises(APIError):
|
|
await client._request("GET", "/test")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_connection_error(self, client):
|
|
"""Test connection error handling."""
|
|
with patch.object(client, "_get_session") as mock_get_session:
|
|
mock_session = Mock()
|
|
mock_session.request = Mock(
|
|
side_effect=aiohttp.ClientConnectionError("Connection failed")
|
|
)
|
|
mock_get_session.return_value = mock_session
|
|
|
|
with pytest.raises(ConnectionError, match="Failed to connect"):
|
|
await client._request("GET", "/test")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_timeout_error(self, client):
|
|
"""Test timeout error handling."""
|
|
with patch.object(client, "_get_session") as mock_get_session:
|
|
mock_session = Mock()
|
|
mock_session.request = Mock(
|
|
side_effect=aiohttp.ServerTimeoutError("Timeout")
|
|
)
|
|
mock_get_session.return_value = mock_session
|
|
|
|
with pytest.raises(TimeoutError, match="timed out"):
|
|
await client._request("GET", "/test")
|
|
|
|
|
|
class TestAsyncWikiJSClientTestConnection:
|
|
"""Test AsyncWikiJSClient connection testing."""
|
|
|
|
@pytest.fixture
|
|
def client(self):
|
|
"""Create test client."""
|
|
return AsyncWikiJSClient("https://wiki.example.com", auth="test-key")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_successful_connection(self, client):
|
|
"""Test successful connection test."""
|
|
mock_response = {"data": {"site": {"title": "Test Wiki"}}}
|
|
|
|
with patch.object(client, "_request", new_callable=AsyncMock) as mock_request:
|
|
mock_request.return_value = mock_response
|
|
|
|
result = await client.test_connection()
|
|
|
|
assert result is True
|
|
mock_request.assert_called_once()
|
|
args, kwargs = mock_request.call_args
|
|
assert args[0] == "POST"
|
|
assert args[1] == "/graphql"
|
|
assert "query" in kwargs["json_data"]
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_connection_graphql_error(self, client):
|
|
"""Test connection with GraphQL error."""
|
|
mock_response = {"errors": [{"message": "Unauthorized"}]}
|
|
|
|
with patch.object(client, "_request", new_callable=AsyncMock) as mock_request:
|
|
mock_request.return_value = mock_response
|
|
|
|
with pytest.raises(AuthenticationError, match="GraphQL query failed"):
|
|
await client.test_connection()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_connection_invalid_response(self, client):
|
|
"""Test connection with invalid response."""
|
|
mock_response = {"data": {}} # Missing 'site' key
|
|
|
|
with patch.object(client, "_request", new_callable=AsyncMock) as mock_request:
|
|
mock_request.return_value = mock_response
|
|
|
|
with pytest.raises(APIError, match="Unexpected response format"):
|
|
await client.test_connection()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_connection_no_base_url(self):
|
|
"""Test connection with no base URL."""
|
|
client = AsyncWikiJSClient("https://wiki.example.com", auth="test-key")
|
|
client.base_url = None
|
|
|
|
with pytest.raises(ConfigurationError, match="Base URL not configured"):
|
|
await client.test_connection()
|
|
|
|
|
|
class TestAsyncWikiJSClientContextManager:
|
|
"""Test AsyncWikiJSClient async context manager."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_context_manager(self):
|
|
"""Test async context manager."""
|
|
client = AsyncWikiJSClient("https://wiki.example.com", auth="test-key")
|
|
|
|
# Mock the session
|
|
mock_session = AsyncMock()
|
|
mock_session.closed = False
|
|
|
|
with patch.object(client, "_create_session", return_value=mock_session):
|
|
async with client as ctx_client:
|
|
assert ctx_client is client
|
|
assert client._session is mock_session
|
|
|
|
# Check that close was called
|
|
mock_session.close.assert_called_once()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_manual_close(self):
|
|
"""Test manual close."""
|
|
client = AsyncWikiJSClient("https://wiki.example.com", auth="test-key")
|
|
|
|
# Mock the session
|
|
mock_session = AsyncMock()
|
|
mock_session.closed = False
|
|
client._session = mock_session
|
|
|
|
await client.close()
|
|
|
|
mock_session.close.assert_called_once()
|
|
|
|
|
|
class TestAsyncWikiJSClientSessionCreation:
|
|
"""Test AsyncWikiJSClient session creation."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_session(self):
|
|
"""Test session creation."""
|
|
client = AsyncWikiJSClient("https://wiki.example.com", auth="test-key")
|
|
|
|
session = client._create_session()
|
|
|
|
assert isinstance(session, aiohttp.ClientSession)
|
|
assert "wikijs-python-sdk" in session.headers["User-Agent"]
|
|
assert session.headers["Accept"] == "application/json"
|
|
assert session.headers["Content-Type"] == "application/json"
|
|
|
|
# Clean up
|
|
await session.close()
|
|
if client._connector:
|
|
await client._connector.close()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_session_creates_if_none(self):
|
|
"""Test get_session creates session if none exists."""
|
|
client = AsyncWikiJSClient("https://wiki.example.com", auth="test-key")
|
|
|
|
assert client._session is None
|
|
|
|
session = client._get_session()
|
|
|
|
assert session is not None
|
|
assert isinstance(session, aiohttp.ClientSession)
|
|
|
|
# Clean up
|
|
await session.close()
|
|
if client._connector:
|
|
await client._connector.close()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_session_reuses_existing(self):
|
|
"""Test get_session reuses existing session."""
|
|
client = AsyncWikiJSClient("https://wiki.example.com", auth="test-key")
|
|
|
|
session1 = client._get_session()
|
|
session2 = client._get_session()
|
|
|
|
assert session1 is session2
|
|
|
|
# Clean up
|
|
await session1.close()
|
|
if client._connector:
|
|
await client._connector.close()
|