ready for try

This commit is contained in:
2025-07-29 20:16:11 -04:00
parent 29001b02a5
commit 18a82711cb
33 changed files with 7446 additions and 47 deletions

1
tests/auth/__init__.py Normal file
View File

@@ -0,0 +1 @@
"""Authentication tests for wikijs-python-sdk."""

112
tests/auth/test_api_key.py Normal file
View File

@@ -0,0 +1,112 @@
"""Tests for API key authentication."""
import pytest
from wikijs.auth.api_key import APIKeyAuth
class TestAPIKeyAuth:
"""Test APIKeyAuth implementation."""
def test_init_with_valid_key(self, mock_api_key):
"""Test initialization with valid API key."""
auth = APIKeyAuth(mock_api_key)
assert auth._api_key == mock_api_key
def test_init_with_whitespace_key(self):
"""Test initialization trims whitespace from API key."""
auth = APIKeyAuth(" test-key ")
assert auth._api_key == "test-key"
def test_init_with_empty_key_raises_error(self):
"""Test that empty API key raises ValueError."""
with pytest.raises(ValueError, match="API key cannot be empty"):
APIKeyAuth("")
def test_init_with_whitespace_only_key_raises_error(self):
"""Test that whitespace-only API key raises ValueError."""
with pytest.raises(ValueError, match="API key cannot be empty"):
APIKeyAuth(" ")
def test_init_with_none_raises_error(self):
"""Test that None API key raises ValueError."""
with pytest.raises(ValueError, match="API key cannot be empty"):
APIKeyAuth(None)
def test_get_headers_returns_bearer_token(self, api_key_auth, mock_api_key):
"""Test that get_headers returns proper Authorization header."""
headers = api_key_auth.get_headers()
expected_headers = {
"Authorization": f"Bearer {mock_api_key}",
"Content-Type": "application/json"
}
assert headers == expected_headers
def test_is_valid_returns_true_for_valid_key(self, api_key_auth):
"""Test that is_valid returns True for valid key."""
assert api_key_auth.is_valid() is True
def test_is_valid_returns_false_for_empty_key(self):
"""Test that is_valid handles edge cases."""
# This tests the edge case where _api_key somehow becomes empty
auth = APIKeyAuth("test")
auth._api_key = ""
assert auth.is_valid() is False
auth._api_key = None
assert auth.is_valid() is False
def test_refresh_is_noop(self, api_key_auth):
"""Test that refresh does nothing (API keys don't refresh)."""
original_key = api_key_auth._api_key
api_key_auth.refresh()
assert api_key_auth._api_key == original_key
def test_api_key_property_masks_key(self):
"""Test that api_key property masks the key for security."""
# Test short key (<=8 chars)
auth = APIKeyAuth("short")
assert auth.api_key == "*****"
# Test medium key (<=8 chars)
auth = APIKeyAuth("medium12")
assert auth.api_key == "********"
# Test long key (>8 chars) - shows first 4 and last 4
auth = APIKeyAuth("this-is-a-very-long-api-key-for-testing")
expected = "this" + "*" * (len("this-is-a-very-long-api-key-for-testing") - 8) + "ting"
assert auth.api_key == expected
def test_repr_shows_masked_key(self, mock_api_key):
"""Test that __repr__ shows masked API key."""
auth = APIKeyAuth(mock_api_key)
repr_str = repr(auth)
assert "APIKeyAuth" in repr_str
assert mock_api_key not in repr_str # Real key should not appear
assert auth.api_key in repr_str # Masked key should appear
def test_validate_credentials_succeeds_for_valid_key(self, api_key_auth):
"""Test that validate_credentials succeeds for valid key."""
# Should not raise any exception
api_key_auth.validate_credentials()
assert api_key_auth.is_valid() is True
def test_different_key_lengths_mask_correctly(self):
"""Test that different key lengths are masked correctly."""
test_cases = [
("a", "*"),
("ab", "**"),
("abc", "***"),
("abcd", "****"),
("abcdefgh", "********"),
("abcdefghi", "abcd*fghi"), # 9 chars: first 4 + 1 star + last 4
("abcdefghij", "abcd**ghij"), # 10 chars: first 4 + 2 stars + last 4
("very-long-api-key-here", "very**************here"), # 22 chars: first 4 + 14 stars + last 4
]
for key, expected_mask in test_cases:
auth = APIKeyAuth(key)
actual = auth.api_key
assert actual == expected_mask, f"Failed for key '{key}': expected '{expected_mask}', got '{actual}'"

92
tests/auth/test_base.py Normal file
View File

@@ -0,0 +1,92 @@
"""Tests for base authentication functionality."""
import pytest
from unittest.mock import Mock
from wikijs.auth.base import AuthHandler, NoAuth
from wikijs.exceptions import AuthenticationError
class TestAuthHandler:
"""Test abstract AuthHandler functionality."""
def test_cannot_instantiate_abstract_class(self):
"""Test that AuthHandler cannot be instantiated directly."""
with pytest.raises(TypeError):
AuthHandler()
def test_validate_credentials_calls_is_valid(self):
"""Test that validate_credentials calls is_valid."""
# Create concrete implementation for testing
class TestAuth(AuthHandler):
def __init__(self, valid=True):
self.valid = valid
self.refresh_called = False
def get_headers(self):
return {"Authorization": "test"}
def is_valid(self):
return self.valid
def refresh(self):
self.refresh_called = True
self.valid = True
# Test valid credentials
auth = TestAuth(valid=True)
auth.validate_credentials() # Should not raise
assert not auth.refresh_called
# Test invalid credentials that can be refreshed
auth = TestAuth(valid=False)
auth.validate_credentials() # Should not raise
assert auth.refresh_called
assert auth.valid
def test_validate_credentials_raises_on_invalid_after_refresh(self):
"""Test that validate_credentials raises if still invalid after refresh."""
class TestAuth(AuthHandler):
def get_headers(self):
return {"Authorization": "test"}
def is_valid(self):
return False # Always invalid
def refresh(self):
pass # No-op refresh
auth = TestAuth()
with pytest.raises(AuthenticationError, match="Authentication credentials are invalid"):
auth.validate_credentials()
class TestNoAuth:
"""Test NoAuth implementation."""
def test_init(self, no_auth):
"""Test NoAuth initialization."""
assert isinstance(no_auth, NoAuth)
def test_get_headers_returns_empty_dict(self, no_auth):
"""Test that get_headers returns empty dictionary."""
headers = no_auth.get_headers()
assert headers == {}
assert isinstance(headers, dict)
def test_is_valid_always_true(self, no_auth):
"""Test that is_valid always returns True."""
assert no_auth.is_valid() is True
def test_refresh_is_noop(self, no_auth):
"""Test that refresh does nothing."""
# Should not raise any exception
no_auth.refresh()
# State should be unchanged
assert no_auth.is_valid() is True
def test_validate_credentials_succeeds(self, no_auth):
"""Test that validate_credentials always succeeds."""
# Should not raise any exception
no_auth.validate_credentials()
assert no_auth.is_valid() is True

213
tests/auth/test_jwt.py Normal file
View File

@@ -0,0 +1,213 @@
"""Tests for JWT authentication."""
import time
from datetime import datetime, timedelta
from unittest.mock import patch
import pytest
from wikijs.auth.jwt import JWTAuth
from wikijs.exceptions import AuthenticationError
class TestJWTAuth:
"""Test JWTAuth implementation."""
def test_init_with_valid_token(self, mock_jwt_token):
"""Test initialization with valid JWT token."""
auth = JWTAuth(mock_jwt_token)
assert auth._token == mock_jwt_token
assert auth._refresh_token is None
assert auth._expires_at is None
def test_init_with_all_parameters(self, mock_jwt_token):
"""Test initialization with all parameters."""
refresh_token = "refresh-token-123"
expires_at = time.time() + 3600
auth = JWTAuth(mock_jwt_token, refresh_token, expires_at)
assert auth._token == mock_jwt_token
assert auth._refresh_token == refresh_token
assert auth._expires_at == expires_at
def test_init_with_whitespace_token(self):
"""Test initialization trims whitespace from token."""
auth = JWTAuth(" test-token ")
assert auth._token == "test-token"
def test_init_with_empty_token_raises_error(self):
"""Test that empty JWT token raises ValueError."""
with pytest.raises(ValueError, match="JWT token cannot be empty"):
JWTAuth("")
def test_init_with_whitespace_only_token_raises_error(self):
"""Test that whitespace-only JWT token raises ValueError."""
with pytest.raises(ValueError, match="JWT token cannot be empty"):
JWTAuth(" ")
def test_init_with_none_raises_error(self):
"""Test that None JWT token raises ValueError."""
with pytest.raises(ValueError, match="JWT token cannot be empty"):
JWTAuth(None)
def test_get_headers_returns_bearer_token(self, jwt_auth, mock_jwt_token):
"""Test that get_headers returns proper Authorization header."""
headers = jwt_auth.get_headers()
expected_headers = {
"Authorization": f"Bearer {mock_jwt_token}",
"Content-Type": "application/json"
}
assert headers == expected_headers
def test_get_headers_attempts_refresh_if_invalid(self, mock_jwt_token):
"""Test that get_headers attempts refresh if token is invalid."""
# Create JWT with expired token
expires_at = time.time() - 3600 # Expired 1 hour ago
refresh_token = "refresh-token-123"
auth = JWTAuth(mock_jwt_token, refresh_token, expires_at)
# Mock the refresh method to avoid actual implementation
with patch.object(auth, 'refresh') as mock_refresh:
mock_refresh.side_effect = AuthenticationError("Refresh not implemented")
with pytest.raises(AuthenticationError):
auth.get_headers()
mock_refresh.assert_called_once()
def test_is_valid_returns_true_for_valid_token_no_expiry(self, jwt_auth):
"""Test that is_valid returns True for valid token without expiry."""
assert jwt_auth.is_valid() is True
def test_is_valid_returns_true_for_non_expired_token(self, mock_jwt_token):
"""Test that is_valid returns True for non-expired token."""
expires_at = time.time() + 3600 # Expires in 1 hour
auth = JWTAuth(mock_jwt_token, expires_at=expires_at)
assert auth.is_valid() is True
def test_is_valid_returns_false_for_expired_token(self, mock_jwt_token):
"""Test that is_valid returns False for expired token."""
expires_at = time.time() - 3600 # Expired 1 hour ago
auth = JWTAuth(mock_jwt_token, expires_at=expires_at)
assert auth.is_valid() is False
def test_is_valid_considers_refresh_buffer(self, mock_jwt_token):
"""Test that is_valid considers refresh buffer."""
# Token expires in 4 minutes (less than 5 minute buffer)
expires_at = time.time() + 240
auth = JWTAuth(mock_jwt_token, expires_at=expires_at)
assert auth.is_valid() is False # Should be invalid due to buffer
def test_is_valid_returns_false_for_empty_token(self, mock_jwt_token):
"""Test that is_valid handles edge cases."""
auth = JWTAuth(mock_jwt_token)
auth._token = ""
assert auth.is_valid() is False
auth._token = None
assert auth.is_valid() is False
def test_refresh_raises_error_without_refresh_token(self, jwt_auth):
"""Test that refresh raises error when no refresh token available."""
with pytest.raises(AuthenticationError, match="JWT token expired and no refresh token available"):
jwt_auth.refresh()
def test_refresh_raises_not_implemented_error(self, mock_jwt_token):
"""Test that refresh raises not implemented error."""
refresh_token = "refresh-token-123"
auth = JWTAuth(mock_jwt_token, refresh_token)
with pytest.raises(AuthenticationError, match="JWT token refresh not yet implemented"):
auth.refresh()
def test_is_expired_returns_false_no_expiry(self, jwt_auth):
"""Test that is_expired returns False when no expiry set."""
assert jwt_auth.is_expired() is False
def test_is_expired_returns_false_for_valid_token(self, mock_jwt_token):
"""Test that is_expired returns False for valid token."""
expires_at = time.time() + 3600 # Expires in 1 hour
auth = JWTAuth(mock_jwt_token, expires_at=expires_at)
assert auth.is_expired() is False
def test_is_expired_returns_true_for_expired_token(self, mock_jwt_token):
"""Test that is_expired returns True for expired token."""
expires_at = time.time() - 3600 # Expired 1 hour ago
auth = JWTAuth(mock_jwt_token, expires_at=expires_at)
assert auth.is_expired() is True
def test_time_until_expiry_returns_none_no_expiry(self, jwt_auth):
"""Test that time_until_expiry returns None when no expiry set."""
assert jwt_auth.time_until_expiry() is None
def test_time_until_expiry_returns_correct_delta(self, mock_jwt_token):
"""Test that time_until_expiry returns correct timedelta."""
expires_at = time.time() + 3600 # Expires in 1 hour
auth = JWTAuth(mock_jwt_token, expires_at=expires_at)
time_left = auth.time_until_expiry()
assert isinstance(time_left, timedelta)
# Should be approximately 1 hour (allowing for small time differences)
assert 3550 <= time_left.total_seconds() <= 3600
def test_time_until_expiry_returns_zero_for_expired(self, mock_jwt_token):
"""Test that time_until_expiry returns zero for expired token."""
expires_at = time.time() - 3600 # Expired 1 hour ago
auth = JWTAuth(mock_jwt_token, expires_at=expires_at)
time_left = auth.time_until_expiry()
assert time_left.total_seconds() == 0
def test_token_preview_masks_token(self, mock_jwt_token):
"""Test that token_preview masks the token for security."""
auth = JWTAuth(mock_jwt_token)
preview = auth.token_preview
assert preview != mock_jwt_token # Should not show full token
assert preview.startswith(mock_jwt_token[:10])
assert preview.endswith(mock_jwt_token[-10:])
assert "..." in preview
def test_token_preview_handles_short_token(self):
"""Test that token_preview handles short tokens."""
short_token = "short"
auth = JWTAuth(short_token)
preview = auth.token_preview
assert preview == "*****" # Should be all asterisks
def test_token_preview_handles_none_token(self, mock_jwt_token):
"""Test that token_preview handles None token."""
auth = JWTAuth(mock_jwt_token)
auth._token = None
assert auth.token_preview == "None"
def test_repr_shows_masked_token(self, mock_jwt_token):
"""Test that __repr__ shows masked token."""
expires_at = time.time() + 3600
auth = JWTAuth(mock_jwt_token, expires_at=expires_at)
repr_str = repr(auth)
assert "JWTAuth" in repr_str
assert mock_jwt_token not in repr_str # Real token should not appear
assert auth.token_preview in repr_str # Masked token should appear
assert str(expires_at) in repr_str
def test_validate_credentials_succeeds_for_valid_token(self, jwt_auth):
"""Test that validate_credentials succeeds for valid token."""
# Should not raise any exception
jwt_auth.validate_credentials()
assert jwt_auth.is_valid() is True
def test_refresh_token_whitespace_handling(self, mock_jwt_token):
"""Test that refresh token whitespace is handled correctly."""
refresh_token = " refresh-token-123 "
auth = JWTAuth(mock_jwt_token, refresh_token)
assert auth._refresh_token == "refresh-token-123"
# Test None refresh token
auth = JWTAuth(mock_jwt_token, None)
assert auth._refresh_token is None