diff --git a/tests/unit/test_verification_code_service.py b/tests/unit/test_verification_code_service.py index 3eab8f155..2698e3096 100755 --- a/tests/unit/test_verification_code_service.py +++ b/tests/unit/test_verification_code_service.py @@ -1,7 +1,6 @@ """验证码服务单元测试.""" -from __future__ import annotations - +import re from datetime import datetime, timedelta, timezone from unittest.mock import MagicMock @@ -9,9 +8,9 @@ import pytest from packages.application.auth.verification_code_service import ( CODE_TYPE_EMAIL_BIND, + CODE_TYPE_EMAIL_LOGIN, CODE_TYPE_PHONE_BIND, DAILY_LIMIT, - DEFAULT_TTL_SECONDS, MAX_ATTEMPTS, RESEND_COOLDOWN_SECONDS, VerificationCodeService, @@ -21,298 +20,560 @@ from packages.application.auth.verification_code_service import ( ) from packages.domain.verification_code import VerificationCode +# ── Test Fixtures ──────────────────────────────────────────────────────────── + @pytest.fixture def mock_repo(): - return MagicMock() + """mock 验证码仓储.""" + repo = MagicMock() + repo.find_latest.return_value = None + repo.count_today.return_value = 0 + return repo @pytest.fixture -def code_service(mock_repo): - return VerificationCodeService(mock_repo) +def service(mock_repo): + """验证码服务实例.""" + return VerificationCodeService(repo=mock_repo) -@pytest.fixture -def sample_code(): - code = VerificationCode.create( - recipient="test@example.com", - code_type=CODE_TYPE_EMAIL_BIND, - ttl_seconds=300, +def _make_code( + recipient="test@example.com", + code_type=CODE_TYPE_EMAIL_BIND, + code="123456", + ttl=300, + used=False, + attempts=0, + created_at=None, +): + """创建一个测试用验证码实体.""" + now = created_at or datetime.now(timezone.utc) + vc = VerificationCode( + id="test-code-id", + recipient=recipient, + code=code, + code_type=code_type, + expires_at=now + timedelta(seconds=ttl), + used_at=now if used else None, + attempts=attempts, + created_at=now, ) - return code + return vc -class TestVerificationCodeServiceGenerate: +# ── generate 方法测试 ─────────────────────────────────────────────────────── + + +class TestGenerate: """generate 方法测试""" - def test_generate_success(self, code_service, mock_repo, sample_code): - """生成验证码成功""" - mock_repo.find_latest.return_value = None - mock_repo.count_today.return_value = 0 - mock_repo.save.return_value = None - - code, error = code_service.generate("test@example.com", CODE_TYPE_EMAIL_BIND) + def test_generate_success(self, service, mock_repo): + """成功生成验证码.""" + code, error = service.generate("user@example.com", CODE_TYPE_EMAIL_BIND) assert error is None assert code is not None - assert code.recipient == "test@example.com" + assert code.recipient == "user@example.com" assert code.code_type == CODE_TYPE_EMAIL_BIND + assert len(code.code) == 6 + assert code.code.isdigit() + assert not code.is_used mock_repo.save.assert_called_once() - def test_generate_empty_recipient(self, code_service): - """空接收方返回错误""" - code, error = code_service.generate("", CODE_TYPE_EMAIL_BIND) - assert code is None - assert "接收方不能为空" in error + def test_generate_with_custom_code(self, service, mock_repo): + """使用自定义验证码.""" + code, error = service.generate("user@example.com", CODE_TYPE_EMAIL_LOGIN, custom_code="999999") - def test_generate_invalid_type(self, code_service): - """无效验证码类型返回错误""" - code, error = code_service.generate("test@example.com", "invalid_type") + assert error is None + assert code.code == "999999" + + def test_generate_custom_ttl(self, service, mock_repo): + """自定义 TTL.""" + code, _ = service.generate("u@e.com", CODE_TYPE_EMAIL_BIND, ttl_seconds=600) + delta = code.expires_at - code.created_at + assert delta.total_seconds() == 600 + + def test_generate_default_ttl(self, service, mock_repo): + """默认 TTL.""" + code, _ = service.generate("u@e.com", CODE_TYPE_EMAIL_BIND) + delta = code.expires_at - code.created_at + assert delta.total_seconds() == 300 # 默认5分钟 + + def test_generate_empty_recipient(self, service): + """空接收方.""" + code, error = service.generate("", CODE_TYPE_EMAIL_BIND) + assert code is None + assert "不能为空" in error + + def test_generate_whitespace_recipient(self, service): + """全空白接收方.""" + code, error = service.generate(" ", CODE_TYPE_EMAIL_BIND) + assert code is None + assert "不能为空" in error + + def test_generate_invalid_type(self, service): + """无效验证码类型.""" + code, error = service.generate("u@e.com", "invalid_type") assert code is None assert "无效的验证码类型" in error - def test_generate_cooldown(self, code_service, mock_repo, sample_code): - """冷却期内返回频控错误""" - # 最新的验证码刚创建10秒前 - sample_code.created_at = datetime.now(timezone.utc) - timedelta(seconds=10) - mock_repo.find_latest.return_value = sample_code - mock_repo.count_today.return_value = 1 + def test_generate_recipient_stripped(self, service, mock_repo): + """接收方前后空格会被清理.""" + code, _ = service.generate(" user@e.com ", CODE_TYPE_EMAIL_BIND) + assert code.recipient == "user@e.com" - code, error = code_service.generate("test@example.com", CODE_TYPE_EMAIL_BIND) + def test_generate_phone_code(self, service, mock_repo): + """手机验证码生成.""" + code, error = service.generate("13800138000", CODE_TYPE_PHONE_BIND) + assert error is None + assert code.code_type == CODE_TYPE_PHONE_BIND + assert len(code.code) == 6 + +# ── generate 频控测试 ─────────────────────────────────────────────────────── + + +class TestGenerateRateLimit: + """generate 频控测试""" + + def test_cooldown_active_rejects(self, service, mock_repo): + """冷却期内拒绝重发.""" + recent = _make_code(created_at=datetime.now(timezone.utc) - timedelta(seconds=10)) + mock_repo.find_latest.return_value = recent + + code, error = service.generate("u@e.com", CODE_TYPE_EMAIL_BIND) assert code is None assert "发送太频繁" in error - assert "秒后再试" in error + # 等待时间应该接近 50 秒 (60-10) + match = re.search(r"(\d+)\s*秒", error) + assert match + wait = int(match.group(1)) + assert 45 <= wait <= 55 - def test_generate_daily_limit_exceeded(self, code_service, mock_repo): - """超过每日上限返回错误""" - mock_repo.find_latest.return_value = None # 没有冷却期问题 + def test_cooldown_expired_allows(self, service, mock_repo): + """冷却期过后允许重发.""" + old = _make_code(created_at=datetime.now(timezone.utc) - timedelta(seconds=120)) + mock_repo.find_latest.return_value = old + mock_repo.count_today.return_value = 1 + + code, error = service.generate("u@e.com", CODE_TYPE_EMAIL_BIND) + assert error is None + assert code is not None + + def test_daily_limit_reached(self, service, mock_repo): + """达到每日上限.""" + mock_repo.find_latest.return_value = None mock_repo.count_today.return_value = DAILY_LIMIT - code, error = code_service.generate("test@example.com", CODE_TYPE_EMAIL_BIND) - + code, error = service.generate("u@e.com", CODE_TYPE_EMAIL_BIND) assert code is None assert "今日发送次数已达上限" in error - def test_generate_recipient_stripped(self, code_service, mock_repo, sample_code): - """recipient 会被 strip""" + def test_daily_limit_one_below_allows(self, service, mock_repo): + """未达到上限时允许.""" mock_repo.find_latest.return_value = None + mock_repo.count_today.return_value = DAILY_LIMIT - 1 + + code, error = service.generate("u@e.com", CODE_TYPE_EMAIL_BIND) + assert error is None + assert code is not None + + def test_custom_daily_limit(self, mock_repo): + """自定义每日上限.""" + svc = VerificationCodeService(repo=mock_repo, daily_limit=3) + mock_repo.count_today.return_value = 3 + + code, error = svc.generate("u@e.com", CODE_TYPE_EMAIL_BIND) + assert code is None + assert "已达上限" in error + + def test_custom_cooldown(self, mock_repo): + """自定义冷却时间.""" + svc = VerificationCodeService(repo=mock_repo, resend_cooldown=30) + recent = _make_code(created_at=datetime.now(timezone.utc) - timedelta(seconds=10)) + mock_repo.find_latest.return_value = recent + + code, error = svc.generate("u@e.com", CODE_TYPE_EMAIL_BIND) + assert code is None + match = re.search(r"(\d+)\s*秒", error) + assert match + wait = int(match.group(1)) + assert 15 <= wait <= 25 + + def test_cooldown_different_types_independent(self, service, mock_repo): + """不同类型的验证码冷却独立.""" + # email_bind 类型有一个近期验证码 + recent = _make_code(code_type=CODE_TYPE_EMAIL_BIND) + mock_repo.find_latest.side_effect = lambda r, t: recent if t == CODE_TYPE_EMAIL_BIND else None mock_repo.count_today.return_value = 0 - mock_repo.save.return_value = None - code_service.generate(" test@example.com ", CODE_TYPE_EMAIL_BIND) - - # 传给 repo 的应该是 strip 后的值 - save_call = mock_repo.save.call_args[0][0] - assert save_call.recipient == "test@example.com" - - def test_generate_custom_code(self, code_service, mock_repo): - """使用自定义验证码""" - mock_repo.find_latest.return_value = None - mock_repo.count_today.return_value = 0 - mock_repo.save.return_value = None - - code, _ = code_service.generate("test@example.com", CODE_TYPE_EMAIL_BIND, custom_code="123456") - assert code.code == "123456" - - def test_generate_custom_ttl(self, code_service, mock_repo): - """自定义 TTL""" - mock_repo.find_latest.return_value = None - mock_repo.count_today.return_value = 0 - mock_repo.save.return_value = None - - code, _ = code_service.generate("test@example.com", CODE_TYPE_EMAIL_BIND, ttl_seconds=600) + # email_login 类型应该可以正常发送 + code, error = service.generate("u@e.com", CODE_TYPE_EMAIL_LOGIN) + assert error is None assert code is not None -class TestVerificationCodeServiceVerify: +# ── verify 方法测试 ───────────────────────────────────────────────────────── + + +class TestVerify: """verify 方法测试""" - def test_verify_success(self, code_service, mock_repo, sample_code): - """验证成功""" - mock_repo.find_latest.return_value = sample_code + def test_verify_success(self, service, mock_repo): + """验证码正确.""" + code = _make_code(code="654321") + mock_repo.find_latest.return_value = code - success, error = code_service.verify("test@example.com", CODE_TYPE_EMAIL_BIND, sample_code.code) - - assert success is True + ok, error = service.verify("test@example.com", CODE_TYPE_EMAIL_BIND, "654321") + assert ok is True assert error is None - assert sample_code.is_used is True + assert code.is_used # 标记为已使用 + assert mock_repo.save.call_count >= 2 # increment + mark_used - def test_verify_wrong_code(self, code_service, mock_repo, sample_code): - """验证码错误""" - mock_repo.find_latest.return_value = sample_code + def test_verify_wrong_code(self, service, mock_repo): + """验证码错误.""" + code = _make_code(code="123456") + mock_repo.find_latest.return_value = code - success, error = code_service.verify("test@example.com", CODE_TYPE_EMAIL_BIND, "wrongcode") - - assert success is False + ok, error = service.verify("test@e.com", CODE_TYPE_EMAIL_BIND, "000000") + assert ok is False assert "验证码错误" in error + assert not code.is_used # 不标记为已使用 + assert code.attempts == 1 # 尝试次数+1 - def test_verify_not_found(self, code_service, mock_repo): - """验证码不存在""" + def test_verify_no_code_found(self, service, mock_repo): + """找不到验证码.""" mock_repo.find_latest.return_value = None - success, error = code_service.verify("test@example.com", CODE_TYPE_EMAIL_BIND, "123456") - - assert success is False + ok, error = service.verify("u@e.com", CODE_TYPE_EMAIL_BIND, "123456") + assert ok is False assert "不存在或已过期" in error - def test_verify_expired(self, code_service, mock_repo): - """验证码已过期""" - expired_code = VerificationCode.create( - recipient="test@example.com", - code_type=CODE_TYPE_EMAIL_BIND, - ttl_seconds=1, # 1秒过期 - ) - # 手动设置过期时间 - expired_code.expires_at = datetime.now(timezone.utc) - timedelta(seconds=10) - mock_repo.find_latest.return_value = expired_code + def test_verify_empty_params(self, service): + """参数为空.""" + ok, error = service.verify("", CODE_TYPE_EMAIL_BIND, "123456") + assert ok is False + assert "参数不完整" in error - success, error = code_service.verify("test@example.com", CODE_TYPE_EMAIL_BIND, expired_code.code) + ok2, error2 = service.verify("u@e.com", CODE_TYPE_EMAIL_BIND, "") + assert ok2 is False + assert "参数不完整" in error2 - assert success is False - assert "已过期" in error + def test_verify_whitespace_params(self, service, mock_repo): + """参数前后空格会被清理.""" + code = _make_code(recipient="u@e.com", code="111111") + mock_repo.find_latest.return_value = code - def test_verify_already_used(self, code_service, mock_repo, sample_code): - """验证码已使用""" - sample_code.mark_used() - mock_repo.find_latest.return_value = sample_code + ok, error = service.verify(" u@e.com ", CODE_TYPE_EMAIL_BIND, " 111111 ") + assert ok is True + assert error is None - success, error = code_service.verify("test@example.com", CODE_TYPE_EMAIL_BIND, sample_code.code) + def test_verify_already_used(self, service, mock_repo): + """验证码已使用.""" + code = _make_code(used=True) + mock_repo.find_latest.return_value = code - assert success is False + ok, error = service.verify("u@e.com", CODE_TYPE_EMAIL_BIND, code.code) + assert ok is False assert "已使用" in error - def test_verify_max_attempts_exceeded(self, code_service, mock_repo, sample_code): - """尝试次数过多""" - # 先把尝试次数加到超过上限 - for _ in range(MAX_ATTEMPTS + 1): - sample_code.increment_attempts() - mock_repo.find_latest.return_value = sample_code + def test_verify_expired(self, service, mock_repo): + """验证码已过期.""" + code = _make_code(ttl=-60) # 已过期 + mock_repo.find_latest.return_value = code - success, error = code_service.verify("test@example.com", CODE_TYPE_EMAIL_BIND, sample_code.code) + ok, error = service.verify("u@e.com", CODE_TYPE_EMAIL_BIND, code.code) + assert ok is False + assert "已过期" in error - assert success is False + def test_verify_too_many_attempts(self, service, mock_repo): + """尝试次数过多.""" + code = _make_code(attempts=MAX_ATTEMPTS + 1) + mock_repo.find_latest.return_value = code + + ok, error = service.verify("u@e.com", CODE_TYPE_EMAIL_BIND, code.code) + assert ok is False assert "验证次数过多" in error - def test_verify_empty_params(self, code_service): - """空参数返回错误""" - success, error = code_service.verify("", CODE_TYPE_EMAIL_BIND, "123456") - assert success is False - assert "参数不完整" in error + def test_verify_attempts_increment_each_time(self, service, mock_repo): + """每次错误尝试都增加尝试次数.""" + code = _make_code(code="123456", attempts=0) + mock_repo.find_latest.return_value = code - success, error = code_service.verify("test@example.com", CODE_TYPE_EMAIL_BIND, "") - assert success is False - assert "参数不完整" in error + for _ in range(3): + service.verify("u@e.com", CODE_TYPE_EMAIL_BIND, "wrong") - def test_verify_increments_attempts(self, code_service, mock_repo, sample_code): - """验证会增加尝试次数""" - initial_attempts = sample_code.attempts - mock_repo.find_latest.return_value = sample_code + assert code.attempts == 3 - code_service.verify("test@example.com", CODE_TYPE_EMAIL_BIND, "wrong") + def test_verify_without_consume(self, service, mock_repo): + """验证成功但不标记为已使用(consume=False).""" + code = _make_code(code="999999") + mock_repo.find_latest.return_value = code - assert sample_code.attempts == initial_attempts + 1 - - def test_verify_no_consume(self, code_service, mock_repo, sample_code): - """consume=False 时不标记为已使用""" - mock_repo.find_latest.return_value = sample_code - - success, _ = code_service.verify("test@example.com", CODE_TYPE_EMAIL_BIND, sample_code.code, consume=False) - - assert success is True - assert sample_code.is_used is False - - -class TestVerifyPhone: - """validate_phone 函数测试""" - - def test_valid_phone(self): - """有效手机号""" - ok, err = validate_phone("13800000001") + ok, error = service.verify("u@e.com", CODE_TYPE_EMAIL_BIND, "999999", consume=False) assert ok is True - assert err == "" + assert error is None + assert not code.is_used # 不标记为已使用 - def test_valid_phone_with_plus86(self): - """带 +86 前缀的手机号""" - ok, err = validate_phone("+8613800000001") + def test_verify_consume_default_true(self, service, mock_repo): + """默认 consume=True.""" + code = _make_code(code="123456") + mock_repo.find_latest.return_value = code + + service.verify("u@e.com", CODE_TYPE_EMAIL_BIND, "123456") + assert code.is_used + + def test_verify_used_checked_before_attempts(self, service, mock_repo): + """已使用优先于其他检查.""" + code = _make_code(used=True, attempts=0) + mock_repo.find_latest.return_value = code + + ok, error = service.verify("u@e.com", CODE_TYPE_EMAIL_BIND, code.code) + assert ok is False + assert "已使用" in error + # attempts 会被 increment,但错误原因是已使用 + assert code.attempts == 1 + + def test_custom_max_attempts(self, mock_repo): + """自定义最大尝试次数.""" + svc = VerificationCodeService(repo=mock_repo, max_attempts=2) + code = _make_code(attempts=2) + mock_repo.find_latest.return_value = code + + ok, error = svc.verify("u@e.com", CODE_TYPE_EMAIL_BIND, code.code) + assert ok is False + assert "验证次数过多" in error + + +# ── validate_phone 测试 ───────────────────────────────────────────────────── + + +class TestValidatePhone: + """手机号格式校验测试""" + + def test_valid_11_digit(self): + """标准11位手机号.""" + ok, msg = validate_phone("13800138000") + assert ok is True + assert msg == "" + + def test_valid_with_plus_86(self): + """带+86前缀.""" + ok, msg = validate_phone("+8613800138000") assert ok is True - def test_invalid_phone_short(self): - """太短的手机号""" - ok, err = validate_phone("123") + def test_invalid_too_short(self): + """位数不足.""" + ok, msg = validate_phone("1380013800") assert ok is False - assert "格式不正确" in err + assert "格式不正确" in msg - def test_invalid_phone_wrong_prefix(self): - """号段不对的手机号""" - ok, err = validate_phone("11000000000") + def test_invalid_too_long(self): + """位数过多.""" + ok, msg = validate_phone("138001380001") assert ok is False - def test_empty_phone(self): - """空手机号""" - ok, err = validate_phone("") + def test_invalid_starts_with_2(self): + """开头不是1.""" + ok, msg = validate_phone("23800138000") assert ok is False - assert "不能为空" in err - def test_phone_with_spaces(self): - """带空格的手机号会被 strip""" - ok, _ = validate_phone(" 13800000001 ") + def test_invalid_starts_with_12(self): + """第二位不在3-9.""" + ok, msg = validate_phone("12800138000") + assert ok is False + + def test_invalid_empty(self): + """空字符串.""" + ok, msg = validate_phone("") + assert ok is False + assert "不能为空" in msg + + def test_invalid_whitespace_only(self): + """仅空白.""" + ok, msg = validate_phone(" ") + assert ok is False + assert "不能为空" in msg + + def test_valid_all_prefixes_3_to_9(self): + """第二位3-9都有效.""" + for n in range(3, 10): + ok, _ = validate_phone(f"1{n}800138000") + assert ok is True, f"1{n} prefix should be valid" + + def test_invalid_contains_letters(self): + """包含字母.""" + ok, msg = validate_phone("13800abc000") + assert ok is False + + def test_strips_whitespace(self): + """前后空格会被清理.""" + ok, msg = validate_phone(" 13800138000 ") assert ok is True +# ── normalize_phone 测试 ──────────────────────────────────────────────────── + + class TestNormalizePhone: - """normalize_phone 函数测试""" + """手机号标准化测试""" - def test_removes_plus86(self): - """去掉 +86 前缀""" - assert normalize_phone("+8613800000001") == "13800000001" + def test_strip_plus_86(self): + """去掉+86前缀.""" + assert normalize_phone("+8613800138000") == "13800138000" def test_no_prefix_stays_same(self): - """没有前缀保持不变""" - assert normalize_phone("13800000001") == "13800000001" + """无前缀保持不变.""" + assert normalize_phone("13800138000") == "13800138000" def test_strips_whitespace(self): - """去掉两端空白""" - assert normalize_phone(" 13800000001 ") == "13800000001" + """清理前后空格.""" + assert normalize_phone(" 13800138000 ") == "13800138000" + + def test_plus_86_with_spaces(self): + """带空格的+86.""" + assert normalize_phone(" +8613800138000 ") == "13800138000" + + +# ── validate_email 测试 ───────────────────────────────────────────────────── class TestValidateEmail: - """validate_email 函数测试""" + """邮箱格式校验测试""" - def test_valid_email(self): - """有效邮箱""" - ok, err = validate_email("test@example.com") + def test_valid_simple(self): + """标准邮箱.""" + ok, msg = validate_email("user@example.com") assert ok is True - assert err == "" + assert msg == "" - def test_valid_email_with_subdomain(self): - """带子域名的邮箱""" - ok, _ = validate_email("user@mail.example.com") + def test_valid_with_dots(self): + """带点号的用户名.""" + ok, _ = validate_email("user.name@example.com") assert ok is True - def test_valid_email_with_plus(self): - """带 + 号的邮箱""" + def test_valid_with_plus(self): + """带加号的邮箱.""" ok, _ = validate_email("user+tag@example.com") assert ok is True - def test_invalid_email_no_at(self): - """没有 @ 的邮箱""" - ok, err = validate_email("notanemail") - assert ok is False - assert "格式不正确" in err - - def test_invalid_email_no_domain(self): - """没有域名的邮箱""" - ok, err = validate_email("user@") - assert ok is False - - def test_empty_email(self): - """空邮箱""" - ok, err = validate_email("") - assert ok is False - assert "不能为空" in err - - def test_email_with_spaces(self): - """带空格的邮箱会被 strip""" - ok, _ = validate_email(" test@example.com ") + def test_valid_with_underscore(self): + """带下划线.""" + ok, _ = validate_email("user_name@example.com") assert ok is True + + def test_valid_subdomain(self): + """多级域名.""" + ok, _ = validate_email("user@mail.example.com") + assert ok is True + + def test_invalid_no_at(self): + """没有@.""" + ok, msg = validate_email("userexample.com") + assert ok is False + assert "格式不正确" in msg + + def test_invalid_empty_local(self): + """@前为空.""" + ok, _ = validate_email("@example.com") + assert ok is False + + def test_invalid_empty_domain(self): + """@后为空.""" + ok, _ = validate_email("user@") + assert ok is False + + def test_invalid_no_tld(self): + """没有顶级域名.""" + ok, _ = validate_email("user@example") + assert ok is False + + def test_invalid_empty(self): + """空字符串.""" + ok, msg = validate_email("") + assert ok is False + assert "不能为空" in msg + + def test_invalid_spaces_only(self): + """仅空白.""" + ok, msg = validate_email(" ") + assert ok is False + assert "不能为空" in msg + + def test_strips_whitespace(self): + """前后空格会被清理.""" + ok, msg = validate_email(" user@e.com ") + assert ok is True + + def test_invalid_special_chars(self): + """特殊字符.""" + ok, _ = validate_email("user name@e.com") + assert ok is False + + def test_valid_numbers(self): + """数字邮箱.""" + ok, _ = validate_email("12345@example.com") + assert ok is True + + +# ── VerificationCode 实体辅助验证 ────────────────────────────────────────── + + +class TestVerificationCodeEntity: + """VerificationCode 实体属性测试""" + + def test_is_expired_false_when_fresh(self): + code = _make_code(ttl=300) + assert code.is_expired is False + + def test_is_expired_true_when_past(self): + code = _make_code(ttl=-1) + assert code.is_expired is True + + def test_is_used_false_initially(self): + code = _make_code() + assert code.is_used is False + + def test_is_used_after_mark_used(self): + code = _make_code() + code.mark_used() + assert code.is_used is True + assert code.used_at is not None + + def test_is_valid_fresh(self): + code = _make_code() + assert code.is_valid is True + + def test_is_valid_when_expired(self): + code = _make_code(ttl=-100) + assert code.is_valid is False + + def test_is_valid_when_used(self): + code = _make_code(used=True) + assert code.is_valid is False + + def test_increment_attempts(self): + code = _make_code(attempts=0) + code.increment_attempts() + assert code.attempts == 1 + code.increment_attempts() + assert code.attempts == 2 + + def test_create_generates_6_digit_code(self): + code = VerificationCode.create("u@e.com", CODE_TYPE_EMAIL_BIND) + assert len(code.code) == 6 + assert code.code.isdigit() + + def test_create_custom_code(self): + code = VerificationCode.create("u@e.com", CODE_TYPE_EMAIL_BIND, custom_code="555555") + assert code.code == "555555" + + def test_create_strips_recipient(self): + code = VerificationCode.create(" u@e.com ", CODE_TYPE_EMAIL_BIND) + assert code.recipient == "u@e.com" + + def test_create_sets_expiry(self): + code = VerificationCode.create("u@e.com", CODE_TYPE_EMAIL_BIND, ttl_seconds=120) + delta = code.expires_at - code.created_at + assert delta.total_seconds() == 120