"""VerificationCode 单元测试.""" from __future__ import annotations from datetime import datetime, timedelta, timezone from unittest.mock import patch import pytest from domain.verification_code import VerificationCode class TestVerificationCodeCreate: """create() 工厂方法测试.""" def test_create_basic(self): vc = VerificationCode.create("test@example.com", "email_login") assert vc.id is not None assert len(vc.id) == 32 assert vc.recipient == "test@example.com" assert vc.code_type == "email_login" assert len(vc.code) == 6 assert vc.code.isdigit() assert vc.used_at is None assert vc.attempts == 0 assert vc.created_at is not None assert vc.expires_at > vc.created_at def test_create_recipient_stripped(self): vc = VerificationCode.create(" test@example.com ", "email_login") assert vc.recipient == "test@example.com" def test_create_custom_code(self): vc = VerificationCode.create("test@example.com", "email_login", custom_code="123456") assert vc.code == "123456" def test_create_custom_ttl(self): fixed_now = datetime(2026, 1, 1, 12, 0, 0, tzinfo=timezone.utc) with patch("domain.verification_code.datetime") as mock_dt: mock_dt.now.return_value = fixed_now mock_dt.side_effect = lambda *a, **kw: datetime(*a, **kw) vc = VerificationCode.create("test@example.com", "email_login", ttl_seconds=60) assert vc.expires_at == fixed_now + timedelta(seconds=60) def test_create_default_ttl_300(self): fixed_now = datetime(2026, 1, 1, 12, 0, 0, tzinfo=timezone.utc) with patch("domain.verification_code.datetime") as mock_dt: mock_dt.now.return_value = fixed_now mock_dt.side_effect = lambda *a, **kw: datetime(*a, **kw) vc = VerificationCode.create("test@example.com", "email_login") assert vc.expires_at == fixed_now + timedelta(seconds=300) def test_create_unique_ids(self): vc1 = VerificationCode.create("a@b.com", "email_login") vc2 = VerificationCode.create("a@b.com", "email_login") assert vc1.id != vc2.id def test_create_unique_codes(self): codes = set() for _ in range(20): vc = VerificationCode.create("a@b.com", "email_login") codes.add(vc.code) # 20个随机6位码几乎肯定不都一样 assert len(codes) > 1 def test_create_phone_recipient(self): vc = VerificationCode.create("13800138000", "phone_login") assert vc.recipient == "13800138000" assert vc.code_type == "phone_login" def test_create_all_code_types(self): for ct in ["email_bind", "phone_bind", "email_login", "phone_login", "reset_password"]: vc = VerificationCode.create("test@example.com", ct) assert vc.code_type == ct class TestVerificationCodeIsExpired: """is_expired 属性测试.""" def test_not_expired_future(self): future = datetime.now(timezone.utc) + timedelta(hours=1) vc = VerificationCode( id="1", recipient="a@b.com", code="123456", code_type="email_login", expires_at=future, ) assert vc.is_expired is False def test_expired_past(self): past = datetime.now(timezone.utc) - timedelta(hours=1) vc = VerificationCode( id="1", recipient="a@b.com", code="123456", code_type="email_login", expires_at=past, ) assert vc.is_expired is True def test_expired_boundary_exact(self): # 用mock固定时间,expires_at等于当前时间不算过期 fixed_now = datetime(2026, 1, 1, 12, 0, 0, tzinfo=timezone.utc) with patch("domain.verification_code.datetime") as mock_dt: mock_dt.now.return_value = fixed_now mock_dt.side_effect = lambda *a, **kw: datetime(*a, **kw) vc = VerificationCode( id="1", recipient="a@b.com", code="123456", code_type="email_login", expires_at=fixed_now, ) assert vc.is_expired is False class TestVerificationCodeIsUsed: """is_used 属性测试.""" def test_not_used_default(self): vc = VerificationCode.create("a@b.com", "email_login") assert vc.is_used is False def test_is_used_after_mark(self): vc = VerificationCode.create("a@b.com", "email_login") vc.mark_used() assert vc.is_used is True class TestVerificationCodeIsValid: """is_valid 属性测试.""" def test_valid_fresh(self): future = datetime.now(timezone.utc) + timedelta(hours=1) vc = VerificationCode( id="1", recipient="a@b.com", code="123456", code_type="email_login", expires_at=future, ) assert vc.is_valid is True def test_invalid_expired(self): past = datetime.now(timezone.utc) - timedelta(hours=1) vc = VerificationCode( id="1", recipient="a@b.com", code="123456", code_type="email_login", expires_at=past, ) assert vc.is_valid is False def test_invalid_used(self): future = datetime.now(timezone.utc) + timedelta(hours=1) vc = VerificationCode( id="1", recipient="a@b.com", code="123456", code_type="email_login", expires_at=future, ) vc.mark_used() assert vc.is_valid is False def test_invalid_expired_and_used(self): past = datetime.now(timezone.utc) - timedelta(hours=1) vc = VerificationCode( id="1", recipient="a@b.com", code="123456", code_type="email_login", expires_at=past, ) vc.mark_used() assert vc.is_valid is False class TestVerificationCodeMarkUsed: """mark_used 方法测试.""" def test_mark_used_sets_timestamp(self): vc = VerificationCode.create("a@b.com", "email_login") assert vc.used_at is None before = datetime.now(timezone.utc) vc.mark_used() after = datetime.now(timezone.utc) assert vc.used_at is not None assert before <= vc.used_at <= after def test_mark_used_twice_overwrites(self): vc = VerificationCode.create("a@b.com", "email_login") vc.mark_used() first = vc.used_at # 时间足够短,一般不会不同,但确保可以重复调用 vc.mark_used() assert vc.used_at is not None class TestVerificationCodeIncrementAttempts: """increment_attempts 方法测试.""" def test_default_zero(self): vc = VerificationCode.create("a@b.com", "email_login") assert vc.attempts == 0 def test_increment_once(self): vc = VerificationCode.create("a@b.com", "email_login") vc.increment_attempts() assert vc.attempts == 1 def test_increment_multiple(self): vc = VerificationCode.create("a@b.com", "email_login") for _i in range(5): vc.increment_attempts() assert vc.attempts == 5 class TestVerificationCodeBasics: """基础构造和 slots 测试.""" def test_direct_construction(self): now = datetime.now(timezone.utc) vc = VerificationCode( id="abc123", recipient="test@test.com", code="000000", code_type="email_bind", expires_at=now + timedelta(minutes=5), used_at=None, attempts=0, created_at=now, ) assert vc.id == "abc123" assert vc.recipient == "test@test.com" assert vc.code == "000000" def test_slots_no_extra_attrs(self): vc = VerificationCode.create("a@b.com", "email_login") with pytest.raises((AttributeError, TypeError)): vc.new_field = "value" # type: ignore[attr-defined] def test_equality_same_id(self): now = datetime(2026, 1, 1, 12, 0, 0, tzinfo=timezone.utc) vc1 = VerificationCode( id="same", recipient="a@b.com", code="111", code_type="email_login", expires_at=now, created_at=now, ) vc2 = VerificationCode( id="same", recipient="a@b.com", code="111", code_type="email_login", expires_at=now, created_at=now, ) assert vc1 == vc2 def test_equality_different_id(self): now = datetime.now(timezone.utc) vc1 = VerificationCode( id="id1", recipient="a@b.com", code="111", code_type="email_login", expires_at=now, ) vc2 = VerificationCode( id="id2", recipient="a@b.com", code="111", code_type="email_login", expires_at=now, ) assert vc1 != vc2