Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 50d74c454e | |||
| 098257efdc |
Executable
+280
@@ -0,0 +1,280 @@
|
|||||||
|
"""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
|
||||||
Reference in New Issue
Block a user