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:
236
tests/unit/test_domain_verification.py
Normal file
236
tests/unit/test_domain_verification.py
Normal file
@@ -0,0 +1,236 @@
|
||||
"""Tests for domain verification service."""
|
||||
import pytest
|
||||
from unittest.mock import Mock, MagicMock
|
||||
|
||||
from gondulf.services.domain_verification import DomainVerificationService
|
||||
from gondulf.dns import DNSService
|
||||
from gondulf.email import EmailService
|
||||
from gondulf.storage import CodeStore
|
||||
from gondulf.services.html_fetcher import HTMLFetcherService
|
||||
from gondulf.services.relme_parser import RelMeParser
|
||||
|
||||
|
||||
class TestDomainVerificationService:
|
||||
"""Tests for DomainVerificationService."""
|
||||
|
||||
@pytest.fixture
|
||||
def mock_dns(self):
|
||||
"""Mock DNS service."""
|
||||
return Mock(spec=DNSService)
|
||||
|
||||
@pytest.fixture
|
||||
def mock_email(self):
|
||||
"""Mock email service."""
|
||||
return Mock(spec=EmailService)
|
||||
|
||||
@pytest.fixture
|
||||
def mock_storage(self):
|
||||
"""Mock code storage."""
|
||||
return Mock(spec=CodeStore)
|
||||
|
||||
@pytest.fixture
|
||||
def mock_fetcher(self):
|
||||
"""Mock HTML fetcher."""
|
||||
return Mock(spec=HTMLFetcherService)
|
||||
|
||||
@pytest.fixture
|
||||
def mock_parser(self):
|
||||
"""Mock rel=me parser."""
|
||||
return Mock(spec=RelMeParser)
|
||||
|
||||
@pytest.fixture
|
||||
def service(self, mock_dns, mock_email, mock_storage, mock_fetcher, mock_parser):
|
||||
"""Create domain verification service with mocks."""
|
||||
return DomainVerificationService(
|
||||
dns_service=mock_dns,
|
||||
email_service=mock_email,
|
||||
code_storage=mock_storage,
|
||||
html_fetcher=mock_fetcher,
|
||||
relme_parser=mock_parser
|
||||
)
|
||||
|
||||
def test_generate_verification_code(self, service):
|
||||
"""Test verification code generation."""
|
||||
code = service.generate_verification_code()
|
||||
assert isinstance(code, str)
|
||||
assert len(code) == 6
|
||||
assert code.isdigit()
|
||||
|
||||
def test_generate_verification_code_unique(self, service):
|
||||
"""Test that generated codes are different."""
|
||||
code1 = service.generate_verification_code()
|
||||
code2 = service.generate_verification_code()
|
||||
# Very unlikely to be the same, but possible
|
||||
# Just check they're both valid
|
||||
assert code1.isdigit()
|
||||
assert code2.isdigit()
|
||||
|
||||
def test_start_verification_dns_fails(self, service, mock_dns):
|
||||
"""Test start_verification when DNS verification fails."""
|
||||
mock_dns.verify_txt_record.return_value = False
|
||||
|
||||
result = service.start_verification("example.com", "https://example.com/")
|
||||
|
||||
assert result["success"] is False
|
||||
assert result["error"] == "dns_verification_failed"
|
||||
|
||||
def test_start_verification_email_discovery_fails(
|
||||
self, service, mock_dns, mock_fetcher, mock_parser
|
||||
):
|
||||
"""Test start_verification when email discovery fails."""
|
||||
mock_dns.verify_txt_record.return_value = True
|
||||
mock_fetcher.fetch.return_value = "<html></html>"
|
||||
mock_parser.find_email.return_value = None
|
||||
|
||||
result = service.start_verification("example.com", "https://example.com/")
|
||||
|
||||
assert result["success"] is False
|
||||
assert result["error"] == "email_discovery_failed"
|
||||
|
||||
def test_start_verification_invalid_email_format(
|
||||
self, service, mock_dns, mock_fetcher, mock_parser
|
||||
):
|
||||
"""Test start_verification with invalid email format."""
|
||||
mock_dns.verify_txt_record.return_value = True
|
||||
mock_fetcher.fetch.return_value = "<html></html>"
|
||||
mock_parser.find_email.return_value = "not-an-email"
|
||||
|
||||
result = service.start_verification("example.com", "https://example.com/")
|
||||
|
||||
assert result["success"] is False
|
||||
assert result["error"] == "invalid_email_format"
|
||||
|
||||
def test_start_verification_email_send_fails(
|
||||
self, service, mock_dns, mock_fetcher, mock_parser, mock_email
|
||||
):
|
||||
"""Test start_verification when email sending fails."""
|
||||
mock_dns.verify_txt_record.return_value = True
|
||||
mock_fetcher.fetch.return_value = "<html></html>"
|
||||
mock_parser.find_email.return_value = "user@example.com"
|
||||
mock_email.send_verification_code.side_effect = Exception("SMTP error")
|
||||
|
||||
result = service.start_verification("example.com", "https://example.com/")
|
||||
|
||||
assert result["success"] is False
|
||||
assert result["error"] == "email_send_failed"
|
||||
|
||||
def test_start_verification_success(
|
||||
self, service, mock_dns, mock_fetcher, mock_parser, mock_email, mock_storage
|
||||
):
|
||||
"""Test successful verification start."""
|
||||
mock_dns.verify_txt_record.return_value = True
|
||||
mock_fetcher.fetch.return_value = "<html></html>"
|
||||
mock_parser.find_email.return_value = "user@example.com"
|
||||
|
||||
result = service.start_verification("example.com", "https://example.com/")
|
||||
|
||||
assert result["success"] is True
|
||||
assert result["email"] == "u***@example.com" # Masked
|
||||
assert result["verification_method"] == "email"
|
||||
mock_email.send_verification_code.assert_called_once()
|
||||
assert mock_storage.store.call_count == 2 # Code and email stored
|
||||
|
||||
def test_verify_email_code_invalid(self, service, mock_storage):
|
||||
"""Test verify_email_code with invalid code."""
|
||||
mock_storage.verify.return_value = False
|
||||
|
||||
result = service.verify_email_code("example.com", "123456")
|
||||
|
||||
assert result["success"] is False
|
||||
assert result["error"] == "invalid_code"
|
||||
|
||||
def test_verify_email_code_email_not_found(self, service, mock_storage):
|
||||
"""Test verify_email_code when email not in storage."""
|
||||
mock_storage.verify.return_value = True
|
||||
mock_storage.get.return_value = None
|
||||
|
||||
result = service.verify_email_code("example.com", "123456")
|
||||
|
||||
assert result["success"] is False
|
||||
assert result["error"] == "email_not_found"
|
||||
|
||||
def test_verify_email_code_success(self, service, mock_storage):
|
||||
"""Test successful email code verification."""
|
||||
mock_storage.verify.return_value = True
|
||||
mock_storage.get.return_value = "user@example.com"
|
||||
|
||||
result = service.verify_email_code("example.com", "123456")
|
||||
|
||||
assert result["success"] is True
|
||||
assert result["email"] == "user@example.com"
|
||||
mock_storage.delete.assert_called_once()
|
||||
|
||||
def test_create_authorization_code(self, service, mock_storage):
|
||||
"""Test authorization code creation."""
|
||||
code = service.create_authorization_code(
|
||||
client_id="https://client.example.com/",
|
||||
redirect_uri="https://client.example.com/callback",
|
||||
state="test_state",
|
||||
code_challenge="challenge",
|
||||
code_challenge_method="S256",
|
||||
scope="profile",
|
||||
me="https://user.example.com/"
|
||||
)
|
||||
|
||||
assert isinstance(code, str)
|
||||
assert len(code) > 0
|
||||
mock_storage.store.assert_called_once()
|
||||
|
||||
def test_verify_dns_record_success(self, service, mock_dns):
|
||||
"""Test DNS record verification success."""
|
||||
mock_dns.verify_txt_record.return_value = True
|
||||
|
||||
result = service._verify_dns_record("example.com")
|
||||
|
||||
assert result is True
|
||||
mock_dns.verify_txt_record.assert_called_with("example.com", "gondulf-verify-domain")
|
||||
|
||||
def test_verify_dns_record_failure(self, service, mock_dns):
|
||||
"""Test DNS record verification failure."""
|
||||
mock_dns.verify_txt_record.return_value = False
|
||||
|
||||
result = service._verify_dns_record("example.com")
|
||||
|
||||
assert result is False
|
||||
|
||||
def test_verify_dns_record_exception(self, service, mock_dns):
|
||||
"""Test DNS record verification handles exceptions."""
|
||||
mock_dns.verify_txt_record.side_effect = Exception("DNS error")
|
||||
|
||||
result = service._verify_dns_record("example.com")
|
||||
|
||||
assert result is False
|
||||
|
||||
def test_discover_email_success(self, service, mock_fetcher, mock_parser):
|
||||
"""Test email discovery success."""
|
||||
mock_fetcher.fetch.return_value = "<html></html>"
|
||||
mock_parser.find_email.return_value = "user@example.com"
|
||||
|
||||
email = service._discover_email("https://example.com/")
|
||||
|
||||
assert email == "user@example.com"
|
||||
|
||||
def test_discover_email_fetch_fails(self, service, mock_fetcher):
|
||||
"""Test email discovery when fetch fails."""
|
||||
mock_fetcher.fetch.return_value = None
|
||||
|
||||
email = service._discover_email("https://example.com/")
|
||||
|
||||
assert email is None
|
||||
|
||||
def test_discover_email_no_email_found(self, service, mock_fetcher, mock_parser):
|
||||
"""Test email discovery when no email found."""
|
||||
mock_fetcher.fetch.return_value = "<html></html>"
|
||||
mock_parser.find_email.return_value = None
|
||||
|
||||
email = service._discover_email("https://example.com/")
|
||||
|
||||
assert email is None
|
||||
|
||||
def test_discover_email_exception(self, service, mock_fetcher):
|
||||
"""Test email discovery handles exceptions."""
|
||||
mock_fetcher.fetch.side_effect = Exception("Fetch error")
|
||||
|
||||
email = service._discover_email("https://example.com/")
|
||||
|
||||
assert email is None
|
||||
Reference in New Issue
Block a user