feat(phase-2): implement domain verification system
Implements complete domain verification flow with: - rel=me link verification service - HTML fetching with security controls - Rate limiting to prevent abuse - Email validation utilities - Authorization and verification API endpoints - User-facing templates for authorization and verification flows This completes Phase 2: Domain Verification as designed. Tests: - All Phase 2 unit tests passing - Coverage: 85% overall - Migration tests updated 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
199
tests/unit/test_validation.py
Normal file
199
tests/unit/test_validation.py
Normal file
@@ -0,0 +1,199 @@
|
||||
"""Tests for validation utilities."""
|
||||
import pytest
|
||||
|
||||
from gondulf.utils.validation import (
|
||||
mask_email,
|
||||
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 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_remove_default_port(self):
|
||||
"""Test normalizing URL with default HTTPS port."""
|
||||
assert normalize_client_id("https://example.com:443/") == "https://example.com/"
|
||||
|
||||
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_http_scheme_raises_error(self):
|
||||
"""Test that HTTP scheme raises ValueError."""
|
||||
with pytest.raises(ValueError, match="must use https scheme"):
|
||||
normalize_client_id("http://example.com/")
|
||||
|
||||
def test_normalize_no_scheme_raises_error(self):
|
||||
"""Test that missing scheme raises ValueError."""
|
||||
with pytest.raises(ValueError, match="must use https scheme"):
|
||||
normalize_client_id("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
|
||||
Reference in New Issue
Block a user