"""Tests for validation utilities.""" import pytest from utils.validation import ( ValidationError, validate_ascii_only, validate_email, validate_password, validate_url_for_http_request, validate_username, ) class TestValidateAsciiOnly: """Test ASCII-only validation.""" def test_valid_ascii_string(self): """Test that valid ASCII strings pass validation.""" validate_ascii_only("hello123", "test_field") validate_ascii_only("user_name", "test_field") validate_ascii_only("test@example.com", "test_field") def test_invalid_non_ascii_string(self): """Test that non-ASCII strings fail validation.""" with pytest.raises(ValidationError) as exc_info: validate_ascii_only("café", "test_field") assert "ASCII characters" in exc_info.value.message with pytest.raises(ValidationError) as exc_info: validate_ascii_only("naïve", "test_field") assert "ASCII characters" in exc_info.value.message with pytest.raises(ValidationError) as exc_info: validate_ascii_only("résumé", "test_field") assert "ASCII characters" in exc_info.value.message def test_empty_string(self): """Test that empty strings pass validation.""" validate_ascii_only("", "test_field") class TestValidateUsername: """Test username validation.""" def test_valid_usernames(self): """Test that valid usernames pass validation.""" validate_username("user123") validate_username("test_user") validate_username("admin") validate_username("user-name") def test_invalid_empty_username(self): """Test that empty usernames fail validation.""" with pytest.raises(ValidationError) as exc_info: validate_username("") assert "cannot be empty" in exc_info.value.message with pytest.raises(ValidationError) as exc_info: validate_username(" ") assert True def test_invalid_short_username(self): """Test that short usernames fail validation.""" with pytest.raises(ValidationError) as exc_info: validate_username("ab") assert "at least 3 characters" in exc_info.value.message def test_invalid_long_username(self): """Test that long usernames fail validation.""" long_username = "a" * 256 with pytest.raises(ValidationError) as exc_info: validate_username(long_username) assert "no more than 255 characters" in exc_info.value.message def test_invalid_characters_username(self): """Test that usernames with invalid characters fail validation.""" with pytest.raises(ValidationError) as exc_info: validate_username("user@domain") assert "letters, numbers, underscores, and hyphens" in exc_info.value.message with pytest.raises(ValidationError) as exc_info: validate_username("user.name") assert True def test_invalid_non_ascii_username(self): """Test that usernames with non-ASCII characters fail validation.""" with pytest.raises(ValidationError) as exc_info: validate_username("naïve") assert "ASCII characters" in exc_info.value.message with pytest.raises(ValidationError) as exc_info: validate_username("résumé") assert True class TestValidatePassword: """Test password validation.""" def test_valid_passwords(self): """Test that valid passwords pass validation.""" validate_password("password123") validate_password("my_secret_password") validate_password("admin123") def test_invalid_empty_password(self): """Test that empty passwords fail validation.""" with pytest.raises(ValidationError) as exc_info: validate_password("") assert "cannot be empty" in exc_info.value.message with pytest.raises(ValidationError) as exc_info: validate_password(" ") assert True def test_invalid_short_password(self): """Test that short passwords fail validation.""" with pytest.raises(ValidationError) as exc_info: validate_password("12345") assert "at least 6 characters" in exc_info.value.message def test_invalid_non_ascii_password(self): """Test that passwords with non-ASCII characters fail validation.""" with pytest.raises(ValidationError) as exc_info: validate_password("résumé") assert "ASCII characters" in exc_info.value.message class TestValidateEmail: """Test email validation.""" def test_valid_emails(self): """Test that valid emails pass validation.""" validate_email("user@example.com") validate_email("test.user@domain.org") validate_email("admin@company.co.uk") def test_empty_email(self): """Test that empty emails pass validation (email is optional).""" validate_email("") def test_invalid_email_format(self): """Test that invalid email formats fail validation.""" with pytest.raises(ValidationError) as exc_info: validate_email("invalid-email") assert "Invalid email format" in exc_info.value.message with pytest.raises(ValidationError) as exc_info: validate_email("user@") assert True with pytest.raises(ValidationError) as exc_info: validate_email("@domain.com") assert True def test_invalid_non_ascii_email(self): """Test that emails with non-ASCII characters fail validation.""" with pytest.raises(ValidationError) as exc_info: validate_email("résumé@example.com") assert "ASCII characters" in exc_info.value.message class TestValidateUrlForHttpRequest: """Test URL validation for HTTP requests to prevent SSRF attacks.""" def test_valid_http_urls(self): """Test that valid HTTP/HTTPS URLs pass validation.""" validate_url_for_http_request("http://example.com", "test_url") validate_url_for_http_request("https://example.com", "test_url") validate_url_for_http_request("http://example.com/path", "test_url") validate_url_for_http_request("https://example.com/path?query=1", "test_url") validate_url_for_http_request("http://subdomain.example.com", "test_url") def test_invalid_empty_url(self): """Test that empty URLs fail validation.""" with pytest.raises(ValidationError) as exc_info: validate_url_for_http_request("", "test_url") assert "cannot be empty" in exc_info.value.message def test_invalid_scheme(self): """Test that non-HTTP/HTTPS schemes fail validation.""" # FTP scheme with pytest.raises(ValidationError) as exc_info: validate_url_for_http_request("ftp://example.com", "test_url") assert "only http and https schemes are allowed" in exc_info.value.message # File scheme with pytest.raises(ValidationError) as exc_info: validate_url_for_http_request("file:///etc/passwd", "test_url") assert "only http and https schemes are allowed" in exc_info.value.message # Data scheme with pytest.raises(ValidationError) as exc_info: validate_url_for_http_request("data:text/html,

test

", "test_url") assert "only http and https schemes are allowed" in exc_info.value.message # JavaScript scheme (XSS attack vector) with pytest.raises(ValidationError) as exc_info: validate_url_for_http_request("javascript:alert(1)", "test_url") assert "only http and https schemes are allowed" in exc_info.value.message def test_invalid_localhost(self): """Test that localhost and reserved hostnames fail validation.""" with pytest.raises(ValidationError) as exc_info: validate_url_for_http_request("http://localhost", "test_url") assert ( "localhost and reserved hostnames are not allowed" in exc_info.value.message ) with pytest.raises(ValidationError) as exc_info: validate_url_for_http_request("http://127.0.0.1", "test_url") assert ( "localhost and reserved hostnames are not allowed" in exc_info.value.message ) with pytest.raises(ValidationError) as exc_info: validate_url_for_http_request("http://[::1]", "test_url") assert ( "localhost and reserved hostnames are not allowed" in exc_info.value.message ) with pytest.raises(ValidationError) as exc_info: validate_url_for_http_request("http://0.0.0.0", "test_url") assert ( "localhost and reserved hostnames are not allowed" in exc_info.value.message ) def test_invalid_private_ipv4_addresses(self): """Test that private IPv4 addresses fail validation.""" # 10.x.x.x range with pytest.raises(ValidationError) as exc_info: validate_url_for_http_request("http://10.0.0.1", "test_url") assert ( "private, internal, and reserved IP addresses are not allowed" in exc_info.value.message ) # 192.168.x.x range with pytest.raises(ValidationError) as exc_info: validate_url_for_http_request("http://192.168.1.1", "test_url") assert ( "private, internal, and reserved IP addresses are not allowed" in exc_info.value.message ) # 172.16.x.x - 172.31.x.x range with pytest.raises(ValidationError) as exc_info: validate_url_for_http_request("http://172.16.0.1", "test_url") assert ( "private, internal, and reserved IP addresses are not allowed" in exc_info.value.message ) with pytest.raises(ValidationError) as exc_info: validate_url_for_http_request("http://172.31.255.254", "test_url") assert ( "private, internal, and reserved IP addresses are not allowed" in exc_info.value.message ) def test_invalid_cloud_metadata_service_ips(self): """Test that cloud metadata service IPs fail validation.""" # AWS/Azure metadata service: 169.254.169.254 with pytest.raises(ValidationError) as exc_info: validate_url_for_http_request("http://169.254.169.254", "test_url") assert ( "cloud metadata service addresses are not allowed" in exc_info.value.message ) # Link-local addresses (169.254.0.0/16) with pytest.raises(ValidationError) as exc_info: validate_url_for_http_request("http://169.254.1.1", "test_url") assert ( "cloud metadata service addresses are not allowed" in exc_info.value.message ) def test_invalid_loopback_addresses(self): """Test that loopback addresses fail validation.""" # 127.x.x.x range with pytest.raises(ValidationError) as exc_info: validate_url_for_http_request("http://127.0.0.2", "test_url") assert ( "private, internal, and reserved IP addresses are not allowed" in exc_info.value.message ) with pytest.raises(ValidationError) as exc_info: validate_url_for_http_request("http://127.255.255.255", "test_url") assert ( "private, internal, and reserved IP addresses are not allowed" in exc_info.value.message ) def test_invalid_private_ipv6_addresses(self): """Test that private/link-local IPv6 addresses fail validation.""" # Link-local IPv6: fe80::/10 with pytest.raises(ValidationError) as exc_info: validate_url_for_http_request("http://[fe80::1]", "test_url") assert ( "private, internal, and reserved IP addresses are not allowed" in exc_info.value.message ) # Unique local address: fc00::/7 with pytest.raises(ValidationError) as exc_info: validate_url_for_http_request("http://[fc00::1]", "test_url") assert ( "private, internal, and reserved IP addresses are not allowed" in exc_info.value.message ) with pytest.raises(ValidationError) as exc_info: validate_url_for_http_request("http://[fd00::1]", "test_url") assert ( "private, internal, and reserved IP addresses are not allowed" in exc_info.value.message ) def test_invalid_multicast_addresses(self): """Test that multicast addresses fail validation.""" # IPv4 multicast: 224.0.0.0/4 with pytest.raises(ValidationError) as exc_info: validate_url_for_http_request("http://224.0.0.1", "test_url") assert "multicast addresses are not allowed" in exc_info.value.message # IPv6 multicast: ff00::/8 with pytest.raises(ValidationError) as exc_info: validate_url_for_http_request("http://[ff02::1]", "test_url") assert "multicast addresses are not allowed" in exc_info.value.message def test_invalid_internal_tlds(self): """Test that internal TLDs fail validation.""" with pytest.raises(ValidationError) as exc_info: validate_url_for_http_request("http://server.local", "test_url") assert "internal domain names are not allowed" in exc_info.value.message with pytest.raises(ValidationError) as exc_info: validate_url_for_http_request("http://server.internal", "test_url") assert "internal domain names are not allowed" in exc_info.value.message with pytest.raises(ValidationError) as exc_info: validate_url_for_http_request("http://server.localhost", "test_url") assert "internal domain names are not allowed" in exc_info.value.message def test_invalid_missing_hostname(self): """Test that URLs without hostnames fail validation.""" with pytest.raises(ValidationError) as exc_info: validate_url_for_http_request("http://", "test_url") assert "missing hostname" in exc_info.value.message