"""Tests for validation utilities.""" import pytest from gondulf.utils.validation import ( mask_email, validate_client_id, normalize_client_id, validate_redirect_uri, extract_domain_from_url, validate_email ) class TestMaskEmail: """Tests for mask_email function.""" def test_mask_email_basic(self): """Test basic email masking.""" assert mask_email("user@example.com") == "u***@example.com" def test_mask_email_long_local(self): """Test masking email with long local part.""" assert mask_email("verylongusername@example.com") == "v***@example.com" def test_mask_email_single_char_local(self): """Test masking email with single character local part.""" # Should return unchanged if local part is only 1 character assert mask_email("a@example.com") == "a@example.com" def test_mask_email_no_at_sign(self): """Test masking invalid email without @ sign.""" assert mask_email("notanemail") == "notanemail" def test_mask_email_empty_string(self): """Test masking empty string.""" assert mask_email("") == "" class TestValidateClientId: """Tests for validate_client_id function.""" def test_valid_https_basic(self): """Test valid basic HTTPS URL.""" is_valid, error = validate_client_id("https://example.com") assert is_valid is True assert error == "" def test_valid_https_with_path(self): """Test valid HTTPS URL with path.""" is_valid, error = validate_client_id("https://example.com/app") assert is_valid is True assert error == "" def test_valid_https_with_trailing_slash(self): """Test valid HTTPS URL with trailing slash.""" is_valid, error = validate_client_id("https://example.com/") assert is_valid is True assert error == "" def test_valid_https_with_query(self): """Test valid HTTPS URL with query string.""" is_valid, error = validate_client_id("https://example.com?foo=bar") assert is_valid is True assert error == "" def test_valid_https_with_subdomain(self): """Test valid HTTPS URL with subdomain.""" is_valid, error = validate_client_id("https://sub.example.com") assert is_valid is True assert error == "" def test_valid_https_with_non_default_port(self): """Test valid HTTPS URL with non-default port.""" is_valid, error = validate_client_id("https://example.com:8080") assert is_valid is True assert error == "" def test_valid_http_localhost(self): """Test valid HTTP URL with localhost.""" is_valid, error = validate_client_id("http://localhost") assert is_valid is True assert error == "" def test_valid_http_localhost_with_port(self): """Test valid HTTP URL with localhost and port.""" is_valid, error = validate_client_id("http://localhost:3000") assert is_valid is True assert error == "" def test_valid_http_127_0_0_1(self): """Test valid HTTP URL with 127.0.0.1.""" is_valid, error = validate_client_id("http://127.0.0.1") assert is_valid is True assert error == "" def test_valid_http_127_0_0_1_with_port(self): """Test valid HTTP URL with 127.0.0.1 and port.""" is_valid, error = validate_client_id("http://127.0.0.1:8080") assert is_valid is True assert error == "" def test_valid_http_ipv6_loopback(self): """Test valid HTTP URL with IPv6 loopback.""" is_valid, error = validate_client_id("http://[::1]") assert is_valid is True assert error == "" def test_valid_http_ipv6_loopback_with_port(self): """Test valid HTTP URL with IPv6 loopback and port.""" is_valid, error = validate_client_id("http://[::1]:8080") assert is_valid is True assert error == "" def test_invalid_ftp_scheme(self): """Test that FTP scheme is rejected.""" is_valid, error = validate_client_id("ftp://example.com") assert is_valid is False assert "must use https or http scheme" in error def test_invalid_no_scheme(self): """Test that URL without scheme is rejected.""" is_valid, error = validate_client_id("example.com") assert is_valid is False assert "must use https or http scheme" in error def test_invalid_fragment(self): """Test that URL with fragment is rejected.""" is_valid, error = validate_client_id("https://example.com#fragment") assert is_valid is False assert "must not contain a fragment" in error def test_invalid_username(self): """Test that URL with username is rejected.""" is_valid, error = validate_client_id("https://user@example.com") assert is_valid is False assert "must not contain username or password" in error def test_invalid_username_and_password(self): """Test that URL with username and password is rejected.""" is_valid, error = validate_client_id("https://user:pass@example.com") assert is_valid is False assert "must not contain username or password" in error def test_invalid_single_dot_path_segment(self): """Test that URL with single-dot path segment is rejected.""" is_valid, error = validate_client_id("https://example.com/./invalid") assert is_valid is False assert "must not contain single-dot (.) or double-dot (..) path segments" in error def test_invalid_double_dot_path_segment(self): """Test that URL with double-dot path segment is rejected.""" is_valid, error = validate_client_id("https://example.com/../invalid") assert is_valid is False assert "must not contain single-dot (.) or double-dot (..) path segments" in error def test_invalid_http_non_localhost(self): """Test that HTTP scheme is rejected for non-localhost.""" is_valid, error = validate_client_id("http://example.com") assert is_valid is False assert "http scheme is only allowed for localhost" in error def test_invalid_non_loopback_ipv4(self): """Test that non-loopback IPv4 address is rejected.""" is_valid, error = validate_client_id("https://192.168.1.1") assert is_valid is False assert "must not use IP address" in error def test_invalid_non_loopback_ipv4_private(self): """Test that private IPv4 address is rejected.""" is_valid, error = validate_client_id("https://10.0.0.1") assert is_valid is False assert "must not use IP address" in error def test_invalid_non_loopback_ipv6(self): """Test that non-loopback IPv6 address is rejected.""" is_valid, error = validate_client_id("https://[2001:db8::1]") assert is_valid is False assert "must not use IP address" in error def test_invalid_empty_string(self): """Test that empty string is rejected.""" is_valid, error = validate_client_id("") assert is_valid is False assert "must be a valid URL" in error or "must use https or http scheme" in error def test_invalid_malformed_url(self): """Test that malformed URL is rejected.""" is_valid, error = validate_client_id("not-a-url") assert is_valid is False assert "must use https or http scheme" in error class TestNormalizeClientId: """Tests for normalize_client_id function.""" def test_normalize_basic_https(self): """Test normalizing basic HTTPS URL.""" assert normalize_client_id("https://example.com/") == "https://example.com/" def test_normalize_basic_https_no_path(self): """Test normalizing HTTPS URL without path adds trailing slash.""" assert normalize_client_id("https://example.com") == "https://example.com/" def test_normalize_uppercase_hostname(self): """Test normalizing URL with uppercase hostname.""" assert normalize_client_id("HTTPS://EXAMPLE.COM") == "https://example.com/" def test_normalize_mixed_case_hostname(self): """Test normalizing URL with mixed case hostname.""" assert normalize_client_id("https://Example.Com/app") == "https://example.com/app" def test_normalize_preserve_path_case(self): """Test that path case is preserved.""" assert normalize_client_id("https://example.com/APP") == "https://example.com/APP" def test_normalize_remove_default_https_port(self): """Test normalizing URL with default HTTPS port.""" assert normalize_client_id("https://example.com:443/") == "https://example.com/" def test_normalize_remove_default_http_port(self): """Test normalizing URL with default HTTP port for localhost.""" assert normalize_client_id("http://localhost:80/") == "http://localhost/" def test_normalize_preserve_non_default_port(self): """Test normalizing URL with non-default port.""" assert normalize_client_id("https://example.com:8443/") == "https://example.com:8443/" def test_normalize_preserve_path(self): """Test normalizing URL with path.""" assert normalize_client_id("https://example.com/app") == "https://example.com/app" def test_normalize_preserve_query(self): """Test normalizing URL with query string.""" assert normalize_client_id("https://example.com/?foo=bar") == "https://example.com/?foo=bar" def test_normalize_query_without_path(self): """Test normalizing URL with query but no path.""" assert normalize_client_id("https://example.com?foo=bar") == "https://example.com/?foo=bar" def test_normalize_http_localhost(self): """Test normalizing HTTP localhost URL.""" assert normalize_client_id("http://localhost") == "http://localhost/" def test_normalize_http_localhost_with_port(self): """Test normalizing HTTP localhost URL with port.""" assert normalize_client_id("http://localhost:3000") == "http://localhost:3000/" def test_normalize_http_127_0_0_1(self): """Test normalizing HTTP 127.0.0.1 URL.""" assert normalize_client_id("http://127.0.0.1") == "http://127.0.0.1/" def test_normalize_http_ipv6_loopback(self): """Test normalizing HTTP IPv6 loopback URL.""" assert normalize_client_id("http://[::1]") == "http://[::1]/" def test_normalize_http_ipv6_loopback_with_port(self): """Test normalizing HTTP IPv6 loopback URL with port.""" assert normalize_client_id("http://[::1]:8080") == "http://[::1]:8080/" def test_normalize_invalid_http_non_localhost_raises_error(self): """Test that HTTP non-localhost raises ValueError.""" with pytest.raises(ValueError, match="http scheme is only allowed for localhost"): normalize_client_id("http://example.com/") def test_normalize_fragment_raises_error(self): """Test that URL with fragment raises ValueError.""" with pytest.raises(ValueError, match="must not contain a fragment"): normalize_client_id("https://example.com#fragment") def test_normalize_username_raises_error(self): """Test that URL with username raises ValueError.""" with pytest.raises(ValueError, match="must not contain username or password"): normalize_client_id("https://user@example.com") def test_normalize_path_traversal_raises_error(self): """Test that URL with path traversal raises ValueError.""" with pytest.raises(ValueError, match="must not contain single-dot"): normalize_client_id("https://example.com/./app") def test_normalize_no_scheme_raises_error(self): """Test that missing scheme raises ValueError.""" with pytest.raises(ValueError, match="must use https or http scheme"): normalize_client_id("example.com") def test_normalize_invalid_scheme_raises_error(self): """Test that invalid scheme raises ValueError.""" with pytest.raises(ValueError, match="must use https or http scheme"): normalize_client_id("ftp://example.com") class TestValidateRedirectUri: """Tests for validate_redirect_uri function.""" def test_validate_same_origin(self): """Test redirect URI with same origin as client_id.""" assert validate_redirect_uri( "https://example.com/callback", "https://example.com/" ) is True def test_validate_different_path_same_origin(self): """Test redirect URI with different path but same origin.""" assert validate_redirect_uri( "https://example.com/auth/callback", "https://example.com/" ) is True def test_validate_subdomain(self): """Test redirect URI on subdomain of client_id.""" assert validate_redirect_uri( "https://app.example.com/callback", "https://example.com/" ) is True def test_validate_different_domain_fails(self): """Test redirect URI on completely different domain fails.""" assert validate_redirect_uri( "https://evil.com/callback", "https://example.com/" ) is False def test_validate_localhost_http_allowed(self): """Test that localhost can use HTTP.""" assert validate_redirect_uri( "http://localhost/callback", "https://example.com/" ) is True def test_validate_127_0_0_1_http_allowed(self): """Test that 127.0.0.1 can use HTTP.""" assert validate_redirect_uri( "http://127.0.0.1:8000/callback", "https://example.com/" ) is True def test_validate_http_non_localhost_fails(self): """Test that HTTP on non-localhost fails.""" assert validate_redirect_uri( "http://example.com/callback", "https://example.com/" ) is False def test_validate_malformed_uri_fails(self): """Test that malformed URI fails gracefully.""" assert validate_redirect_uri( "not a url", "https://example.com/" ) is False class TestExtractDomainFromUrl: """Tests for extract_domain_from_url function.""" def test_extract_domain_basic(self): """Test extracting domain from basic URL.""" assert extract_domain_from_url("https://example.com/") == "example.com" def test_extract_domain_with_path(self): """Test extracting domain from URL with path.""" assert extract_domain_from_url("https://example.com/path/to/page") == "example.com" def test_extract_domain_with_port(self): """Test extracting domain from URL with port.""" assert extract_domain_from_url("https://example.com:8443/") == "example.com" def test_extract_domain_subdomain(self): """Test extracting subdomain.""" assert extract_domain_from_url("https://blog.example.com/") == "blog.example.com" def test_extract_domain_no_hostname_raises_error(self): """Test that URL without hostname raises ValueError.""" with pytest.raises(ValueError, match="URL has no hostname"): extract_domain_from_url("file:///path/to/file") def test_extract_domain_invalid_url_raises_error(self): """Test that invalid URL raises ValueError.""" with pytest.raises(ValueError, match="Invalid URL"): extract_domain_from_url("not a url") class TestValidateEmail: """Tests for validate_email function.""" def test_validate_email_basic(self): """Test validating basic email.""" assert validate_email("user@example.com") is True def test_validate_email_with_plus(self): """Test validating email with plus sign.""" assert validate_email("user+tag@example.com") is True def test_validate_email_with_dots(self): """Test validating email with dots.""" assert validate_email("first.last@example.com") is True def test_validate_email_subdomain(self): """Test validating email with subdomain.""" assert validate_email("user@mail.example.com") is True def test_validate_email_no_at_sign(self): """Test that email without @ sign fails.""" assert validate_email("notanemail") is False def test_validate_email_no_domain(self): """Test that email without domain fails.""" assert validate_email("user@") is False def test_validate_email_no_local_part(self): """Test that email without local part fails.""" assert validate_email("@example.com") is False def test_validate_email_no_tld(self): """Test that email without TLD fails.""" assert validate_email("user@example") is False def test_validate_email_empty_string(self): """Test that empty string fails.""" assert validate_email("") is False