dfbd141895
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m54s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m1s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m14s
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Successful in 2m22s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 1m8s
CI/CD Pipeline / Validate - Code Quality (push) Failing after 3m33s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 5m25s
CI/CD Pipeline / Integration Tests (push) Successful in 2m38s
CI/CD Pipeline / Unit Tests (push) Failing after 6m7s
CI/CD Pipeline / Build Staging API Image (push) Successful in 12m16s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 3m12s
CI/CD Pipeline / ACR Image Cleanup (push) Failing after 13s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 5m30s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 6m24s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
254 lines
8.8 KiB
Python
Executable File
254 lines
8.8 KiB
Python
Executable File
"""
|
|
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
|