merge: integrate ZSY customer service and profile capabilities

This commit is contained in:
张胜宇
2026-09-11 22:31:51 +08:00
94 changed files with 7933 additions and 77 deletions
@@ -0,0 +1,26 @@
from app.core.conversation_privacy import sanitize_customer_service_message
def test_customer_service_message_hides_sensitive_credentials_before_storage() -> None:
"""敏感值不得进入客服会话持久化和后续异步处理链。"""
message = (
"登录密码: Secret123;验证码 123456;身份证 11010519491231002X;"
"银行卡 6222021234567890123;手机号 13812345678"
)
sanitized = sanitize_customer_service_message(message)
assert "Secret123" not in sanitized
assert "123456" not in sanitized
assert "11010519491231002X" not in sanitized
assert "6222021234567890123" not in sanitized
assert "13812345678" not in sanitized
assert "登录密码" in sanitized
assert "验证码" in sanitized
def test_customer_service_message_keeps_ordinary_password_question_unchanged() -> None:
"""普通业务咨询不应被误判为用户实际提交的密码。"""
message = "忘记登录密码怎么办?"
assert sanitize_customer_service_message(message) == message
+61 -1
View File
@@ -6,7 +6,7 @@ import pytest
from app.core.config import Settings
from app.core.errors import UnauthorizedAgentError
from app.core.security import JwtAuthenticator
from app.core.security import JwtAuthenticator, VisitorTokenIssuer
# 开发专用密钥目录(tools/generate_jwt_keys.py 生成)。本测试需要真实密钥完成签发与验签,
# 所以路径只在这里定义一次:换密钥目录时改这一处,避免多处硬编码各自漂移。
@@ -43,6 +43,35 @@ def test_authenticate_valid_token() -> None:
assert context.trace_id
def test_authenticate_visitor_token_returns_limited_anonymous_context() -> None:
now = datetime.now(UTC)
private_key = Path("config/jwt/jwt-private.pem").read_text(encoding="utf-8")
token = jwt.encode(
{"sub": "2", "iss": "jr-auth", "aud": "jr-agent-platform", "iat": now,
"nbf": now, "exp": now + timedelta(minutes=5), "jti": "visitor-jti-1",
"visitor": True},
private_key,
algorithm="RS256",
)
context = JwtAuthenticator(_settings()).authenticate(token)
assert context.roles == ("visitor",)
assert context.permissions == ("agent:run", "knowledge:query")
assert context.customer_ids == ()
assert context.data_scope == "public"
def test_visitor_token_issuer_creates_short_lived_limited_token() -> None:
token, expires_at = VisitorTokenIssuer(_settings()).issue()
context = JwtAuthenticator(_settings()).authenticate(token)
assert context.roles == ("visitor",)
assert context.permissions == ("agent:run", "knowledge:query")
assert expires_at > datetime.now(UTC)
@pytest.mark.parametrize("subject", ["abc", "", "0", "-1", "1.5", "12", "1" * 21,
"18446744073709551616"])
def test_invalid_numeric_subject_is_unauthorized(subject):
@@ -66,6 +95,37 @@ def test_signed_invalid_subject_returns_401_before_identity_query(monkeypatch):
resolve.assert_not_awaited()
@pytest.mark.asyncio
async def test_visitor_token_skips_identity_database_resolution(monkeypatch):
from unittest.mock import AsyncMock
from fastapi.security import HTTPAuthorizationCredentials
from starlette.requests import Request
from app.api.dependencies.auth import _authenticator, build_request_context
now = datetime.now(UTC)
private_key = Path("config/jwt/jwt-private.pem").read_text(encoding="utf-8")
token = jwt.encode(
{"sub": "2", "iss": "jr-local", "aud": "jr-agent-platform", "iat": now,
"nbf": now, "exp": now + timedelta(minutes=5), "jti": "visitor-jti-2",
"visitor": True},
private_key,
algorithm="RS256",
)
resolve = AsyncMock()
monkeypatch.setattr("app.service.identity_service.IdentityService.resolve", resolve)
_authenticator.cache_clear()
request = Request({"type": "http", "method": "GET", "path": "/", "headers": []})
context = await build_request_context(
request, HTTPAuthorizationCredentials(scheme="Bearer", credentials=token)
)
assert context.roles == ("visitor",)
resolve.assert_not_awaited()
def test_authenticate_rejects_expired_token() -> None:
with pytest.raises(UnauthorizedAgentError):
now = datetime.now(UTC)