87 lines
2.9 KiB
Python
87 lines
2.9 KiB
Python
"""utils/captcha.py 验证码校验:原子消费(get→del→compare)杜绝重放与无限试错。
|
||
|
||
fakeredis 不支持 Lua eval,这里用 Python 复刻 Lua 语义(原子 get→del→compare,
|
||
无论对错都消费验证码)作为 fake,验证业务层行为;另断言生产代码确实调用
|
||
_CAPTCHA_VERIFY_LUA 脚本。
|
||
"""
|
||
import pytest
|
||
import fakeredis.aioredis
|
||
|
||
|
||
@pytest.fixture
|
||
def _captcha_redis(monkeypatch):
|
||
from core import redis_client as rc
|
||
|
||
redis = fakeredis.aioredis.FakeRedis(decode_responses=True)
|
||
|
||
async def fake_eval(script, numkeys, key, user_input):
|
||
# 复刻 _CAPTCHA_VERIFY_LUA 语义:原子 get→del→compare,无论对错都消费
|
||
stored = await redis.get(key)
|
||
if not stored:
|
||
return 0
|
||
await redis.delete(key)
|
||
if str(stored).upper().strip() == str(user_input).upper().strip():
|
||
return 1
|
||
return 0
|
||
|
||
monkeypatch.setattr(redis, 'eval', fake_eval)
|
||
monkeypatch.setattr(rc, 'get_redis', lambda: redis)
|
||
return redis
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_verify_captcha_correct_then_consumed(_captcha_redis):
|
||
from utils.captcha import verify_captcha
|
||
|
||
await _captcha_redis.set('k1', 'Ab3X')
|
||
# 大小写不敏感比较通过
|
||
assert await verify_captcha('k1', 'ab3x') is True
|
||
# 已消费:同一验证码无法再次使用(防重放)
|
||
assert await verify_captcha('k1', 'Ab3X') is False
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_verify_captcha_wrong_input_burns_captcha(_captcha_redis):
|
||
from utils.captcha import verify_captcha
|
||
|
||
await _captcha_redis.set('k2', 'Ab3X')
|
||
# 错误输入 → False,且验证码被消费(失败即失效,防无限试错)
|
||
assert await verify_captcha('k2', 'wrong') is False
|
||
# 随后即使用正确输入也无法通过
|
||
assert await verify_captcha('k2', 'Ab3X') is False
|
||
assert await _captcha_redis.get('k2') is None
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_verify_captcha_missing_key(_captcha_redis):
|
||
from utils.captcha import verify_captcha
|
||
|
||
assert await verify_captcha('missing', 'any') is False
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_verify_captcha_empty_input(_captcha_redis):
|
||
from utils.captcha import verify_captcha
|
||
|
||
assert await verify_captcha('k', '') is False
|
||
assert await verify_captcha('', 'x') is False
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_verify_captcha_uses_atomic_lua_script(_captcha_redis, monkeypatch):
|
||
from utils.captcha import verify_captcha, _CAPTCHA_VERIFY_LUA
|
||
|
||
await _captcha_redis.set('k1', 'Ab3X')
|
||
calls = []
|
||
original = _captcha_redis.eval
|
||
|
||
async def spy_eval(script, numkeys, key, user_input):
|
||
calls.append((script, numkeys))
|
||
return await original(script, numkeys, key, user_input)
|
||
|
||
monkeypatch.setattr(_captcha_redis, 'eval', spy_eval)
|
||
|
||
assert await verify_captcha('k1', 'Ab3X') is True
|
||
# 生产代码调用原子 Lua 脚本(numkeys=1)
|
||
assert calls and calls[0] == (_CAPTCHA_VERIFY_LUA, 1)
|