From 098257efdc0fba85647dc812e31a26c0403a62ca Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Wed, 29 Jul 2026 21:51:45 +0800 Subject: [PATCH 1/2] test(domain): add wave195 verification_code unit tests (+27) Add comprehensive unit tests for VerificationCode domain entity: - create() factory: basic, stripped recipient, custom code/ttl, uniqueness - is_expired / is_used / is_valid properties - mark_used() and increment_attempts() - Direct construction, slots, equality --- tests/unit/domain/test_verification_code.py | 281 ++++++++++++++++++++ 1 file changed, 281 insertions(+) create mode 100755 tests/unit/domain/test_verification_code.py diff --git a/tests/unit/domain/test_verification_code.py b/tests/unit/domain/test_verification_code.py new file mode 100755 index 000000000..6b713c07e --- /dev/null +++ b/tests/unit/domain/test_verification_code.py @@ -0,0 +1,281 @@ +"""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 -- 2.54.0 From 50d74c454ed01f2f08d07407d1f7f9eaac962ad5 Mon Sep 17 00:00:00 2001 From: CI Bot Date: Wed, 29 Jul 2026 13:57:35 +0000 Subject: [PATCH 2/2] style: auto-format with black + isort + prettier [skip ci-format-check] --- tests/unit/domain/test_verification_code.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/unit/domain/test_verification_code.py b/tests/unit/domain/test_verification_code.py index 6b713c07e..d55e0f9d1 100755 --- a/tests/unit/domain/test_verification_code.py +++ b/tests/unit/domain/test_verification_code.py @@ -6,7 +6,6 @@ from datetime import datetime, timedelta, timezone from unittest.mock import patch import pytest - from domain.verification_code import VerificationCode -- 2.54.0