mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-10-28 19:06:36 +08:00
180 lines
4.8 KiB
Python
180 lines
4.8 KiB
Python
"""
|
|
验证通行密钥挑战的缓存、有效期和一次性消费。
|
|
"""
|
|
|
|
from concurrent.futures import ThreadPoolExecutor
|
|
|
|
import pytest
|
|
|
|
from app.application.security.passkey import (
|
|
PASSKEY_CHALLENGE_TTL_SECONDS,
|
|
PasskeyChallengeStore,
|
|
configure_passkey_challenge_cache,
|
|
)
|
|
from app.runtime.cache import TTLCache
|
|
|
|
|
|
def setup_function():
|
|
"""
|
|
每个测试重新注入挑战缓存,隔离前一用例的事务令牌。
|
|
"""
|
|
configure_passkey_challenge_cache(TTLCache(
|
|
region="passkey_challenge",
|
|
maxsize=4096,
|
|
ttl=PASSKEY_CHALLENGE_TTL_SECONDS,
|
|
))
|
|
|
|
|
|
def test_challenge_can_only_be_consumed_once():
|
|
"""
|
|
挑战事务只能成功消费一次,后续读取不再返回挑战值。
|
|
"""
|
|
token = PasskeyChallengeStore.issue(
|
|
challenge="server-challenge",
|
|
purpose="authentication",
|
|
user_id=1,
|
|
)
|
|
|
|
challenge = PasskeyChallengeStore.consume(
|
|
transaction_token=token,
|
|
purpose="authentication",
|
|
)
|
|
|
|
assert challenge is not None
|
|
assert challenge.challenge == "server-challenge"
|
|
assert challenge.user_id == 1
|
|
assert (
|
|
PasskeyChallengeStore.consume(
|
|
transaction_token=token,
|
|
purpose="authentication",
|
|
)
|
|
is None
|
|
)
|
|
|
|
|
|
def test_wrong_purpose_invalidates_transaction():
|
|
"""
|
|
以错误用途消费事务会使令牌失效,不能再用原用途重试。
|
|
"""
|
|
token = PasskeyChallengeStore.issue(
|
|
challenge="server-challenge",
|
|
purpose="registration",
|
|
user_id=1,
|
|
)
|
|
|
|
assert (
|
|
PasskeyChallengeStore.consume(
|
|
transaction_token=token,
|
|
purpose="authentication",
|
|
)
|
|
is None
|
|
)
|
|
assert (
|
|
PasskeyChallengeStore.consume(
|
|
transaction_token=token,
|
|
purpose="registration",
|
|
)
|
|
is None
|
|
)
|
|
|
|
|
|
def test_expired_challenge_cannot_be_consumed(monkeypatch):
|
|
"""
|
|
挑战超过缓存有效期后不能继续用于认证。
|
|
"""
|
|
expired_cache = TTLCache(region="expired_passkey_challenge", maxsize=1, ttl=0)
|
|
monkeypatch.setattr(PasskeyChallengeStore, "_cache", expired_cache)
|
|
token = PasskeyChallengeStore.issue(
|
|
challenge="server-challenge",
|
|
purpose="authentication",
|
|
user_id=None,
|
|
)
|
|
|
|
assert (
|
|
PasskeyChallengeStore.consume(
|
|
transaction_token=token,
|
|
purpose="authentication",
|
|
)
|
|
is None
|
|
)
|
|
|
|
|
|
def test_concurrent_consumers_have_single_winner():
|
|
"""
|
|
多个线程并发消费同一事务时,只允许一个调用取得挑战。
|
|
"""
|
|
token = PasskeyChallengeStore.issue(
|
|
challenge="server-challenge",
|
|
purpose="authentication",
|
|
user_id=None,
|
|
)
|
|
|
|
def consume():
|
|
return PasskeyChallengeStore.consume(
|
|
transaction_token=token,
|
|
purpose="authentication",
|
|
)
|
|
|
|
with ThreadPoolExecutor(max_workers=8) as executor:
|
|
results = list(executor.map(lambda _: consume(), range(8)))
|
|
|
|
assert sum(result is not None for result in results) == 1
|
|
|
|
|
|
def test_challenge_store_requires_explicit_cache(monkeypatch):
|
|
"""
|
|
未完成启动装配时不得签发一个实际未保存的事务 token。
|
|
"""
|
|
monkeypatch.setattr(PasskeyChallengeStore, "_cache", None)
|
|
|
|
with pytest.raises(RuntimeError, match="缓存尚未配置"):
|
|
PasskeyChallengeStore.issue(
|
|
challenge="server-challenge",
|
|
purpose="authentication",
|
|
user_id=None,
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize("operation", ["store", "consume"])
|
|
def test_challenge_cache_failure_is_not_treated_as_a_cache_miss(
|
|
monkeypatch,
|
|
operation,
|
|
):
|
|
"""
|
|
安全缓存故障必须向认证入口传播,不能伪装成正常 miss。
|
|
"""
|
|
class FailingCache:
|
|
"""
|
|
模拟严格缓存写入或领取故障。
|
|
"""
|
|
|
|
def store(self, _key, _value):
|
|
"""
|
|
按用例模拟写入结果。
|
|
"""
|
|
if operation == "store":
|
|
raise RuntimeError("cache unavailable")
|
|
|
|
def consume(self, _key):
|
|
"""
|
|
按用例模拟领取结果。
|
|
"""
|
|
if operation == "consume":
|
|
raise RuntimeError("cache unavailable")
|
|
return None
|
|
|
|
monkeypatch.setattr(PasskeyChallengeStore, "_cache", FailingCache())
|
|
|
|
with pytest.raises(RuntimeError, match="cache unavailable"):
|
|
if operation == "store":
|
|
PasskeyChallengeStore.issue(
|
|
challenge="server-challenge",
|
|
purpose="authentication",
|
|
user_id=None,
|
|
)
|
|
else:
|
|
PasskeyChallengeStore.consume(
|
|
transaction_token="transaction-token",
|
|
purpose="authentication",
|
|
)
|