"""Tests for utility helper functions.""" import pytest from unittest.mock import Mock from wikijs.exceptions import ValidationError from wikijs.utils.helpers import ( normalize_url, validate_url, sanitize_path, build_api_url, parse_wiki_response, extract_error_message, chunk_list, safe_get, ) 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 import sys 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)