mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-10-29 11:27:43 +08:00
170 lines
5.3 KiB
Python
170 lines
5.3 KiB
Python
"""
|
||
验证通行密钥注册错误的业务归一与配置修正提示。
|
||
"""
|
||
|
||
from types import SimpleNamespace
|
||
from unittest.mock import patch
|
||
|
||
import pytest
|
||
import webauthn
|
||
import webauthn.helpers
|
||
from webauthn.helpers.exceptions import InvalidRegistrationResponse
|
||
|
||
from app.api.endpoints import mfa as mfa_endpoint
|
||
from app.application.security.passkey import (
|
||
PASSKEY_CHALLENGE_TTL_SECONDS,
|
||
PasskeyChallengeStore,
|
||
PassKeyHelper,
|
||
PassKeyRegistrationOriginMismatchError,
|
||
PassKeyRegistrationVerificationError,
|
||
configure_passkey_challenge_cache,
|
||
)
|
||
from app.runtime.cache import TTLCache
|
||
|
||
|
||
def setup_function():
|
||
"""
|
||
为注册错误路径显式装配隔离的 challenge 缓存。
|
||
"""
|
||
configure_passkey_challenge_cache(TTLCache(
|
||
region="passkey_challenge",
|
||
maxsize=4096,
|
||
ttl=PASSKEY_CHALLENGE_TTL_SECONDS,
|
||
))
|
||
|
||
|
||
def _registration_request(user_id: int = 1) -> mfa_endpoint.PassKeyRegistrationFinish:
|
||
"""
|
||
构造只用于错误路径的注册完成请求。
|
||
"""
|
||
transaction_token = PasskeyChallengeStore.issue(
|
||
challenge="challenge",
|
||
purpose="registration",
|
||
user_id=user_id,
|
||
)
|
||
return mfa_endpoint.PassKeyRegistrationFinish(
|
||
credential={"id": "credential-id"},
|
||
transaction_token=transaction_token,
|
||
name="测试通行密钥",
|
||
)
|
||
|
||
|
||
def _current_user() -> SimpleNamespace:
|
||
"""
|
||
构造注册错误路径所需的当前用户契约。
|
||
"""
|
||
return SimpleNamespace(id=1, name="admin")
|
||
|
||
|
||
def test_passkey_helper_classifies_origin_mismatch():
|
||
"""
|
||
来源不一致应在 WebAuthn 边界转换为稳定的业务异常。
|
||
"""
|
||
library_error = InvalidRegistrationResponse(
|
||
'Unexpected client data origin "http://localhost:5173", '
|
||
'expected "http://localhost:3000"'
|
||
)
|
||
|
||
# passkey 在方法内按需导入 webauthn,替身打在 webauthn 的来源模块上
|
||
with patch.object(
|
||
webauthn.helpers,
|
||
"parse_registration_credential_json",
|
||
return_value=object(),
|
||
), patch.object(
|
||
webauthn,
|
||
"verify_registration_response",
|
||
side_effect=library_error,
|
||
), pytest.raises(PassKeyRegistrationOriginMismatchError) as exc_info:
|
||
PassKeyHelper.verify_registration_response(
|
||
credential={"id": "credential-id"},
|
||
expected_challenge="Y2hhbGxlbmdl",
|
||
)
|
||
|
||
assert exc_info.value.__cause__ is library_error
|
||
|
||
|
||
def test_passkey_helper_classifies_other_verification_failure():
|
||
"""
|
||
其他注册验证错误不应被误判为来源配置问题。
|
||
"""
|
||
library_error = InvalidRegistrationResponse(
|
||
"Client data challenge was not expected challenge"
|
||
)
|
||
|
||
# passkey 在方法内按需导入 webauthn,替身打在 webauthn 的来源模块上
|
||
with patch.object(
|
||
webauthn.helpers,
|
||
"parse_registration_credential_json",
|
||
return_value=object(),
|
||
), patch.object(
|
||
webauthn,
|
||
"verify_registration_response",
|
||
side_effect=library_error,
|
||
), pytest.raises(PassKeyRegistrationVerificationError) as exc_info:
|
||
PassKeyHelper.verify_registration_response(
|
||
credential={"id": "credential-id"},
|
||
expected_challenge="Y2hhbGxlbmdl",
|
||
)
|
||
|
||
assert exc_info.value.__cause__ is library_error
|
||
|
||
|
||
def test_passkey_register_finish_returns_actionable_origin_message():
|
||
"""
|
||
来源配置不一致时应告诉管理员如何修正访问地址。
|
||
"""
|
||
with patch.object(
|
||
mfa_endpoint.PassKeyHelper,
|
||
"verify_registration_response",
|
||
side_effect=PassKeyRegistrationOriginMismatchError(),
|
||
):
|
||
response = mfa_endpoint.passkey_register_finish(
|
||
passkey_req=_registration_request(),
|
||
current_user=_current_user(),
|
||
)
|
||
|
||
assert not response.success
|
||
assert response.message == "访问域名与系统配置不一致,请使用配置的域名重试"
|
||
assert "APP_DOMAIN" not in response.message
|
||
assert "Unexpected client data origin" not in response.message
|
||
|
||
|
||
def test_passkey_register_finish_hides_other_verification_details():
|
||
"""
|
||
其他 WebAuthn 验证细节只记录在服务端,不返回给客户端。
|
||
"""
|
||
with patch.object(
|
||
mfa_endpoint.PassKeyHelper,
|
||
"verify_registration_response",
|
||
side_effect=PassKeyRegistrationVerificationError(
|
||
"Client data challenge was not expected challenge"
|
||
),
|
||
):
|
||
response = mfa_endpoint.passkey_register_finish(
|
||
passkey_req=_registration_request(),
|
||
current_user=_current_user(),
|
||
)
|
||
|
||
assert not response.success
|
||
assert response.message == "通行密钥注册验证失败,请重新发起注册后重试"
|
||
assert "challenge" not in response.message
|
||
|
||
|
||
def test_passkey_register_finish_hides_unexpected_error_details():
|
||
"""
|
||
未知内部异常应返回通用提示,避免泄露实现信息。
|
||
"""
|
||
with patch.object(
|
||
mfa_endpoint.PassKeyHelper,
|
||
"verify_registration_response",
|
||
side_effect=RuntimeError("database connection details"),
|
||
):
|
||
response = mfa_endpoint.passkey_register_finish(
|
||
passkey_req=_registration_request(),
|
||
current_user=_current_user(),
|
||
)
|
||
|
||
assert not response.success
|
||
assert response.message == "通行密钥注册失败,请稍后重试"
|
||
assert "database" not in response.message
|