Fix 3 critical issues identified in repository review

**Critical Fixes:**

1. **Fixed Error Hierarchy** (wikijs/exceptions.py)
   - ConnectionError and TimeoutError now properly inherit from APIError
   - Ensures consistent exception handling across the SDK
   - Added proper __init__ methods with status_code=None

2. **Fixed test_connection Method** (wikijs/client.py)
   - Changed from basic HTTP GET to proper GraphQL query validation
   - Now uses query { site { title } } to validate API connectivity
   - Provides better error messages for authentication failures
   - Validates both connectivity AND API access

3. **Implemented JWT Token Refresh** (wikijs/auth/jwt.py)
   - Added base_url parameter to JWTAuth class
   - Implemented complete refresh() method with HTTP request to /api/auth/refresh
   - Handles token, refresh token, and expiration updates
   - Proper error handling for network failures and auth errors

**Bonus Fixes:**
- Dynamic user agent version (uses __version__ from version.py instead of hardcoded)
- Updated all JWT tests to include required base_url parameter
- Updated test mocks to match new GraphQL-based test_connection

**Test Results:**
- All 231 tests passing 
- Test coverage: 91.64% (target: 85%) 
- No test failures or errors

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Claude
2025-10-22 17:44:44 +00:00
parent 3e2430fbe0
commit 66f4471e53
6 changed files with 197 additions and 94 deletions

View File

@@ -19,17 +19,23 @@ class JWTAuth(AuthHandler):
Args:
token: The JWT token string.
base_url: The base URL of the Wiki.js instance (needed for token refresh).
refresh_token: Optional refresh token for automatic renewal.
expires_at: Optional expiration timestamp (Unix timestamp).
Example:
>>> auth = JWTAuth("eyJ0eXAiOiJKV1QiLCJhbGc...")
>>> auth = JWTAuth(
... token="eyJ0eXAiOiJKV1QiLCJhbGc...",
... base_url="https://wiki.example.com",
... refresh_token="refresh_token_here"
... )
>>> client = WikiJSClient("https://wiki.example.com", auth=auth)
"""
def __init__(
self,
token: str,
base_url: str,
refresh_token: Optional[str] = None,
expires_at: Optional[float] = None,
) -> None:
@@ -37,16 +43,21 @@ class JWTAuth(AuthHandler):
Args:
token: The JWT token string.
base_url: The base URL of the Wiki.js instance.
refresh_token: Optional refresh token for automatic renewal.
expires_at: Optional expiration timestamp (Unix timestamp).
Raises:
ValueError: If token is empty or None.
ValueError: If token or base_url is empty or None.
"""
if not token or not token.strip():
raise ValueError("JWT token cannot be empty")
if not base_url or not base_url.strip():
raise ValueError("Base URL cannot be empty")
self._token = token.strip()
self._base_url = base_url.strip().rstrip("/")
self._refresh_token = refresh_token.strip() if refresh_token else None
self._expires_at = expires_at
self._refresh_buffer = 300 # Refresh 5 minutes before expiration
@@ -91,15 +102,13 @@ class JWTAuth(AuthHandler):
def refresh(self) -> None:
"""Refresh the JWT token using the refresh token.
This method attempts to refresh the JWT token using the refresh token.
If no refresh token is available, it raises an AuthenticationError.
Note: This is a placeholder implementation. In a real implementation,
this would make an HTTP request to the Wiki.js token refresh endpoint.
This method attempts to refresh the JWT token using the refresh token
by making a request to the Wiki.js authentication endpoint.
Raises:
AuthenticationError: If refresh token is not available or refresh fails.
"""
import requests
from ..exceptions import AuthenticationError
if not self._refresh_token:
@@ -107,16 +116,57 @@ class JWTAuth(AuthHandler):
"JWT token expired and no refresh token available"
)
# TODO: Implement actual token refresh logic
# This would typically involve:
# 1. Making a POST request to /auth/refresh endpoint
# 2. Sending the refresh token
# 3. Updating self._token and self._expires_at with the response
try:
# Make request to Wiki.js token refresh endpoint
refresh_url = f"{self._base_url}/api/auth/refresh"
raise AuthenticationError(
"JWT token refresh not yet implemented. "
"Please provide a new token or use API key authentication."
)
response = requests.post(
refresh_url,
json={"refreshToken": self._refresh_token},
headers={
"Content-Type": "application/json",
"Accept": "application/json",
},
timeout=30,
)
# Check if request was successful
if not response.ok:
error_msg = "Token refresh failed"
try:
error_data = response.json()
error_msg = error_data.get("message", error_msg)
except Exception:
pass
raise AuthenticationError(
f"Failed to refresh JWT token: {error_msg} (HTTP {response.status_code})"
)
# Parse response
data = response.json()
# Update token and expiration
if "token" in data:
self._token = data["token"]
if "expiresAt" in data:
self._expires_at = data["expiresAt"]
elif "expires_at" in data:
self._expires_at = data["expires_at"]
# Optionally update refresh token if a new one is provided
if "refreshToken" in data:
self._refresh_token = data["refreshToken"]
except requests.exceptions.RequestException as e:
raise AuthenticationError(
f"Failed to refresh JWT token: {str(e)}"
) from e
except Exception as e:
raise AuthenticationError(
f"Unexpected error during token refresh: {str(e)}"
) from e
def is_expired(self) -> bool:
"""Check if the JWT token is expired.