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:
2025-11-20 13:44:33 -07:00
parent 11ecd953d8
commit 074f74002c
28 changed files with 2283 additions and 14 deletions

View 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