Files
wikijs-sdk-python/tests/utils/test_helpers.py
l3ocho ade9aacf56 Fix code formatting and linting issues
- Updated GitHub Actions workflow to use correct flake8 configuration
- Fixed line length issues by using 88 characters as configured
- Removed unused imports and trailing whitespace
- Fixed f-string placeholders and unused variables
- All linting checks now pass with project configuration

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-30 20:49:40 -04:00

459 lines
17 KiB
Python

"""Tests for utility helper functions."""
from unittest.mock import Mock
import pytest
from wikijs.exceptions import ValidationError
from wikijs.utils.helpers import (
build_api_url,
chunk_list,
extract_error_message,
normalize_url,
parse_wiki_response,
safe_get,
sanitize_path,
validate_url,
)
class TestNormalizeUrl:
"""Test URL normalization."""
def test_normalize_url_basic(self):
"""Test basic URL normalization."""
assert normalize_url("https://wiki.example.com") == "https://wiki.example.com"
def test_normalize_url_remove_trailing_slash(self):
"""Test trailing slash removal."""
assert normalize_url("https://wiki.example.com/") == "https://wiki.example.com"
def test_normalize_url_remove_multiple_trailing_slashes(self):
"""Test multiple trailing slash removal."""
assert (
normalize_url("https://wiki.example.com///") == "https://wiki.example.com"
)
def test_normalize_url_with_path(self):
"""Test URL with path normalization."""
assert (
normalize_url("https://wiki.example.com/wiki/")
== "https://wiki.example.com/wiki"
)
def test_normalize_url_empty(self):
"""Test empty URL raises error."""
with pytest.raises(ValidationError, match="Base URL cannot be empty"):
normalize_url("")
def test_normalize_url_none(self):
"""Test None URL raises error."""
with pytest.raises(ValidationError, match="Base URL cannot be empty"):
normalize_url(None)
def test_normalize_url_invalid_scheme(self):
"""Test invalid URL scheme gets https:// prepended."""
# The normalize_url function adds https:// to URLs without checking scheme
result = normalize_url("ftp://wiki.example.com")
assert result == "https://ftp://wiki.example.com"
def test_normalize_url_invalid_format(self):
"""Test invalid URL format raises ValidationError."""
with pytest.raises(ValidationError, match="Invalid URL format"):
normalize_url("not a valid url with spaces")
def test_normalize_url_no_scheme(self):
"""Test URL without scheme gets https:// added."""
result = normalize_url("wiki.example.com")
assert result == "https://wiki.example.com"
def test_normalize_url_with_port(self):
"""Test URL with port."""
assert (
normalize_url("https://wiki.example.com:8080")
== "https://wiki.example.com:8080"
)
class TestValidateUrl:
"""Test URL validation."""
def test_validate_url_valid_https(self):
"""Test valid HTTPS URL."""
assert validate_url("https://wiki.example.com") is True
def test_validate_url_valid_http(self):
"""Test valid HTTP URL."""
assert validate_url("http://wiki.example.com") is True
def test_validate_url_with_path(self):
"""Test valid URL with path."""
assert validate_url("https://wiki.example.com/wiki") is True
def test_validate_url_with_port(self):
"""Test valid URL with port."""
assert validate_url("https://wiki.example.com:8080") is True
def test_validate_url_invalid_scheme(self):
"""Test invalid URL scheme - validate_url only checks format, not scheme type."""
# validate_url only checks that there's a scheme and netloc, not the scheme type
assert validate_url("ftp://wiki.example.com") is True
def test_validate_url_no_scheme(self):
"""Test URL without scheme."""
assert validate_url("wiki.example.com") is False
def test_validate_url_empty(self):
"""Test empty URL."""
assert validate_url("") is False
def test_validate_url_none(self):
"""Test None URL."""
assert validate_url(None) is False
class TestSanitizePath:
"""Test path sanitization."""
def test_sanitize_path_basic(self):
"""Test basic path sanitization."""
assert sanitize_path("simple-path") == "simple-path"
def test_sanitize_path_with_slashes(self):
"""Test path with slashes."""
assert sanitize_path("/path/to/page/") == "path/to/page"
def test_sanitize_path_multiple_slashes(self):
"""Test path with multiple slashes."""
assert sanitize_path("//path///to//page//") == "path/to/page"
def test_sanitize_path_empty(self):
"""Test empty path raises error."""
with pytest.raises(ValidationError, match="Path cannot be empty"):
sanitize_path("")
def test_sanitize_path_none(self):
"""Test None path raises error."""
with pytest.raises(ValidationError, match="Path cannot be empty"):
sanitize_path(None)
class TestBuildApiUrl:
"""Test API URL building."""
def test_build_api_url_basic(self):
"""Test basic API URL building."""
result = build_api_url("https://wiki.example.com", "/test")
assert result == "https://wiki.example.com/test"
def test_build_api_url_with_trailing_slash(self):
"""Test API URL building with trailing slash on base."""
result = build_api_url("https://wiki.example.com/", "/test")
assert result == "https://wiki.example.com/test"
def test_build_api_url_without_leading_slash(self):
"""Test API URL building without leading slash on endpoint."""
result = build_api_url("https://wiki.example.com", "test")
assert result == "https://wiki.example.com/test"
def test_build_api_url_complex_endpoint(self):
"""Test API URL building with complex endpoint."""
result = build_api_url("https://wiki.example.com", "/api/v1/pages")
assert result == "https://wiki.example.com/api/v1/pages"
def test_build_api_url_empty_endpoint(self):
"""Test API URL building with empty endpoint."""
result = build_api_url("https://wiki.example.com", "")
assert "https://wiki.example.com" in result
class TestParseWikiResponse:
"""Test Wiki.js response parsing."""
def test_parse_wiki_response_with_data(self):
"""Test parsing response with data field."""
response = {"data": {"pages": []}, "meta": {"total": 0}}
result = parse_wiki_response(response)
assert result == {"data": {"pages": []}, "meta": {"total": 0}}
def test_parse_wiki_response_without_data(self):
"""Test parsing response without data field."""
response = {"pages": [], "total": 0}
result = parse_wiki_response(response)
assert result == {"pages": [], "total": 0}
def test_parse_wiki_response_empty(self):
"""Test parsing empty response."""
response = {}
result = parse_wiki_response(response)
assert result == {}
def test_parse_wiki_response_none(self):
"""Test parsing None response."""
result = parse_wiki_response(None)
assert result == {} or result is None
class TestExtractErrorMessage:
"""Test error message extraction."""
def test_extract_error_message_json_with_message(self):
"""Test extracting error from JSON response with message."""
mock_response = Mock()
mock_response.text = '{"message": "Not found"}'
mock_response.json.return_value = {"message": "Not found"}
result = extract_error_message(mock_response)
assert result == "Not found"
def test_extract_error_message_json_with_errors_array(self):
"""Test extracting error from JSON response with error field."""
mock_response = Mock()
mock_response.text = '{"error": "Invalid field"}'
mock_response.json.return_value = {"error": "Invalid field"}
result = extract_error_message(mock_response)
assert result == "Invalid field"
def test_extract_error_message_json_with_error_string(self):
"""Test extracting error from JSON response with error string."""
mock_response = Mock()
mock_response.text = '{"error": "Authentication failed"}'
mock_response.json.return_value = {"error": "Authentication failed"}
result = extract_error_message(mock_response)
assert result == "Authentication failed"
def test_extract_error_message_invalid_json(self):
"""Test extracting error from invalid JSON response."""
mock_response = Mock()
mock_response.text = "Invalid JSON response"
mock_response.json.side_effect = ValueError("Invalid JSON")
result = extract_error_message(mock_response)
assert result == "Invalid JSON response"
def test_extract_error_message_empty_response(self):
"""Test extracting error from empty response."""
mock_response = Mock()
mock_response.text = ""
mock_response.json.side_effect = ValueError("Empty response")
result = extract_error_message(mock_response)
# Should return either empty string or default error message
assert result in ["", "Unknown error"]
class TestChunkList:
"""Test list chunking."""
def test_chunk_list_basic(self):
"""Test basic list chunking."""
items = [1, 2, 3, 4, 5, 6]
result = chunk_list(items, 2)
assert result == [[1, 2], [3, 4], [5, 6]]
def test_chunk_list_uneven(self):
"""Test list chunking with uneven division."""
items = [1, 2, 3, 4, 5]
result = chunk_list(items, 2)
assert result == [[1, 2], [3, 4], [5]]
def test_chunk_list_larger_chunk_size(self):
"""Test list chunking with chunk size larger than list."""
items = [1, 2, 3]
result = chunk_list(items, 5)
assert result == [[1, 2, 3]]
def test_chunk_list_empty(self):
"""Test chunking empty list."""
result = chunk_list([], 2)
assert result == []
def test_chunk_list_chunk_size_one(self):
"""Test chunking with chunk size of 1."""
items = [1, 2, 3]
result = chunk_list(items, 1)
assert result == [[1], [2], [3]]
class TestSafeGet:
"""Test safe dictionary value retrieval."""
def test_safe_get_existing_key(self):
"""Test getting existing key."""
data = {"key": "value", "nested": {"inner": "data"}}
assert safe_get(data, "key") == "value"
def test_safe_get_missing_key(self):
"""Test getting missing key with default."""
data = {"key": "value"}
assert safe_get(data, "missing") is None
def test_safe_get_missing_key_with_custom_default(self):
"""Test getting missing key with custom default."""
data = {"key": "value"}
assert safe_get(data, "missing", "default") == "default"
def test_safe_get_nested_key(self):
"""Test getting nested key (if supported)."""
data = {"nested": {"inner": "data"}}
# This might not be supported, but test if it works
result = safe_get(data, "nested")
assert result == {"inner": "data"}
def test_safe_get_empty_dict(self):
"""Test getting from empty dictionary."""
assert safe_get({}, "key") is None
def test_safe_get_none_data(self):
"""Test getting from None data."""
with pytest.raises(AttributeError):
safe_get(None, "key")
def test_safe_get_dot_notation(self):
"""Test safe_get with dot notation."""
data = {"user": {"profile": {"name": "John"}}}
assert safe_get(data, "user.profile.name") == "John"
def test_safe_get_dot_notation_missing(self):
"""Test safe_get with dot notation for missing key."""
data = {"user": {"profile": {"name": "John"}}}
assert safe_get(data, "user.missing.name") is None
assert safe_get(data, "user.missing.name", "default") == "default"
def test_safe_get_dot_notation_non_dict(self):
"""Test safe_get with dot notation when intermediate value is not dict."""
data = {"user": "not_a_dict"}
assert safe_get(data, "user.name") is None
class TestUtilityEdgeCases:
"""Test edge cases for utility functions."""
def test_validate_url_with_none(self):
"""Test validate_url with None input."""
assert validate_url(None) is False
def test_validate_url_with_exception(self):
"""Test validate_url when urlparse raises exception."""
# This is hard to trigger, but test the exception path
assert validate_url("") is False
def test_validate_url_malformed_url(self):
"""Test validate_url with malformed URL that causes exception."""
# Test with a string that could cause urlparse to raise an exception
from unittest.mock import patch
with patch("wikijs.utils.helpers.urlparse") as mock_urlparse:
mock_urlparse.side_effect = Exception("Parse error")
assert validate_url("http://example.com") is False
def test_sanitize_path_whitespace_only(self):
"""Test sanitize_path with whitespace-only input."""
# Whitespace gets stripped and then triggers the empty path check
with pytest.raises(ValidationError, match="Path contains no valid characters"):
sanitize_path(" ")
def test_sanitize_path_invalid_characters_only(self):
"""Test sanitize_path with only invalid characters."""
with pytest.raises(ValidationError, match="Path contains no valid characters"):
sanitize_path("!@#$%^&*()")
def test_sanitize_path_complex_cleanup(self):
"""Test sanitize_path with complex cleanup needs."""
result = sanitize_path(" //hello world//test// ")
assert result == "hello-world/test"
def test_parse_wiki_response_with_error_dict(self):
"""Test parse_wiki_response with error dict."""
response = {"error": {"message": "Not found", "code": "404"}}
from wikijs.exceptions import APIError
with pytest.raises(APIError, match="API Error: Not found"):
parse_wiki_response(response)
def test_parse_wiki_response_with_error_string(self):
"""Test parse_wiki_response with error string."""
response = {"error": "Simple error message"}
from wikijs.exceptions import APIError
with pytest.raises(APIError, match="API Error: Simple error message"):
parse_wiki_response(response)
def test_parse_wiki_response_with_errors_array(self):
"""Test parse_wiki_response with errors array."""
response = {
"errors": [
{"message": "GraphQL error"},
{"message": "Another error"},
]
}
from wikijs.exceptions import APIError
with pytest.raises(APIError, match="GraphQL Error: GraphQL error"):
parse_wiki_response(response)
def test_parse_wiki_response_with_non_dict_errors(self):
"""Test parse_wiki_response with non-dict errors."""
response = {"errors": "String error"}
from wikijs.exceptions import APIError
with pytest.raises(APIError, match="GraphQL Error: String error"):
parse_wiki_response(response)
def test_parse_wiki_response_non_dict_input(self):
"""Test parse_wiki_response with non-dict input."""
assert parse_wiki_response("string") == "string"
assert parse_wiki_response(42) == 42
assert parse_wiki_response([1, 2, 3]) == [1, 2, 3]
def test_extract_error_message_with_nested_error(self):
"""Test extract_error_message with nested error structures."""
mock_response = Mock()
mock_response.text = '{"detail": "Validation failed"}'
mock_response.json.return_value = {"detail": "Validation failed"}
result = extract_error_message(mock_response)
assert result == "Validation failed"
def test_extract_error_message_with_msg_field(self):
"""Test extract_error_message with msg field."""
mock_response = Mock()
mock_response.text = '{"msg": "Short message"}'
mock_response.json.return_value = {"msg": "Short message"}
result = extract_error_message(mock_response)
assert result == "Short message"
def test_extract_error_message_long_text(self):
"""Test extract_error_message with very long response text."""
long_text = "x" * 250 # Longer than 200 chars
mock_response = Mock()
mock_response.text = long_text
mock_response.json.side_effect = ValueError("Invalid JSON")
result = extract_error_message(mock_response)
assert len(result) == 203 # 200 chars + "..."
assert result.endswith("...")
def test_extract_error_message_no_json_no_text(self):
"""Test extract_error_message with object that has neither json nor text."""
obj = "simple string"
result = extract_error_message(obj)
assert result == "simple string"
def test_chunk_list_zero_chunk_size(self):
"""Test chunk_list with zero chunk size."""
with pytest.raises(ValueError, match="Chunk size must be positive"):
chunk_list([1, 2, 3], 0)
def test_chunk_list_negative_chunk_size(self):
"""Test chunk_list with negative chunk size."""
with pytest.raises(ValueError, match="Chunk size must be positive"):
chunk_list([1, 2, 3], -1)