feat: implement production-ready features from improvement plan phase 2.5 & 2.6

Phase 2.5: Fix Foundation (CRITICAL)
- Fixed 4 failing tests by adding cache attribute to mock_client fixture
- Created comprehensive cache tests for Pages endpoint (test_pages_cache.py)
- Added missing dependencies: pydantic[email] and aiohttp to core requirements
- Updated requirements.txt with proper dependency versions
- Achieved 82.67% test coverage with 454 passing tests

Phase 2.6: Production Essentials
- Implemented structured logging (wikijs/logging.py)
  * JSON and text log formatters
  * Configurable log levels and output destinations
  * Integration with client operations

- Implemented metrics and telemetry (wikijs/metrics.py)
  * Request tracking with duration, status codes, errors
  * Latency percentiles (min, max, avg, p50, p95, p99)
  * Error rate calculation
  * Thread-safe metrics collection

- Implemented rate limiting (wikijs/ratelimit.py)
  * Token bucket algorithm for request throttling
  * Per-endpoint rate limiting support
  * Configurable timeout handling
  * Burst capacity management

- Created SECURITY.md policy
  * Vulnerability reporting procedures
  * Security best practices
  * Response timelines
  * Supported versions

Documentation
- Added comprehensive logging guide (docs/logging.md)
- Added metrics and telemetry guide (docs/metrics.md)
- Added rate limiting guide (docs/rate_limiting.md)
- Updated README.md with production features section
- Updated IMPROVEMENT_PLAN_2.md with completed checkboxes

Testing
- Created test suite for logging (tests/test_logging.py)
- Created test suite for metrics (tests/test_metrics.py)
- Created test suite for rate limiting (tests/test_ratelimit.py)
- All 454 tests passing
- Test coverage: 82.67%

Breaking Changes: None
Dependencies Added: pydantic[email], email-validator, dnspython

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Claude
2025-10-23 16:45:02 +00:00
parent 6fbd24d737
commit cef6903cbc
15 changed files with 1278 additions and 40 deletions

View File

@@ -17,6 +17,7 @@ class TestPagesEndpoint:
def mock_client(self):
"""Create a mock WikiJS client."""
client = Mock(spec=WikiJSClient)
client.cache = None
return client
@pytest.fixture

View File

@@ -0,0 +1,175 @@
"""Tests for Pages endpoint caching functionality."""
import pytest
from unittest.mock import MagicMock, Mock
from wikijs.cache import MemoryCache, CacheKey
from wikijs.endpoints.pages import PagesEndpoint
from wikijs.models import Page
class TestPagesCaching:
"""Test caching behavior in Pages endpoint."""
def test_get_with_cache_hit(self):
"""Test page retrieval uses cache when available."""
# Setup
cache = MemoryCache(ttl=300)
client = MagicMock()
client.cache = cache
client._request = MagicMock()
pages = PagesEndpoint(client)
# Pre-populate cache
page_data = {"id": 123, "title": "Test", "path": "test"}
cache_key = CacheKey("page", "123", "get")
cache.set(cache_key, page_data)
# Execute
result = pages.get(123)
# Verify cache was used, not API
client._request.assert_not_called()
assert result["id"] == 123
def test_get_with_cache_miss(self):
"""Test page retrieval calls API on cache miss."""
# Setup
cache = MemoryCache(ttl=300)
client = MagicMock()
client.cache = cache
# Mock the _post method on the endpoint
pages = PagesEndpoint(client)
pages._post = Mock(return_value={
"data": {"pages": {"single": {
"id": 123,
"title": "Test",
"path": "test",
"content": "Test content",
"description": "Test desc",
"isPublished": True,
"isPrivate": False,
"tags": [],
"locale": "en",
"authorId": 1,
"authorName": "Test User",
"authorEmail": "test@example.com",
"editor": "markdown",
"createdAt": "2023-01-01T00:00:00Z",
"updatedAt": "2023-01-02T00:00:00Z"
}}}
})
# Execute
result = pages.get(123)
# Verify API was called
pages._post.assert_called_once()
# Verify result was cached
cache_key = CacheKey("page", "123", "get")
cached = cache.get(cache_key)
assert cached is not None
def test_update_invalidates_cache(self):
"""Test page update invalidates cache."""
# Setup
cache = MemoryCache(ttl=300)
client = MagicMock()
client.cache = cache
pages = PagesEndpoint(client)
pages._post = Mock(return_value={
"data": {"updatePage": {
"id": 123,
"title": "New",
"path": "test",
"content": "Updated content",
"description": "Updated desc",
"isPublished": True,
"isPrivate": False,
"tags": [],
"locale": "en",
"authorId": 1,
"authorName": "Test User",
"authorEmail": "test@example.com",
"editor": "markdown",
"createdAt": "2023-01-01T00:00:00Z",
"updatedAt": "2023-01-02T00:00:00Z"
}}
})
# Pre-populate cache
cache_key = CacheKey("page", "123", "get")
cache.set(cache_key, {"id": 123, "title": "Old"})
# Verify cache is populated
assert cache.get(cache_key) is not None
# Execute update
pages.update(123, {"title": "New"})
# Verify cache was invalidated
cached = cache.get(cache_key)
assert cached is None
def test_delete_invalidates_cache(self):
"""Test page delete invalidates cache."""
# Setup
cache = MemoryCache(ttl=300)
client = MagicMock()
client.cache = cache
pages = PagesEndpoint(client)
pages._post = Mock(return_value={
"data": {"deletePage": {"success": True}}
})
# Pre-populate cache
cache_key = CacheKey("page", "123", "get")
cache.set(cache_key, {"id": 123, "title": "Test"})
# Verify cache is populated
assert cache.get(cache_key) is not None
# Execute delete
pages.delete(123)
# Verify cache was invalidated
cached = cache.get(cache_key)
assert cached is None
def test_get_without_cache(self):
"""Test page retrieval without cache configured."""
# Setup
client = MagicMock()
client.cache = None
pages = PagesEndpoint(client)
pages._post = Mock(return_value={
"data": {"pages": {"single": {
"id": 123,
"title": "Test",
"path": "test",
"content": "Test content",
"description": "Test desc",
"isPublished": True,
"isPrivate": False,
"tags": [],
"locale": "en",
"authorId": 1,
"authorName": "Test User",
"authorEmail": "test@example.com",
"editor": "markdown",
"createdAt": "2023-01-01T00:00:00Z",
"updatedAt": "2023-01-02T00:00:00Z"
}}}
})
# Execute
result = pages.get(123)
# Verify API was called
pages._post.assert_called_once()
assert result.id == 123