""" VerificationCode 验证码领域模型单元测试 """ from datetime import datetime, timedelta, timezone import pytest from domain.verification_code import VerificationCode class TestVerificationCodeCreate: """创建验证码测试""" def test_create_default_6digit_code(self): vc = VerificationCode.create("test@example.com", "email_bind") assert vc.id is not None assert len(vc.id) == 32 # uuid4 hex assert vc.recipient == "test@example.com" assert vc.code_type == "email_bind" assert len(vc.code) == 6 assert vc.code.isdigit() assert vc.used_at is None assert vc.attempts == 0 def test_create_custom_code(self): vc = VerificationCode.create("13800138000", "phone_bind", custom_code="123456") assert vc.code == "123456" def test_create_default_ttl_300s(self): before = datetime.now(timezone.utc) vc = VerificationCode.create("test@example.com", "email_login") after = datetime.now(timezone.utc) expected_expiry_min = before + timedelta(seconds=300) expected_expiry_max = after + timedelta(seconds=300) assert expected_expiry_min <= vc.expires_at <= expected_expiry_max def test_create_custom_ttl(self): vc = VerificationCode.create("test@example.com", "reset_password", ttl_seconds=60) expected = datetime.now(timezone.utc) + timedelta(seconds=60) diff = abs((vc.expires_at - expected).total_seconds()) assert diff < 2 def test_create_recipient_stripped(self): vc = VerificationCode.create(" test@example.com ", "email_bind") assert vc.recipient == "test@example.com" 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_sets_created_at(self): vc = VerificationCode.create("test@example.com", "email_bind") assert vc.created_at is not None assert isinstance(vc.created_at, datetime) class TestVerificationCodeExpiry: """过期状态测试""" def test_fresh_code_not_expired(self): vc = VerificationCode.create("test@example.com", "email_bind") assert vc.is_expired is False def test_expired_code_is_expired(self): vc = VerificationCode.create("test@example.com", "email_bind", ttl_seconds=-60) assert vc.is_expired is True def test_boundary_not_expired_at_expiry_time(self): now = datetime.now(timezone.utc) vc = VerificationCode.create("test@example.com", "email_bind") vc.expires_at = now + timedelta(seconds=1) assert vc.is_expired is False def test_boundary_expired_right_after(self): vc = VerificationCode.create("test@example.com", "email_bind") vc.expires_at = datetime.now(timezone.utc) - timedelta(microseconds=1) assert vc.is_expired is True class TestVerificationCodeUsed: """使用状态测试""" def test_fresh_code_not_used(self): vc = VerificationCode.create("test@example.com", "email_bind") assert vc.is_used is False def test_mark_used(self): vc = VerificationCode.create("test@example.com", "email_bind") vc.mark_used() assert vc.is_used is True assert vc.used_at is not None assert isinstance(vc.used_at, datetime) def test_mark_used_sets_recent_time(self): vc = VerificationCode.create("test@example.com", "email_bind") before = datetime.now(timezone.utc) vc.mark_used() after = datetime.now(timezone.utc) assert before <= vc.used_at <= after def test_mark_used_idempotent(self): vc = VerificationCode.create("test@example.com", "email_bind") vc.mark_used() first_used_at = vc.used_at vc.mark_used() # 第二次会更新时间 assert vc.used_at >= first_used_at class TestVerificationCodeValidity: """有效性(未过期+未使用)测试""" def test_fresh_code_is_valid(self): vc = VerificationCode.create("test@example.com", "email_bind") assert vc.is_valid is True def test_expired_code_not_valid(self): vc = VerificationCode.create("test@example.com", "email_bind", ttl_seconds=-10) assert vc.is_valid is False def test_used_code_not_valid(self): vc = VerificationCode.create("test@example.com", "email_bind") vc.mark_used() assert vc.is_valid is False def test_expired_and_used_not_valid(self): vc = VerificationCode.create("test@example.com", "email_bind", ttl_seconds=-10) vc.mark_used() assert vc.is_valid is False class TestVerificationCodeAttempts: """尝试次数测试""" def test_initial_attempts_zero(self): vc = VerificationCode.create("test@example.com", "email_bind") assert vc.attempts == 0 def test_increment_attempts(self): vc = VerificationCode.create("test@example.com", "email_bind") vc.increment_attempts() assert vc.attempts == 1 def test_increment_attempts_multiple(self): vc = VerificationCode.create("test@example.com", "email_bind") for _ in range(5): vc.increment_attempts() assert vc.attempts == 5 class TestVerificationCodeTypes: """不同验证码类型测试""" @pytest.mark.parametrize( "code_type", [ "email_bind", "phone_bind", "email_login", "phone_login", "reset_password", ], ) def test_all_supported_types(self, code_type): vc = VerificationCode.create("test@example.com", code_type) assert vc.code_type == code_type assert vc.is_valid is True class TestVerificationCodeExtended: """VerificationCode 深度补充测试""" def test_id_is_hex(self): vc = VerificationCode.create("test@example.com", "email_bind") int(vc.id, 16) def test_ids_are_unique(self): vc1 = VerificationCode.create("test@example.com", "email_bind") vc2 = VerificationCode.create("test@example.com", "email_bind") assert vc1.id != vc2.id def test_slots_no_extra_attrs(self): import pytest vc = VerificationCode.create("test@example.com", "email_bind") with pytest.raises(AttributeError): vc.new_field = "value" # type: ignore[attr-defined] def test_custom_code_non_numeric(self): """自定义 code 可以是非数字""" vc = VerificationCode.create("test@example.com", "email_bind", custom_code="abcdef") assert vc.code == "abcdef" def test_custom_code_short(self): vc = VerificationCode.create("test@example.com", "email_bind", custom_code="12") assert vc.code == "12" def test_custom_code_long(self): long_code = "1" * 20 vc = VerificationCode.create("test@example.com", "email_bind", custom_code=long_code) assert vc.code == long_code assert len(vc.code) == 20 def test_ttl_zero_expires_immediately(self): vc = VerificationCode.create("test@example.com", "email_bind", ttl_seconds=0) # ttl=0 时 expires_at = now,可能已过期或刚好 assert vc.expires_at is not None def test_very_long_ttl(self): vc = VerificationCode.create("test@example.com", "email_bind", ttl_seconds=86400) assert vc.is_expired is False def test_negative_ttl_expired(self): vc = VerificationCode.create("test@example.com", "email_bind", ttl_seconds=-100) assert vc.is_expired is True def test_attempts_starts_at_zero(self): vc = VerificationCode.create("test@example.com", "email_bind") assert vc.attempts == 0 def test_increment_attempts_returns_none(self): vc = VerificationCode.create("test@example.com", "email_bind") result = vc.increment_attempts() assert result is None assert vc.attempts == 1 def test_mark_used_returns_none(self): vc = VerificationCode.create("test@example.com", "email_bind") result = vc.mark_used() assert result is None assert vc.is_used is True def test_code_type_empty_string(self): vc = VerificationCode.create("test@example.com", "") assert vc.code_type == "" def test_recipient_empty_string(self): vc = VerificationCode.create("", "email_bind") assert vc.recipient == "" def test_created_at_equals_expires_minus_ttl(self): vc = VerificationCode.create("test@example.com", "email_bind", ttl_seconds=60) diff = (vc.expires_at - vc.created_at).total_seconds() assert abs(diff - 60) < 2 def test_multiple_mark_used_updates_time(self): vc = VerificationCode.create("test@example.com", "email_bind") vc.mark_used() first = vc.used_at vc.mark_used() second = vc.used_at assert second >= first