Files
Aether/tests/api/test_pipeline.py

1628 lines
59 KiB
Python
Raw Normal View History

"""
API Pipeline 测试
测试 ApiRequestPipeline 的核心功能
- 认证流程API KeyJWT Token
- 余额计算
- 审计日志记录
"""
from datetime import datetime, timezone
from types import SimpleNamespace
perf: 优化请求鉴权链路并批量化统计/调度查询 - 为 Pipeline/Context 增加按需读取请求体能力,支持 async 懒加载 JSON body - 为 chat/cli/video/claude/openai-cli 适配器关闭默认预读,减少无效 body 读取与超时风险 - 将本地登录、JWT 用户加载、API Key 鉴权迁移到线程池隔离会话执行,避免阻塞事件循环 - 为 API Key 鉴权返回结构化余额结果,并在主请求会话中重新绑定 user/api_key 后再校验状态、过期和锁定信息 - 为 management/user token 前缀认证引入独立会话与结果回绑,避免跨会话对象写入失效 - 为 Usage 余额检查补充结构化返回,统一透出 remaining 与欠费/不可用文案映射 - 为用户与管理端活跃请求查询增加 maintain_status 开关,避免轮询指定 id 时误触发状态修复 - 重写 user_me usage 汇总逻辑,支持 group_by=None 的粗粒度聚合 - 修正 provider 维度成功率与平均响应时间统计,基于 success_count 和成功响应耗时汇总计算 - 前端 Usage 轮询由 setInterval 改为串行 setTimeout,避免并发轮询叠加 - 为 StatsAggregator 增加按本地日期批量计算百分位能力,替代逐天 fan-out 查询 - 为混合统计查询合并连续实时日期区间,并批量读取 StatsDaily,减少逐日查询次数 - 为用户日统计增加批量聚合入口,替代逐用户循环聚合 - 为系统配置导出改用 selectinload 预加载 provider 关联数据,减少 N+1 查询 - 为管理员用户列表增加钱包批量查询,避免逐用户回表 - 为调度器增加 provider 轻量引用预过滤,先按 allowed_providers 缩小范围再加载完整 provider 图 - 为 CandidateBuilder 增加 provider refs/provider_ids 查询能力,保留分页顺序 - 为模型缓存增加 provider_model_mappings 索引缓存与 model_mappings 规则缓存,减少重复全量扫描 - 为请求候选中间态改为 flush/batch commit,降低 pending/streaming 状态切换的事务往返 - 为钱包访问结果补充 balance_snapshot,并抽取余额快照复用逻辑 - 补充 pipeline、auth、admin users、user_me usage、stats aggregator、model cache、 scheduler、wallet、request candidate 等回归与契约测试
2026-03-09 22:57:23 +08:00
from typing import Any
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from fastapi import HTTPException
perf: 优化请求鉴权链路并批量化统计/调度查询 - 为 Pipeline/Context 增加按需读取请求体能力,支持 async 懒加载 JSON body - 为 chat/cli/video/claude/openai-cli 适配器关闭默认预读,减少无效 body 读取与超时风险 - 将本地登录、JWT 用户加载、API Key 鉴权迁移到线程池隔离会话执行,避免阻塞事件循环 - 为 API Key 鉴权返回结构化余额结果,并在主请求会话中重新绑定 user/api_key 后再校验状态、过期和锁定信息 - 为 management/user token 前缀认证引入独立会话与结果回绑,避免跨会话对象写入失效 - 为 Usage 余额检查补充结构化返回,统一透出 remaining 与欠费/不可用文案映射 - 为用户与管理端活跃请求查询增加 maintain_status 开关,避免轮询指定 id 时误触发状态修复 - 重写 user_me usage 汇总逻辑,支持 group_by=None 的粗粒度聚合 - 修正 provider 维度成功率与平均响应时间统计,基于 success_count 和成功响应耗时汇总计算 - 前端 Usage 轮询由 setInterval 改为串行 setTimeout,避免并发轮询叠加 - 为 StatsAggregator 增加按本地日期批量计算百分位能力,替代逐天 fan-out 查询 - 为混合统计查询合并连续实时日期区间,并批量读取 StatsDaily,减少逐日查询次数 - 为用户日统计增加批量聚合入口,替代逐用户循环聚合 - 为系统配置导出改用 selectinload 预加载 provider 关联数据,减少 N+1 查询 - 为管理员用户列表增加钱包批量查询,避免逐用户回表 - 为调度器增加 provider 轻量引用预过滤,先按 allowed_providers 缩小范围再加载完整 provider 图 - 为 CandidateBuilder 增加 provider refs/provider_ids 查询能力,保留分页顺序 - 为模型缓存增加 provider_model_mappings 索引缓存与 model_mappings 规则缓存,减少重复全量扫描 - 为请求候选中间态改为 flush/batch commit,降低 pending/streaming 状态切换的事务往返 - 为钱包访问结果补充 balance_snapshot,并抽取余额快照复用逻辑 - 补充 pipeline、auth、admin users、user_me usage、stats aggregator、model cache、 scheduler、wallet、request candidate 等回归与契约测试
2026-03-09 22:57:23 +08:00
from src.api.base.adapter import ApiMode
from src.api.base.pipeline import ApiRequestPipeline
from src.core.enums import UserRole
perf: 优化请求鉴权链路并批量化统计/调度查询 - 为 Pipeline/Context 增加按需读取请求体能力,支持 async 懒加载 JSON body - 为 chat/cli/video/claude/openai-cli 适配器关闭默认预读,减少无效 body 读取与超时风险 - 将本地登录、JWT 用户加载、API Key 鉴权迁移到线程池隔离会话执行,避免阻塞事件循环 - 为 API Key 鉴权返回结构化余额结果,并在主请求会话中重新绑定 user/api_key 后再校验状态、过期和锁定信息 - 为 management/user token 前缀认证引入独立会话与结果回绑,避免跨会话对象写入失效 - 为 Usage 余额检查补充结构化返回,统一透出 remaining 与欠费/不可用文案映射 - 为用户与管理端活跃请求查询增加 maintain_status 开关,避免轮询指定 id 时误触发状态修复 - 重写 user_me usage 汇总逻辑,支持 group_by=None 的粗粒度聚合 - 修正 provider 维度成功率与平均响应时间统计,基于 success_count 和成功响应耗时汇总计算 - 前端 Usage 轮询由 setInterval 改为串行 setTimeout,避免并发轮询叠加 - 为 StatsAggregator 增加按本地日期批量计算百分位能力,替代逐天 fan-out 查询 - 为混合统计查询合并连续实时日期区间,并批量读取 StatsDaily,减少逐日查询次数 - 为用户日统计增加批量聚合入口,替代逐用户循环聚合 - 为系统配置导出改用 selectinload 预加载 provider 关联数据,减少 N+1 查询 - 为管理员用户列表增加钱包批量查询,避免逐用户回表 - 为调度器增加 provider 轻量引用预过滤,先按 allowed_providers 缩小范围再加载完整 provider 图 - 为 CandidateBuilder 增加 provider refs/provider_ids 查询能力,保留分页顺序 - 为模型缓存增加 provider_model_mappings 索引缓存与 model_mappings 规则缓存,减少重复全量扫描 - 为请求候选中间态改为 flush/batch commit,降低 pending/streaming 状态切换的事务往返 - 为钱包访问结果补充 balance_snapshot,并抽取余额快照复用逻辑 - 补充 pipeline、auth、admin users、user_me usage、stats aggregator、model cache、 scheduler、wallet、request candidate 等回归与契约测试
2026-03-09 22:57:23 +08:00
from src.core.modules.hooks import AUTH_TOKEN_PREFIX_AUTHENTICATORS
from src.services.rate_limit.user_rpm_limiter import RpmCheckResult
class TestPipelineBalanceCalculation:
perf: 优化请求鉴权链路并批量化统计/调度查询 - 为 Pipeline/Context 增加按需读取请求体能力,支持 async 懒加载 JSON body - 为 chat/cli/video/claude/openai-cli 适配器关闭默认预读,减少无效 body 读取与超时风险 - 将本地登录、JWT 用户加载、API Key 鉴权迁移到线程池隔离会话执行,避免阻塞事件循环 - 为 API Key 鉴权返回结构化余额结果,并在主请求会话中重新绑定 user/api_key 后再校验状态、过期和锁定信息 - 为 management/user token 前缀认证引入独立会话与结果回绑,避免跨会话对象写入失效 - 为 Usage 余额检查补充结构化返回,统一透出 remaining 与欠费/不可用文案映射 - 为用户与管理端活跃请求查询增加 maintain_status 开关,避免轮询指定 id 时误触发状态修复 - 重写 user_me usage 汇总逻辑,支持 group_by=None 的粗粒度聚合 - 修正 provider 维度成功率与平均响应时间统计,基于 success_count 和成功响应耗时汇总计算 - 前端 Usage 轮询由 setInterval 改为串行 setTimeout,避免并发轮询叠加 - 为 StatsAggregator 增加按本地日期批量计算百分位能力,替代逐天 fan-out 查询 - 为混合统计查询合并连续实时日期区间,并批量读取 StatsDaily,减少逐日查询次数 - 为用户日统计增加批量聚合入口,替代逐用户循环聚合 - 为系统配置导出改用 selectinload 预加载 provider 关联数据,减少 N+1 查询 - 为管理员用户列表增加钱包批量查询,避免逐用户回表 - 为调度器增加 provider 轻量引用预过滤,先按 allowed_providers 缩小范围再加载完整 provider 图 - 为 CandidateBuilder 增加 provider refs/provider_ids 查询能力,保留分页顺序 - 为模型缓存增加 provider_model_mappings 索引缓存与 model_mappings 规则缓存,减少重复全量扫描 - 为请求候选中间态改为 flush/batch commit,降低 pending/streaming 状态切换的事务往返 - 为钱包访问结果补充 balance_snapshot,并抽取余额快照复用逻辑 - 补充 pipeline、auth、admin users、user_me usage、stats aggregator、model cache、 scheduler、wallet、request candidate 等回归与契约测试
2026-03-09 22:57:23 +08:00
"""Balance calculation tests for Pipeline."""
@pytest.fixture
def pipeline(self) -> ApiRequestPipeline:
return ApiRequestPipeline()
perf: 优化请求鉴权链路并批量化统计/调度查询 - 为 Pipeline/Context 增加按需读取请求体能力,支持 async 懒加载 JSON body - 为 chat/cli/video/claude/openai-cli 适配器关闭默认预读,减少无效 body 读取与超时风险 - 将本地登录、JWT 用户加载、API Key 鉴权迁移到线程池隔离会话执行,避免阻塞事件循环 - 为 API Key 鉴权返回结构化余额结果,并在主请求会话中重新绑定 user/api_key 后再校验状态、过期和锁定信息 - 为 management/user token 前缀认证引入独立会话与结果回绑,避免跨会话对象写入失效 - 为 Usage 余额检查补充结构化返回,统一透出 remaining 与欠费/不可用文案映射 - 为用户与管理端活跃请求查询增加 maintain_status 开关,避免轮询指定 id 时误触发状态修复 - 重写 user_me usage 汇总逻辑,支持 group_by=None 的粗粒度聚合 - 修正 provider 维度成功率与平均响应时间统计,基于 success_count 和成功响应耗时汇总计算 - 前端 Usage 轮询由 setInterval 改为串行 setTimeout,避免并发轮询叠加 - 为 StatsAggregator 增加按本地日期批量计算百分位能力,替代逐天 fan-out 查询 - 为混合统计查询合并连续实时日期区间,并批量读取 StatsDaily,减少逐日查询次数 - 为用户日统计增加批量聚合入口,替代逐用户循环聚合 - 为系统配置导出改用 selectinload 预加载 provider 关联数据,减少 N+1 查询 - 为管理员用户列表增加钱包批量查询,避免逐用户回表 - 为调度器增加 provider 轻量引用预过滤,先按 allowed_providers 缩小范围再加载完整 provider 图 - 为 CandidateBuilder 增加 provider refs/provider_ids 查询能力,保留分页顺序 - 为模型缓存增加 provider_model_mappings 索引缓存与 model_mappings 规则缓存,减少重复全量扫描 - 为请求候选中间态改为 flush/batch commit,降低 pending/streaming 状态切换的事务往返 - 为钱包访问结果补充 balance_snapshot,并抽取余额快照复用逻辑 - 补充 pipeline、auth、admin users、user_me usage、stats aggregator、model cache、 scheduler、wallet、request candidate 等回归与契约测试
2026-03-09 22:57:23 +08:00
@pytest.mark.asyncio
async def test_calculate_balance_remaining_with_balance(
self, pipeline: ApiRequestPipeline
) -> None:
"""Returns remaining balance for limited wallets."""
mock_user = MagicMock()
perf: 优化请求鉴权链路并批量化统计/调度查询 - 为 Pipeline/Context 增加按需读取请求体能力,支持 async 懒加载 JSON body - 为 chat/cli/video/claude/openai-cli 适配器关闭默认预读,减少无效 body 读取与超时风险 - 将本地登录、JWT 用户加载、API Key 鉴权迁移到线程池隔离会话执行,避免阻塞事件循环 - 为 API Key 鉴权返回结构化余额结果,并在主请求会话中重新绑定 user/api_key 后再校验状态、过期和锁定信息 - 为 management/user token 前缀认证引入独立会话与结果回绑,避免跨会话对象写入失效 - 为 Usage 余额检查补充结构化返回,统一透出 remaining 与欠费/不可用文案映射 - 为用户与管理端活跃请求查询增加 maintain_status 开关,避免轮询指定 id 时误触发状态修复 - 重写 user_me usage 汇总逻辑,支持 group_by=None 的粗粒度聚合 - 修正 provider 维度成功率与平均响应时间统计,基于 success_count 和成功响应耗时汇总计算 - 前端 Usage 轮询由 setInterval 改为串行 setTimeout,避免并发轮询叠加 - 为 StatsAggregator 增加按本地日期批量计算百分位能力,替代逐天 fan-out 查询 - 为混合统计查询合并连续实时日期区间,并批量读取 StatsDaily,减少逐日查询次数 - 为用户日统计增加批量聚合入口,替代逐用户循环聚合 - 为系统配置导出改用 selectinload 预加载 provider 关联数据,减少 N+1 查询 - 为管理员用户列表增加钱包批量查询,避免逐用户回表 - 为调度器增加 provider 轻量引用预过滤,先按 allowed_providers 缩小范围再加载完整 provider 图 - 为 CandidateBuilder 增加 provider refs/provider_ids 查询能力,保留分页顺序 - 为模型缓存增加 provider_model_mappings 索引缓存与 model_mappings 规则缓存,减少重复全量扫描 - 为请求候选中间态改为 flush/batch commit,降低 pending/streaming 状态切换的事务往返 - 为钱包访问结果补充 balance_snapshot,并抽取余额快照复用逻辑 - 补充 pipeline、auth、admin users、user_me usage、stats aggregator、model cache、 scheduler、wallet、request candidate 等回归与契约测试
2026-03-09 22:57:23 +08:00
mock_user.id = "user-123"
perf: 优化请求鉴权链路并批量化统计/调度查询 - 为 Pipeline/Context 增加按需读取请求体能力,支持 async 懒加载 JSON body - 为 chat/cli/video/claude/openai-cli 适配器关闭默认预读,减少无效 body 读取与超时风险 - 将本地登录、JWT 用户加载、API Key 鉴权迁移到线程池隔离会话执行,避免阻塞事件循环 - 为 API Key 鉴权返回结构化余额结果,并在主请求会话中重新绑定 user/api_key 后再校验状态、过期和锁定信息 - 为 management/user token 前缀认证引入独立会话与结果回绑,避免跨会话对象写入失效 - 为 Usage 余额检查补充结构化返回,统一透出 remaining 与欠费/不可用文案映射 - 为用户与管理端活跃请求查询增加 maintain_status 开关,避免轮询指定 id 时误触发状态修复 - 重写 user_me usage 汇总逻辑,支持 group_by=None 的粗粒度聚合 - 修正 provider 维度成功率与平均响应时间统计,基于 success_count 和成功响应耗时汇总计算 - 前端 Usage 轮询由 setInterval 改为串行 setTimeout,避免并发轮询叠加 - 为 StatsAggregator 增加按本地日期批量计算百分位能力,替代逐天 fan-out 查询 - 为混合统计查询合并连续实时日期区间,并批量读取 StatsDaily,减少逐日查询次数 - 为用户日统计增加批量聚合入口,替代逐用户循环聚合 - 为系统配置导出改用 selectinload 预加载 provider 关联数据,减少 N+1 查询 - 为管理员用户列表增加钱包批量查询,避免逐用户回表 - 为调度器增加 provider 轻量引用预过滤,先按 allowed_providers 缩小范围再加载完整 provider 图 - 为 CandidateBuilder 增加 provider refs/provider_ids 查询能力,保留分页顺序 - 为模型缓存增加 provider_model_mappings 索引缓存与 model_mappings 规则缓存,减少重复全量扫描 - 为请求候选中间态改为 flush/batch commit,降低 pending/streaming 状态切换的事务往返 - 为钱包访问结果补充 balance_snapshot,并抽取余额快照复用逻辑 - 补充 pipeline、auth、admin users、user_me usage、stats aggregator、model cache、 scheduler、wallet、request candidate 等回归与契约测试
2026-03-09 22:57:23 +08:00
thread_db = MagicMock()
db_user = MagicMock()
thread_db.query.return_value.filter.return_value.first.return_value = db_user
with patch("src.api.base.pipeline.create_session", return_value=thread_db):
with patch(
"src.api.base.pipeline.WalletService.get_balance_snapshot",
return_value=70.0,
):
remaining = await pipeline._calculate_balance_remaining_async(mock_user)
assert remaining == 70.0
perf: 优化请求鉴权链路并批量化统计/调度查询 - 为 Pipeline/Context 增加按需读取请求体能力,支持 async 懒加载 JSON body - 为 chat/cli/video/claude/openai-cli 适配器关闭默认预读,减少无效 body 读取与超时风险 - 将本地登录、JWT 用户加载、API Key 鉴权迁移到线程池隔离会话执行,避免阻塞事件循环 - 为 API Key 鉴权返回结构化余额结果,并在主请求会话中重新绑定 user/api_key 后再校验状态、过期和锁定信息 - 为 management/user token 前缀认证引入独立会话与结果回绑,避免跨会话对象写入失效 - 为 Usage 余额检查补充结构化返回,统一透出 remaining 与欠费/不可用文案映射 - 为用户与管理端活跃请求查询增加 maintain_status 开关,避免轮询指定 id 时误触发状态修复 - 重写 user_me usage 汇总逻辑,支持 group_by=None 的粗粒度聚合 - 修正 provider 维度成功率与平均响应时间统计,基于 success_count 和成功响应耗时汇总计算 - 前端 Usage 轮询由 setInterval 改为串行 setTimeout,避免并发轮询叠加 - 为 StatsAggregator 增加按本地日期批量计算百分位能力,替代逐天 fan-out 查询 - 为混合统计查询合并连续实时日期区间,并批量读取 StatsDaily,减少逐日查询次数 - 为用户日统计增加批量聚合入口,替代逐用户循环聚合 - 为系统配置导出改用 selectinload 预加载 provider 关联数据,减少 N+1 查询 - 为管理员用户列表增加钱包批量查询,避免逐用户回表 - 为调度器增加 provider 轻量引用预过滤,先按 allowed_providers 缩小范围再加载完整 provider 图 - 为 CandidateBuilder 增加 provider refs/provider_ids 查询能力,保留分页顺序 - 为模型缓存增加 provider_model_mappings 索引缓存与 model_mappings 规则缓存,减少重复全量扫描 - 为请求候选中间态改为 flush/batch commit,降低 pending/streaming 状态切换的事务往返 - 为钱包访问结果补充 balance_snapshot,并抽取余额快照复用逻辑 - 补充 pipeline、auth、admin users、user_me usage、stats aggregator、model cache、 scheduler、wallet、request candidate 等回归与契约测试
2026-03-09 22:57:23 +08:00
thread_db.close.assert_called_once()
perf: 优化请求鉴权链路并批量化统计/调度查询 - 为 Pipeline/Context 增加按需读取请求体能力,支持 async 懒加载 JSON body - 为 chat/cli/video/claude/openai-cli 适配器关闭默认预读,减少无效 body 读取与超时风险 - 将本地登录、JWT 用户加载、API Key 鉴权迁移到线程池隔离会话执行,避免阻塞事件循环 - 为 API Key 鉴权返回结构化余额结果,并在主请求会话中重新绑定 user/api_key 后再校验状态、过期和锁定信息 - 为 management/user token 前缀认证引入独立会话与结果回绑,避免跨会话对象写入失效 - 为 Usage 余额检查补充结构化返回,统一透出 remaining 与欠费/不可用文案映射 - 为用户与管理端活跃请求查询增加 maintain_status 开关,避免轮询指定 id 时误触发状态修复 - 重写 user_me usage 汇总逻辑,支持 group_by=None 的粗粒度聚合 - 修正 provider 维度成功率与平均响应时间统计,基于 success_count 和成功响应耗时汇总计算 - 前端 Usage 轮询由 setInterval 改为串行 setTimeout,避免并发轮询叠加 - 为 StatsAggregator 增加按本地日期批量计算百分位能力,替代逐天 fan-out 查询 - 为混合统计查询合并连续实时日期区间,并批量读取 StatsDaily,减少逐日查询次数 - 为用户日统计增加批量聚合入口,替代逐用户循环聚合 - 为系统配置导出改用 selectinload 预加载 provider 关联数据,减少 N+1 查询 - 为管理员用户列表增加钱包批量查询,避免逐用户回表 - 为调度器增加 provider 轻量引用预过滤,先按 allowed_providers 缩小范围再加载完整 provider 图 - 为 CandidateBuilder 增加 provider refs/provider_ids 查询能力,保留分页顺序 - 为模型缓存增加 provider_model_mappings 索引缓存与 model_mappings 规则缓存,减少重复全量扫描 - 为请求候选中间态改为 flush/batch commit,降低 pending/streaming 状态切换的事务往返 - 为钱包访问结果补充 balance_snapshot,并抽取余额快照复用逻辑 - 补充 pipeline、auth、admin users、user_me usage、stats aggregator、model cache、 scheduler、wallet、request candidate 等回归与契约测试
2026-03-09 22:57:23 +08:00
@pytest.mark.asyncio
async def test_calculate_balance_remaining_unlimited(
self, pipeline: ApiRequestPipeline
) -> None:
"""Returns None for unlimited wallets."""
mock_user = MagicMock()
perf: 优化请求鉴权链路并批量化统计/调度查询 - 为 Pipeline/Context 增加按需读取请求体能力,支持 async 懒加载 JSON body - 为 chat/cli/video/claude/openai-cli 适配器关闭默认预读,减少无效 body 读取与超时风险 - 将本地登录、JWT 用户加载、API Key 鉴权迁移到线程池隔离会话执行,避免阻塞事件循环 - 为 API Key 鉴权返回结构化余额结果,并在主请求会话中重新绑定 user/api_key 后再校验状态、过期和锁定信息 - 为 management/user token 前缀认证引入独立会话与结果回绑,避免跨会话对象写入失效 - 为 Usage 余额检查补充结构化返回,统一透出 remaining 与欠费/不可用文案映射 - 为用户与管理端活跃请求查询增加 maintain_status 开关,避免轮询指定 id 时误触发状态修复 - 重写 user_me usage 汇总逻辑,支持 group_by=None 的粗粒度聚合 - 修正 provider 维度成功率与平均响应时间统计,基于 success_count 和成功响应耗时汇总计算 - 前端 Usage 轮询由 setInterval 改为串行 setTimeout,避免并发轮询叠加 - 为 StatsAggregator 增加按本地日期批量计算百分位能力,替代逐天 fan-out 查询 - 为混合统计查询合并连续实时日期区间,并批量读取 StatsDaily,减少逐日查询次数 - 为用户日统计增加批量聚合入口,替代逐用户循环聚合 - 为系统配置导出改用 selectinload 预加载 provider 关联数据,减少 N+1 查询 - 为管理员用户列表增加钱包批量查询,避免逐用户回表 - 为调度器增加 provider 轻量引用预过滤,先按 allowed_providers 缩小范围再加载完整 provider 图 - 为 CandidateBuilder 增加 provider refs/provider_ids 查询能力,保留分页顺序 - 为模型缓存增加 provider_model_mappings 索引缓存与 model_mappings 规则缓存,减少重复全量扫描 - 为请求候选中间态改为 flush/batch commit,降低 pending/streaming 状态切换的事务往返 - 为钱包访问结果补充 balance_snapshot,并抽取余额快照复用逻辑 - 补充 pipeline、auth、admin users、user_me usage、stats aggregator、model cache、 scheduler、wallet、request candidate 等回归与契约测试
2026-03-09 22:57:23 +08:00
mock_user.id = "user-123"
perf: 优化请求鉴权链路并批量化统计/调度查询 - 为 Pipeline/Context 增加按需读取请求体能力,支持 async 懒加载 JSON body - 为 chat/cli/video/claude/openai-cli 适配器关闭默认预读,减少无效 body 读取与超时风险 - 将本地登录、JWT 用户加载、API Key 鉴权迁移到线程池隔离会话执行,避免阻塞事件循环 - 为 API Key 鉴权返回结构化余额结果,并在主请求会话中重新绑定 user/api_key 后再校验状态、过期和锁定信息 - 为 management/user token 前缀认证引入独立会话与结果回绑,避免跨会话对象写入失效 - 为 Usage 余额检查补充结构化返回,统一透出 remaining 与欠费/不可用文案映射 - 为用户与管理端活跃请求查询增加 maintain_status 开关,避免轮询指定 id 时误触发状态修复 - 重写 user_me usage 汇总逻辑,支持 group_by=None 的粗粒度聚合 - 修正 provider 维度成功率与平均响应时间统计,基于 success_count 和成功响应耗时汇总计算 - 前端 Usage 轮询由 setInterval 改为串行 setTimeout,避免并发轮询叠加 - 为 StatsAggregator 增加按本地日期批量计算百分位能力,替代逐天 fan-out 查询 - 为混合统计查询合并连续实时日期区间,并批量读取 StatsDaily,减少逐日查询次数 - 为用户日统计增加批量聚合入口,替代逐用户循环聚合 - 为系统配置导出改用 selectinload 预加载 provider 关联数据,减少 N+1 查询 - 为管理员用户列表增加钱包批量查询,避免逐用户回表 - 为调度器增加 provider 轻量引用预过滤,先按 allowed_providers 缩小范围再加载完整 provider 图 - 为 CandidateBuilder 增加 provider refs/provider_ids 查询能力,保留分页顺序 - 为模型缓存增加 provider_model_mappings 索引缓存与 model_mappings 规则缓存,减少重复全量扫描 - 为请求候选中间态改为 flush/batch commit,降低 pending/streaming 状态切换的事务往返 - 为钱包访问结果补充 balance_snapshot,并抽取余额快照复用逻辑 - 补充 pipeline、auth、admin users、user_me usage、stats aggregator、model cache、 scheduler、wallet、request candidate 等回归与契约测试
2026-03-09 22:57:23 +08:00
thread_db = MagicMock()
db_user = MagicMock()
thread_db.query.return_value.filter.return_value.first.return_value = db_user
with patch("src.api.base.pipeline.create_session", return_value=thread_db):
with patch(
"src.api.base.pipeline.WalletService.get_balance_snapshot",
return_value=None,
):
remaining = await pipeline._calculate_balance_remaining_async(mock_user)
assert remaining is None
perf: 优化请求鉴权链路并批量化统计/调度查询 - 为 Pipeline/Context 增加按需读取请求体能力,支持 async 懒加载 JSON body - 为 chat/cli/video/claude/openai-cli 适配器关闭默认预读,减少无效 body 读取与超时风险 - 将本地登录、JWT 用户加载、API Key 鉴权迁移到线程池隔离会话执行,避免阻塞事件循环 - 为 API Key 鉴权返回结构化余额结果,并在主请求会话中重新绑定 user/api_key 后再校验状态、过期和锁定信息 - 为 management/user token 前缀认证引入独立会话与结果回绑,避免跨会话对象写入失效 - 为 Usage 余额检查补充结构化返回,统一透出 remaining 与欠费/不可用文案映射 - 为用户与管理端活跃请求查询增加 maintain_status 开关,避免轮询指定 id 时误触发状态修复 - 重写 user_me usage 汇总逻辑,支持 group_by=None 的粗粒度聚合 - 修正 provider 维度成功率与平均响应时间统计,基于 success_count 和成功响应耗时汇总计算 - 前端 Usage 轮询由 setInterval 改为串行 setTimeout,避免并发轮询叠加 - 为 StatsAggregator 增加按本地日期批量计算百分位能力,替代逐天 fan-out 查询 - 为混合统计查询合并连续实时日期区间,并批量读取 StatsDaily,减少逐日查询次数 - 为用户日统计增加批量聚合入口,替代逐用户循环聚合 - 为系统配置导出改用 selectinload 预加载 provider 关联数据,减少 N+1 查询 - 为管理员用户列表增加钱包批量查询,避免逐用户回表 - 为调度器增加 provider 轻量引用预过滤,先按 allowed_providers 缩小范围再加载完整 provider 图 - 为 CandidateBuilder 增加 provider refs/provider_ids 查询能力,保留分页顺序 - 为模型缓存增加 provider_model_mappings 索引缓存与 model_mappings 规则缓存,减少重复全量扫描 - 为请求候选中间态改为 flush/batch commit,降低 pending/streaming 状态切换的事务往返 - 为钱包访问结果补充 balance_snapshot,并抽取余额快照复用逻辑 - 补充 pipeline、auth、admin users、user_me usage、stats aggregator、model cache、 scheduler、wallet、request candidate 等回归与契约测试
2026-03-09 22:57:23 +08:00
thread_db.close.assert_called_once()
perf: 优化请求鉴权链路并批量化统计/调度查询 - 为 Pipeline/Context 增加按需读取请求体能力,支持 async 懒加载 JSON body - 为 chat/cli/video/claude/openai-cli 适配器关闭默认预读,减少无效 body 读取与超时风险 - 将本地登录、JWT 用户加载、API Key 鉴权迁移到线程池隔离会话执行,避免阻塞事件循环 - 为 API Key 鉴权返回结构化余额结果,并在主请求会话中重新绑定 user/api_key 后再校验状态、过期和锁定信息 - 为 management/user token 前缀认证引入独立会话与结果回绑,避免跨会话对象写入失效 - 为 Usage 余额检查补充结构化返回,统一透出 remaining 与欠费/不可用文案映射 - 为用户与管理端活跃请求查询增加 maintain_status 开关,避免轮询指定 id 时误触发状态修复 - 重写 user_me usage 汇总逻辑,支持 group_by=None 的粗粒度聚合 - 修正 provider 维度成功率与平均响应时间统计,基于 success_count 和成功响应耗时汇总计算 - 前端 Usage 轮询由 setInterval 改为串行 setTimeout,避免并发轮询叠加 - 为 StatsAggregator 增加按本地日期批量计算百分位能力,替代逐天 fan-out 查询 - 为混合统计查询合并连续实时日期区间,并批量读取 StatsDaily,减少逐日查询次数 - 为用户日统计增加批量聚合入口,替代逐用户循环聚合 - 为系统配置导出改用 selectinload 预加载 provider 关联数据,减少 N+1 查询 - 为管理员用户列表增加钱包批量查询,避免逐用户回表 - 为调度器增加 provider 轻量引用预过滤,先按 allowed_providers 缩小范围再加载完整 provider 图 - 为 CandidateBuilder 增加 provider refs/provider_ids 查询能力,保留分页顺序 - 为模型缓存增加 provider_model_mappings 索引缓存与 model_mappings 规则缓存,减少重复全量扫描 - 为请求候选中间态改为 flush/batch commit,降低 pending/streaming 状态切换的事务往返 - 为钱包访问结果补充 balance_snapshot,并抽取余额快照复用逻辑 - 补充 pipeline、auth、admin users、user_me usage、stats aggregator、model cache、 scheduler、wallet、request candidate 等回归与契约测试
2026-03-09 22:57:23 +08:00
@pytest.mark.asyncio
async def test_calculate_balance_remaining_none_user(
self, pipeline: ApiRequestPipeline
) -> None:
"""Returns None when user is missing."""
remaining = await pipeline._calculate_balance_remaining_async(None)
assert remaining is None
perf: 优化请求鉴权链路并批量化统计/调度查询 - 为 Pipeline/Context 增加按需读取请求体能力,支持 async 懒加载 JSON body - 为 chat/cli/video/claude/openai-cli 适配器关闭默认预读,减少无效 body 读取与超时风险 - 将本地登录、JWT 用户加载、API Key 鉴权迁移到线程池隔离会话执行,避免阻塞事件循环 - 为 API Key 鉴权返回结构化余额结果,并在主请求会话中重新绑定 user/api_key 后再校验状态、过期和锁定信息 - 为 management/user token 前缀认证引入独立会话与结果回绑,避免跨会话对象写入失效 - 为 Usage 余额检查补充结构化返回,统一透出 remaining 与欠费/不可用文案映射 - 为用户与管理端活跃请求查询增加 maintain_status 开关,避免轮询指定 id 时误触发状态修复 - 重写 user_me usage 汇总逻辑,支持 group_by=None 的粗粒度聚合 - 修正 provider 维度成功率与平均响应时间统计,基于 success_count 和成功响应耗时汇总计算 - 前端 Usage 轮询由 setInterval 改为串行 setTimeout,避免并发轮询叠加 - 为 StatsAggregator 增加按本地日期批量计算百分位能力,替代逐天 fan-out 查询 - 为混合统计查询合并连续实时日期区间,并批量读取 StatsDaily,减少逐日查询次数 - 为用户日统计增加批量聚合入口,替代逐用户循环聚合 - 为系统配置导出改用 selectinload 预加载 provider 关联数据,减少 N+1 查询 - 为管理员用户列表增加钱包批量查询,避免逐用户回表 - 为调度器增加 provider 轻量引用预过滤,先按 allowed_providers 缩小范围再加载完整 provider 图 - 为 CandidateBuilder 增加 provider refs/provider_ids 查询能力,保留分页顺序 - 为模型缓存增加 provider_model_mappings 索引缓存与 model_mappings 规则缓存,减少重复全量扫描 - 为请求候选中间态改为 flush/batch commit,降低 pending/streaming 状态切换的事务往返 - 为钱包访问结果补充 balance_snapshot,并抽取余额快照复用逻辑 - 补充 pipeline、auth、admin users、user_me usage、stats aggregator、model cache、 scheduler、wallet、request candidate 等回归与契约测试
2026-03-09 22:57:23 +08:00
class TestPipelineRunModes:
"""Returns remaining balance for limited wallets."""
@pytest.fixture
def pipeline(self) -> ApiRequestPipeline:
return ApiRequestPipeline()
@pytest.mark.asyncio
async def test_run_management_mode_skips_balance_calculation(
self, pipeline: ApiRequestPipeline
) -> None:
"""Management mode skips balance calculation."""
mock_request = MagicMock()
mock_request.method = "GET"
mock_request.url.path = "/api/admin/tokens"
mock_request.state = MagicMock()
mock_db = MagicMock()
mock_user = MagicMock()
mock_user.id = "admin-123"
mock_token = MagicMock()
mock_token.id = "mt-123"
mock_adapter = MagicMock()
mock_adapter.name = "test-adapter"
mock_adapter.authorize = MagicMock(return_value=None)
mock_response = MagicMock()
mock_response.status_code = 200
mock_adapter.handle = AsyncMock(return_value=mock_response)
mock_context = MagicMock()
mock_context.db = mock_db
mock_context.request = mock_request
with patch.object(
pipeline,
"_authenticate_management",
new_callable=AsyncMock,
return_value=(mock_user, mock_token),
):
with patch(
"src.api.base.pipeline.ApiRequestContext.build",
return_value=mock_context,
):
with patch.object(
pipeline,
"_calculate_balance_remaining_async",
new_callable=AsyncMock,
) as mock_balance:
with patch.object(pipeline, "_record_audit_event"):
response = await pipeline.run(
mock_adapter,
mock_request,
mock_db,
mode=ApiMode.MANAGEMENT,
)
assert response == mock_response
assert mock_context.management_token == mock_token
mock_balance.assert_not_called()
class TestPipelineAuditLogging:
"""测试 Pipeline 审计日志"""
@pytest.fixture
def pipeline(self) -> ApiRequestPipeline:
return ApiRequestPipeline()
def test_record_audit_event_success(self, pipeline: ApiRequestPipeline) -> None:
"""测试记录成功的审计事件"""
mock_context = MagicMock()
mock_context.db = MagicMock()
mock_context.user = MagicMock()
mock_context.user.id = "user-123"
mock_context.api_key = MagicMock()
mock_context.api_key.id = "key-123"
mock_context.request_id = "req-123"
mock_context.client_ip = "127.0.0.1"
mock_context.user_agent = "test-agent"
mock_context.request_method = "POST"
mock_context.request_path = "/v1/messages"
mock_context.tx_committed_by_route = False
mock_context.gateway_execution_path = None
mock_context.rate_limit_scope = None
mock_context.sync_runtime_state_from_request = MagicMock()
mock_context.request = MagicMock()
mock_context.request.method = "POST"
mock_context.request.url.path = "/v1/messages"
mock_context.original_headers = {}
mock_context.start_time = 1000.0
mock_adapter = MagicMock()
mock_adapter.name = "test-adapter"
mock_adapter.audit_log_enabled = True
mock_adapter.audit_success_event = None
mock_adapter.audit_failure_event = None
with patch.object(
pipeline.audit_service,
"log_event",
) as mock_log:
with patch("time.time", return_value=1001.0):
pipeline._record_audit_event(
mock_context, mock_adapter, success=True, status_code=200
)
mock_log.assert_called_once()
call_kwargs = mock_log.call_args[1]
assert call_kwargs["user_id"] == "user-123"
assert call_kwargs["status_code"] == 200
def test_record_audit_event_failure(self, pipeline: ApiRequestPipeline) -> None:
"""测试记录失败的审计事件"""
mock_context = MagicMock()
mock_context.db = MagicMock()
mock_context.user = MagicMock()
mock_context.user.id = "user-123"
mock_context.api_key = MagicMock()
mock_context.api_key.id = "key-123"
mock_context.request_id = "req-123"
mock_context.client_ip = "127.0.0.1"
mock_context.user_agent = "test-agent"
mock_context.request_method = "POST"
mock_context.request_path = "/v1/messages"
mock_context.tx_committed_by_route = False
mock_context.gateway_execution_path = None
mock_context.rate_limit_scope = None
mock_context.sync_runtime_state_from_request = MagicMock()
mock_context.request = MagicMock()
mock_context.request.method = "POST"
mock_context.request.url.path = "/v1/messages"
mock_context.original_headers = {}
mock_context.start_time = 1000.0
mock_adapter = MagicMock()
mock_adapter.name = "test-adapter"
mock_adapter.audit_log_enabled = True
mock_adapter.audit_success_event = None
mock_adapter.audit_failure_event = None
with patch.object(
pipeline.audit_service,
"log_event",
) as mock_log:
with patch("time.time", return_value=1001.0):
pipeline._record_audit_event(
mock_context,
mock_adapter,
success=False,
status_code=500,
error="Internal error",
)
mock_log.assert_called_once()
call_kwargs = mock_log.call_args[1]
assert call_kwargs["status_code"] == 500
assert call_kwargs["error_message"] == "Internal error"
def test_record_audit_event_commits_when_route_already_committed(
self, pipeline: ApiRequestPipeline
) -> None:
mock_context = MagicMock()
mock_context.db = MagicMock()
mock_context.user = MagicMock()
mock_context.user.id = "user-123"
mock_context.api_key = None
mock_context.request_id = "req-123"
mock_context.client_ip = "127.0.0.1"
mock_context.user_agent = "test-agent"
mock_context.request_method = "POST"
mock_context.request_path = "/api/auth/refresh"
mock_context.tx_committed_by_route = False
mock_context.gateway_execution_path = None
mock_context.rate_limit_scope = None
mock_context.request = MagicMock()
mock_context.request.method = "POST"
mock_context.request.url.path = "/api/auth/refresh"
mock_context.request.state = SimpleNamespace(tx_committed_by_route=True)
mock_context.original_headers = {}
mock_context.start_time = 1000.0
mock_context.sync_runtime_state_from_request = MagicMock(
side_effect=lambda: setattr(mock_context, "tx_committed_by_route", True)
)
mock_adapter = MagicMock()
mock_adapter.name = "test-adapter"
mock_adapter.audit_log_enabled = True
mock_adapter.audit_success_event = None
mock_adapter.audit_failure_event = None
with patch.object(pipeline.audit_service, "log_event") as mock_log:
pipeline._record_audit_event(mock_context, mock_adapter, success=True, status_code=200)
mock_log.assert_called_once()
mock_context.db.commit.assert_called_once()
def test_record_audit_event_no_db(self, pipeline: ApiRequestPipeline) -> None:
"""测试没有数据库会话时跳过审计"""
mock_context = MagicMock()
mock_context.db = None
mock_context.sync_runtime_state_from_request = MagicMock()
mock_context.tx_committed_by_route = False
mock_adapter = MagicMock()
mock_adapter.audit_log_enabled = True
with patch.object(
pipeline.audit_service,
"log_event",
) as mock_log:
# 不应该抛出异常
pipeline._record_audit_event(mock_context, mock_adapter, success=True)
# 不应该调用 log_event
mock_log.assert_not_called()
def test_record_audit_event_disabled(self, pipeline: ApiRequestPipeline) -> None:
"""测试审计日志被禁用时跳过"""
mock_context = MagicMock()
mock_context.db = MagicMock()
mock_adapter = MagicMock()
mock_adapter.audit_log_enabled = False
with patch.object(
pipeline.audit_service,
"log_event",
) as mock_log:
pipeline._record_audit_event(mock_context, mock_adapter, success=True)
mock_log.assert_not_called()
def test_record_audit_event_exception_handling(self, pipeline: ApiRequestPipeline) -> None:
"""测试审计日志异常不影响主流程"""
mock_context = MagicMock()
mock_context.db = MagicMock()
mock_context.user = MagicMock()
mock_context.user.id = "user-123"
mock_context.api_key = MagicMock()
mock_context.api_key.id = "key-123"
mock_context.request_id = "req-123"
mock_context.client_ip = "127.0.0.1"
mock_context.user_agent = "test-agent"
mock_context.request_method = "POST"
mock_context.request_path = "/v1/messages"
mock_context.tx_committed_by_route = False
mock_context.gateway_execution_path = None
mock_context.rate_limit_scope = None
mock_context.sync_runtime_state_from_request = MagicMock()
mock_context.request = MagicMock()
mock_context.request.method = "POST"
mock_context.request.url.path = "/v1/messages"
mock_context.start_time = 1000.0
mock_context.original_headers = {}
mock_adapter = MagicMock()
mock_adapter.name = "test-adapter"
mock_adapter.audit_log_enabled = True
mock_adapter.audit_success_event = None
with patch.object(
pipeline.audit_service,
"log_event",
side_effect=Exception("DB error"),
):
with patch("time.time", return_value=1001.0):
# 不应该抛出异常
pipeline._record_audit_event(mock_context, mock_adapter, success=True)
def test_build_audit_metadata_prefers_context_path_params(
self, pipeline: ApiRequestPipeline
) -> None:
mock_context = MagicMock()
mock_context.start_time = 1000.0
mock_context.mode = "standard"
mock_context.api_format_hint = "gemini"
mock_context.query_params = {}
mock_context.raw_body = b"{}"
mock_context.balance_remaining = 12.5
mock_context.audit_metadata = {}
mock_context.quiet_logging = False
mock_context.user = None
mock_context.api_key = None
mock_context.request_method = "POST"
mock_context.request_path = "/v1beta/models/gemini-2.5-flash:streamGenerateContent"
mock_context.tx_committed_by_route = False
mock_context.gateway_execution_path = "executor_local"
mock_context.rate_limit_scope = "user"
mock_context.sync_runtime_state_from_request = MagicMock()
mock_context.path_params = {"model": "gemini-2.5-flash", "stream": True}
mock_context.request = MagicMock()
mock_context.request.method = "POST"
mock_context.request.url.path = "/v1beta/models/gemini-2.5-flash:streamGenerateContent"
mock_context.original_headers = {"content-type": "application/json"}
mock_adapter = MagicMock()
mock_adapter.name = "public.gemini.content"
mock_adapter.__class__.__name__ = "PublicGeminiContentAdapter"
mock_adapter.mode.value = "standard"
mock_adapter.get_audit_metadata.return_value = {}
with patch("time.time", return_value=1001.0):
metadata = pipeline._build_audit_metadata(
context=mock_context,
adapter=mock_adapter,
success=True,
status_code=200,
error=None,
)
assert metadata["path_params"] == {"model": "gemini-2.5-flash", "stream": True}
assert metadata["gateway_execution_path"] == "executor_local"
assert metadata["rate_limit_scope"] == "user"
@pytest.mark.asyncio
async def test_apply_context_runtime_state_prefers_snapshot_balance(
self, pipeline: ApiRequestPipeline
) -> None:
context = MagicMock()
context.prefetched_balance_remaining = 9.5
context.balance_remaining = None
context.management_token = None
context.quiet_logging = False
auth_state = SimpleNamespace(
user=MagicMock(id="user-1"),
api_key=MagicMock(id="key-1"),
management_token=None,
)
pipeline._calculate_balance_remaining_async = AsyncMock(
side_effect=AssertionError("prefetched snapshot should skip balance lookup")
)
await pipeline._apply_context_runtime_state_legacy(
context,
mode=ApiMode.STANDARD,
auth_state=auth_state,
quiet=True,
)
assert context.balance_remaining == 9.5
assert context.quiet_logging is True
class TestPipelineAuthentication:
"""测试 Pipeline 认证相关逻辑"""
@pytest.fixture
def pipeline(self) -> ApiRequestPipeline:
return ApiRequestPipeline()
perf: 优化请求鉴权链路并批量化统计/调度查询 - 为 Pipeline/Context 增加按需读取请求体能力,支持 async 懒加载 JSON body - 为 chat/cli/video/claude/openai-cli 适配器关闭默认预读,减少无效 body 读取与超时风险 - 将本地登录、JWT 用户加载、API Key 鉴权迁移到线程池隔离会话执行,避免阻塞事件循环 - 为 API Key 鉴权返回结构化余额结果,并在主请求会话中重新绑定 user/api_key 后再校验状态、过期和锁定信息 - 为 management/user token 前缀认证引入独立会话与结果回绑,避免跨会话对象写入失效 - 为 Usage 余额检查补充结构化返回,统一透出 remaining 与欠费/不可用文案映射 - 为用户与管理端活跃请求查询增加 maintain_status 开关,避免轮询指定 id 时误触发状态修复 - 重写 user_me usage 汇总逻辑,支持 group_by=None 的粗粒度聚合 - 修正 provider 维度成功率与平均响应时间统计,基于 success_count 和成功响应耗时汇总计算 - 前端 Usage 轮询由 setInterval 改为串行 setTimeout,避免并发轮询叠加 - 为 StatsAggregator 增加按本地日期批量计算百分位能力,替代逐天 fan-out 查询 - 为混合统计查询合并连续实时日期区间,并批量读取 StatsDaily,减少逐日查询次数 - 为用户日统计增加批量聚合入口,替代逐用户循环聚合 - 为系统配置导出改用 selectinload 预加载 provider 关联数据,减少 N+1 查询 - 为管理员用户列表增加钱包批量查询,避免逐用户回表 - 为调度器增加 provider 轻量引用预过滤,先按 allowed_providers 缩小范围再加载完整 provider 图 - 为 CandidateBuilder 增加 provider refs/provider_ids 查询能力,保留分页顺序 - 为模型缓存增加 provider_model_mappings 索引缓存与 model_mappings 规则缓存,减少重复全量扫描 - 为请求候选中间态改为 flush/batch commit,降低 pending/streaming 状态切换的事务往返 - 为钱包访问结果补充 balance_snapshot,并抽取余额快照复用逻辑 - 补充 pipeline、auth、admin users、user_me usage、stats aggregator、model cache、 scheduler、wallet、request candidate 等回归与契约测试
2026-03-09 22:57:23 +08:00
@pytest.mark.asyncio
async def test_authenticate_client_missing_key(self, pipeline: ApiRequestPipeline) -> None:
"""测试缺少 API Key 时抛出异常"""
mock_request = MagicMock()
mock_request.headers = {}
mock_request.url.path = "/v1/messages"
mock_request.state = MagicMock()
mock_db = MagicMock()
mock_adapter = MagicMock()
mock_adapter.extract_api_key = MagicMock(return_value=None)
with pytest.raises(HTTPException) as exc_info:
perf: 优化请求鉴权链路并批量化统计/调度查询 - 为 Pipeline/Context 增加按需读取请求体能力,支持 async 懒加载 JSON body - 为 chat/cli/video/claude/openai-cli 适配器关闭默认预读,减少无效 body 读取与超时风险 - 将本地登录、JWT 用户加载、API Key 鉴权迁移到线程池隔离会话执行,避免阻塞事件循环 - 为 API Key 鉴权返回结构化余额结果,并在主请求会话中重新绑定 user/api_key 后再校验状态、过期和锁定信息 - 为 management/user token 前缀认证引入独立会话与结果回绑,避免跨会话对象写入失效 - 为 Usage 余额检查补充结构化返回,统一透出 remaining 与欠费/不可用文案映射 - 为用户与管理端活跃请求查询增加 maintain_status 开关,避免轮询指定 id 时误触发状态修复 - 重写 user_me usage 汇总逻辑,支持 group_by=None 的粗粒度聚合 - 修正 provider 维度成功率与平均响应时间统计,基于 success_count 和成功响应耗时汇总计算 - 前端 Usage 轮询由 setInterval 改为串行 setTimeout,避免并发轮询叠加 - 为 StatsAggregator 增加按本地日期批量计算百分位能力,替代逐天 fan-out 查询 - 为混合统计查询合并连续实时日期区间,并批量读取 StatsDaily,减少逐日查询次数 - 为用户日统计增加批量聚合入口,替代逐用户循环聚合 - 为系统配置导出改用 selectinload 预加载 provider 关联数据,减少 N+1 查询 - 为管理员用户列表增加钱包批量查询,避免逐用户回表 - 为调度器增加 provider 轻量引用预过滤,先按 allowed_providers 缩小范围再加载完整 provider 图 - 为 CandidateBuilder 增加 provider refs/provider_ids 查询能力,保留分页顺序 - 为模型缓存增加 provider_model_mappings 索引缓存与 model_mappings 规则缓存,减少重复全量扫描 - 为请求候选中间态改为 flush/batch commit,降低 pending/streaming 状态切换的事务往返 - 为钱包访问结果补充 balance_snapshot,并抽取余额快照复用逻辑 - 补充 pipeline、auth、admin users、user_me usage、stats aggregator、model cache、 scheduler、wallet、request candidate 等回归与契约测试
2026-03-09 22:57:23 +08:00
await pipeline._authenticate_client(mock_request, mock_db, mock_adapter)
assert exc_info.value.status_code == 401
assert "API密钥" in exc_info.value.detail
perf: 优化请求鉴权链路并批量化统计/调度查询 - 为 Pipeline/Context 增加按需读取请求体能力,支持 async 懒加载 JSON body - 为 chat/cli/video/claude/openai-cli 适配器关闭默认预读,减少无效 body 读取与超时风险 - 将本地登录、JWT 用户加载、API Key 鉴权迁移到线程池隔离会话执行,避免阻塞事件循环 - 为 API Key 鉴权返回结构化余额结果,并在主请求会话中重新绑定 user/api_key 后再校验状态、过期和锁定信息 - 为 management/user token 前缀认证引入独立会话与结果回绑,避免跨会话对象写入失效 - 为 Usage 余额检查补充结构化返回,统一透出 remaining 与欠费/不可用文案映射 - 为用户与管理端活跃请求查询增加 maintain_status 开关,避免轮询指定 id 时误触发状态修复 - 重写 user_me usage 汇总逻辑,支持 group_by=None 的粗粒度聚合 - 修正 provider 维度成功率与平均响应时间统计,基于 success_count 和成功响应耗时汇总计算 - 前端 Usage 轮询由 setInterval 改为串行 setTimeout,避免并发轮询叠加 - 为 StatsAggregator 增加按本地日期批量计算百分位能力,替代逐天 fan-out 查询 - 为混合统计查询合并连续实时日期区间,并批量读取 StatsDaily,减少逐日查询次数 - 为用户日统计增加批量聚合入口,替代逐用户循环聚合 - 为系统配置导出改用 selectinload 预加载 provider 关联数据,减少 N+1 查询 - 为管理员用户列表增加钱包批量查询,避免逐用户回表 - 为调度器增加 provider 轻量引用预过滤,先按 allowed_providers 缩小范围再加载完整 provider 图 - 为 CandidateBuilder 增加 provider refs/provider_ids 查询能力,保留分页顺序 - 为模型缓存增加 provider_model_mappings 索引缓存与 model_mappings 规则缓存,减少重复全量扫描 - 为请求候选中间态改为 flush/batch commit,降低 pending/streaming 状态切换的事务往返 - 为钱包访问结果补充 balance_snapshot,并抽取余额快照复用逻辑 - 补充 pipeline、auth、admin users、user_me usage、stats aggregator、model cache、 scheduler、wallet、request candidate 等回归与契约测试
2026-03-09 22:57:23 +08:00
@pytest.mark.asyncio
async def test_authenticate_client_invalid_key(self, pipeline: ApiRequestPipeline) -> None:
"""测试无效的 API Key"""
mock_request = MagicMock()
mock_request.headers = {"Authorization": "Bearer sk-invalid"}
mock_request.url.path = "/v1/messages"
mock_request.state = MagicMock()
mock_db = MagicMock()
mock_adapter = MagicMock()
mock_adapter.extract_api_key = MagicMock(return_value="sk-invalid")
with patch.object(
pipeline.auth_service,
perf: 优化请求鉴权链路并批量化统计/调度查询 - 为 Pipeline/Context 增加按需读取请求体能力,支持 async 懒加载 JSON body - 为 chat/cli/video/claude/openai-cli 适配器关闭默认预读,减少无效 body 读取与超时风险 - 将本地登录、JWT 用户加载、API Key 鉴权迁移到线程池隔离会话执行,避免阻塞事件循环 - 为 API Key 鉴权返回结构化余额结果,并在主请求会话中重新绑定 user/api_key 后再校验状态、过期和锁定信息 - 为 management/user token 前缀认证引入独立会话与结果回绑,避免跨会话对象写入失效 - 为 Usage 余额检查补充结构化返回,统一透出 remaining 与欠费/不可用文案映射 - 为用户与管理端活跃请求查询增加 maintain_status 开关,避免轮询指定 id 时误触发状态修复 - 重写 user_me usage 汇总逻辑,支持 group_by=None 的粗粒度聚合 - 修正 provider 维度成功率与平均响应时间统计,基于 success_count 和成功响应耗时汇总计算 - 前端 Usage 轮询由 setInterval 改为串行 setTimeout,避免并发轮询叠加 - 为 StatsAggregator 增加按本地日期批量计算百分位能力,替代逐天 fan-out 查询 - 为混合统计查询合并连续实时日期区间,并批量读取 StatsDaily,减少逐日查询次数 - 为用户日统计增加批量聚合入口,替代逐用户循环聚合 - 为系统配置导出改用 selectinload 预加载 provider 关联数据,减少 N+1 查询 - 为管理员用户列表增加钱包批量查询,避免逐用户回表 - 为调度器增加 provider 轻量引用预过滤,先按 allowed_providers 缩小范围再加载完整 provider 图 - 为 CandidateBuilder 增加 provider refs/provider_ids 查询能力,保留分页顺序 - 为模型缓存增加 provider_model_mappings 索引缓存与 model_mappings 规则缓存,减少重复全量扫描 - 为请求候选中间态改为 flush/batch commit,降低 pending/streaming 状态切换的事务往返 - 为钱包访问结果补充 balance_snapshot,并抽取余额快照复用逻辑 - 补充 pipeline、auth、admin users、user_me usage、stats aggregator、model cache、 scheduler、wallet、request candidate 等回归与契约测试
2026-03-09 22:57:23 +08:00
"authenticate_api_key_threadsafe",
new_callable=AsyncMock,
return_value=None,
):
with pytest.raises(HTTPException) as exc_info:
perf: 优化请求鉴权链路并批量化统计/调度查询 - 为 Pipeline/Context 增加按需读取请求体能力,支持 async 懒加载 JSON body - 为 chat/cli/video/claude/openai-cli 适配器关闭默认预读,减少无效 body 读取与超时风险 - 将本地登录、JWT 用户加载、API Key 鉴权迁移到线程池隔离会话执行,避免阻塞事件循环 - 为 API Key 鉴权返回结构化余额结果,并在主请求会话中重新绑定 user/api_key 后再校验状态、过期和锁定信息 - 为 management/user token 前缀认证引入独立会话与结果回绑,避免跨会话对象写入失效 - 为 Usage 余额检查补充结构化返回,统一透出 remaining 与欠费/不可用文案映射 - 为用户与管理端活跃请求查询增加 maintain_status 开关,避免轮询指定 id 时误触发状态修复 - 重写 user_me usage 汇总逻辑,支持 group_by=None 的粗粒度聚合 - 修正 provider 维度成功率与平均响应时间统计,基于 success_count 和成功响应耗时汇总计算 - 前端 Usage 轮询由 setInterval 改为串行 setTimeout,避免并发轮询叠加 - 为 StatsAggregator 增加按本地日期批量计算百分位能力,替代逐天 fan-out 查询 - 为混合统计查询合并连续实时日期区间,并批量读取 StatsDaily,减少逐日查询次数 - 为用户日统计增加批量聚合入口,替代逐用户循环聚合 - 为系统配置导出改用 selectinload 预加载 provider 关联数据,减少 N+1 查询 - 为管理员用户列表增加钱包批量查询,避免逐用户回表 - 为调度器增加 provider 轻量引用预过滤,先按 allowed_providers 缩小范围再加载完整 provider 图 - 为 CandidateBuilder 增加 provider refs/provider_ids 查询能力,保留分页顺序 - 为模型缓存增加 provider_model_mappings 索引缓存与 model_mappings 规则缓存,减少重复全量扫描 - 为请求候选中间态改为 flush/batch commit,降低 pending/streaming 状态切换的事务往返 - 为钱包访问结果补充 balance_snapshot,并抽取余额快照复用逻辑 - 补充 pipeline、auth、admin users、user_me usage、stats aggregator、model cache、 scheduler、wallet、request candidate 等回归与契约测试
2026-03-09 22:57:23 +08:00
await pipeline._authenticate_client(mock_request, mock_db, mock_adapter)
assert exc_info.value.status_code == 401
perf: 优化请求鉴权链路并批量化统计/调度查询 - 为 Pipeline/Context 增加按需读取请求体能力,支持 async 懒加载 JSON body - 为 chat/cli/video/claude/openai-cli 适配器关闭默认预读,减少无效 body 读取与超时风险 - 将本地登录、JWT 用户加载、API Key 鉴权迁移到线程池隔离会话执行,避免阻塞事件循环 - 为 API Key 鉴权返回结构化余额结果,并在主请求会话中重新绑定 user/api_key 后再校验状态、过期和锁定信息 - 为 management/user token 前缀认证引入独立会话与结果回绑,避免跨会话对象写入失效 - 为 Usage 余额检查补充结构化返回,统一透出 remaining 与欠费/不可用文案映射 - 为用户与管理端活跃请求查询增加 maintain_status 开关,避免轮询指定 id 时误触发状态修复 - 重写 user_me usage 汇总逻辑,支持 group_by=None 的粗粒度聚合 - 修正 provider 维度成功率与平均响应时间统计,基于 success_count 和成功响应耗时汇总计算 - 前端 Usage 轮询由 setInterval 改为串行 setTimeout,避免并发轮询叠加 - 为 StatsAggregator 增加按本地日期批量计算百分位能力,替代逐天 fan-out 查询 - 为混合统计查询合并连续实时日期区间,并批量读取 StatsDaily,减少逐日查询次数 - 为用户日统计增加批量聚合入口,替代逐用户循环聚合 - 为系统配置导出改用 selectinload 预加载 provider 关联数据,减少 N+1 查询 - 为管理员用户列表增加钱包批量查询,避免逐用户回表 - 为调度器增加 provider 轻量引用预过滤,先按 allowed_providers 缩小范围再加载完整 provider 图 - 为 CandidateBuilder 增加 provider refs/provider_ids 查询能力,保留分页顺序 - 为模型缓存增加 provider_model_mappings 索引缓存与 model_mappings 规则缓存,减少重复全量扫描 - 为请求候选中间态改为 flush/batch commit,降低 pending/streaming 状态切换的事务往返 - 为钱包访问结果补充 balance_snapshot,并抽取余额快照复用逻辑 - 补充 pipeline、auth、admin users、user_me usage、stats aggregator、model cache、 scheduler、wallet、request candidate 等回归与契约测试
2026-03-09 22:57:23 +08:00
@pytest.mark.asyncio
async def test_authenticate_client_balance_exceeded(self, pipeline: ApiRequestPipeline) -> None:
"""测试余额不足时抛出异常"""
mock_user = MagicMock()
mock_user.id = "user-123"
mock_api_key = MagicMock()
mock_api_key.id = "key-123"
mock_api_key.is_standalone = False
mock_request = MagicMock()
mock_request.headers = {"Authorization": "Bearer sk-test"}
mock_request.url.path = "/v1/messages"
mock_request.state = MagicMock()
mock_db = MagicMock()
perf: 优化请求鉴权链路并批量化统计/调度查询 - 为 Pipeline/Context 增加按需读取请求体能力,支持 async 懒加载 JSON body - 为 chat/cli/video/claude/openai-cli 适配器关闭默认预读,减少无效 body 读取与超时风险 - 将本地登录、JWT 用户加载、API Key 鉴权迁移到线程池隔离会话执行,避免阻塞事件循环 - 为 API Key 鉴权返回结构化余额结果,并在主请求会话中重新绑定 user/api_key 后再校验状态、过期和锁定信息 - 为 management/user token 前缀认证引入独立会话与结果回绑,避免跨会话对象写入失效 - 为 Usage 余额检查补充结构化返回,统一透出 remaining 与欠费/不可用文案映射 - 为用户与管理端活跃请求查询增加 maintain_status 开关,避免轮询指定 id 时误触发状态修复 - 重写 user_me usage 汇总逻辑,支持 group_by=None 的粗粒度聚合 - 修正 provider 维度成功率与平均响应时间统计,基于 success_count 和成功响应耗时汇总计算 - 前端 Usage 轮询由 setInterval 改为串行 setTimeout,避免并发轮询叠加 - 为 StatsAggregator 增加按本地日期批量计算百分位能力,替代逐天 fan-out 查询 - 为混合统计查询合并连续实时日期区间,并批量读取 StatsDaily,减少逐日查询次数 - 为用户日统计增加批量聚合入口,替代逐用户循环聚合 - 为系统配置导出改用 selectinload 预加载 provider 关联数据,减少 N+1 查询 - 为管理员用户列表增加钱包批量查询,避免逐用户回表 - 为调度器增加 provider 轻量引用预过滤,先按 allowed_providers 缩小范围再加载完整 provider 图 - 为 CandidateBuilder 增加 provider refs/provider_ids 查询能力,保留分页顺序 - 为模型缓存增加 provider_model_mappings 索引缓存与 model_mappings 规则缓存,减少重复全量扫描 - 为请求候选中间态改为 flush/batch commit,降低 pending/streaming 状态切换的事务往返 - 为钱包访问结果补充 balance_snapshot,并抽取余额快照复用逻辑 - 补充 pipeline、auth、admin users、user_me usage、stats aggregator、model cache、 scheduler、wallet、request candidate 等回归与契约测试
2026-03-09 22:57:23 +08:00
db_user = MagicMock()
db_user.id = "user-123"
db_user.is_active = True
db_user.is_deleted = False
db_api_key = MagicMock()
db_api_key.id = "key-123"
db_api_key.user_id = "user-123"
db_api_key.is_active = True
db_api_key.is_locked = False
db_api_key.is_standalone = False
db_api_key.expires_at = None
user_query = MagicMock()
user_query.filter.return_value.first.return_value = db_user
api_key_query = MagicMock()
api_key_query.filter.return_value.first.return_value = db_api_key
mock_db.query.side_effect = [user_query, api_key_query]
mock_adapter = MagicMock()
mock_adapter.extract_api_key = MagicMock(return_value="sk-test")
with patch.object(
pipeline.auth_service,
perf: 优化请求鉴权链路并批量化统计/调度查询 - 为 Pipeline/Context 增加按需读取请求体能力,支持 async 懒加载 JSON body - 为 chat/cli/video/claude/openai-cli 适配器关闭默认预读,减少无效 body 读取与超时风险 - 将本地登录、JWT 用户加载、API Key 鉴权迁移到线程池隔离会话执行,避免阻塞事件循环 - 为 API Key 鉴权返回结构化余额结果,并在主请求会话中重新绑定 user/api_key 后再校验状态、过期和锁定信息 - 为 management/user token 前缀认证引入独立会话与结果回绑,避免跨会话对象写入失效 - 为 Usage 余额检查补充结构化返回,统一透出 remaining 与欠费/不可用文案映射 - 为用户与管理端活跃请求查询增加 maintain_status 开关,避免轮询指定 id 时误触发状态修复 - 重写 user_me usage 汇总逻辑,支持 group_by=None 的粗粒度聚合 - 修正 provider 维度成功率与平均响应时间统计,基于 success_count 和成功响应耗时汇总计算 - 前端 Usage 轮询由 setInterval 改为串行 setTimeout,避免并发轮询叠加 - 为 StatsAggregator 增加按本地日期批量计算百分位能力,替代逐天 fan-out 查询 - 为混合统计查询合并连续实时日期区间,并批量读取 StatsDaily,减少逐日查询次数 - 为用户日统计增加批量聚合入口,替代逐用户循环聚合 - 为系统配置导出改用 selectinload 预加载 provider 关联数据,减少 N+1 查询 - 为管理员用户列表增加钱包批量查询,避免逐用户回表 - 为调度器增加 provider 轻量引用预过滤,先按 allowed_providers 缩小范围再加载完整 provider 图 - 为 CandidateBuilder 增加 provider refs/provider_ids 查询能力,保留分页顺序 - 为模型缓存增加 provider_model_mappings 索引缓存与 model_mappings 规则缓存,减少重复全量扫描 - 为请求候选中间态改为 flush/batch commit,降低 pending/streaming 状态切换的事务往返 - 为钱包访问结果补充 balance_snapshot,并抽取余额快照复用逻辑 - 补充 pipeline、auth、admin users、user_me usage、stats aggregator、model cache、 scheduler、wallet、request candidate 等回归与契约测试
2026-03-09 22:57:23 +08:00
"authenticate_api_key_threadsafe",
new_callable=AsyncMock,
return_value=MagicMock(
user=mock_user,
api_key=mock_api_key,
access_allowed=False,
balance_remaining=0.0,
),
):
perf: 优化请求鉴权链路并批量化统计/调度查询 - 为 Pipeline/Context 增加按需读取请求体能力,支持 async 懒加载 JSON body - 为 chat/cli/video/claude/openai-cli 适配器关闭默认预读,减少无效 body 读取与超时风险 - 将本地登录、JWT 用户加载、API Key 鉴权迁移到线程池隔离会话执行,避免阻塞事件循环 - 为 API Key 鉴权返回结构化余额结果,并在主请求会话中重新绑定 user/api_key 后再校验状态、过期和锁定信息 - 为 management/user token 前缀认证引入独立会话与结果回绑,避免跨会话对象写入失效 - 为 Usage 余额检查补充结构化返回,统一透出 remaining 与欠费/不可用文案映射 - 为用户与管理端活跃请求查询增加 maintain_status 开关,避免轮询指定 id 时误触发状态修复 - 重写 user_me usage 汇总逻辑,支持 group_by=None 的粗粒度聚合 - 修正 provider 维度成功率与平均响应时间统计,基于 success_count 和成功响应耗时汇总计算 - 前端 Usage 轮询由 setInterval 改为串行 setTimeout,避免并发轮询叠加 - 为 StatsAggregator 增加按本地日期批量计算百分位能力,替代逐天 fan-out 查询 - 为混合统计查询合并连续实时日期区间,并批量读取 StatsDaily,减少逐日查询次数 - 为用户日统计增加批量聚合入口,替代逐用户循环聚合 - 为系统配置导出改用 selectinload 预加载 provider 关联数据,减少 N+1 查询 - 为管理员用户列表增加钱包批量查询,避免逐用户回表 - 为调度器增加 provider 轻量引用预过滤,先按 allowed_providers 缩小范围再加载完整 provider 图 - 为 CandidateBuilder 增加 provider refs/provider_ids 查询能力,保留分页顺序 - 为模型缓存增加 provider_model_mappings 索引缓存与 model_mappings 规则缓存,减少重复全量扫描 - 为请求候选中间态改为 flush/batch commit,降低 pending/streaming 状态切换的事务往返 - 为钱包访问结果补充 balance_snapshot,并抽取余额快照复用逻辑 - 补充 pipeline、auth、admin users、user_me usage、stats aggregator、model cache、 scheduler、wallet、request candidate 等回归与契约测试
2026-03-09 22:57:23 +08:00
from src.core.exceptions import BalanceInsufficientException
with pytest.raises(BalanceInsufficientException):
await pipeline._authenticate_client(mock_request, mock_db, mock_adapter)
@pytest.mark.asyncio
async def test_authenticate_client_requery_detects_inactive_user(
self, pipeline: ApiRequestPipeline
) -> None:
mock_user = MagicMock()
mock_user.id = "user-123"
mock_api_key = MagicMock()
mock_api_key.id = "key-123"
mock_request = MagicMock()
mock_request.headers = {"Authorization": "Bearer sk-test"}
mock_request.url.path = "/v1/messages"
mock_request.state = MagicMock()
mock_adapter = MagicMock()
mock_adapter.extract_api_key = MagicMock(return_value="sk-test")
db_user = MagicMock()
db_user.id = "user-123"
db_user.is_active = False
db_user.is_deleted = False
db_api_key = MagicMock()
db_api_key.id = "key-123"
db_api_key.user_id = "user-123"
db_api_key.is_active = True
db_api_key.is_locked = False
db_api_key.is_standalone = False
db_api_key.expires_at = None
mock_db = MagicMock()
user_query = MagicMock()
user_query.filter.return_value.first.return_value = db_user
api_key_query = MagicMock()
api_key_query.filter.return_value.first.return_value = db_api_key
mock_db.query.side_effect = [user_query, api_key_query]
with patch.object(
pipeline.auth_service,
"authenticate_api_key_threadsafe",
new_callable=AsyncMock,
return_value=MagicMock(
user=mock_user,
api_key=mock_api_key,
access_allowed=True,
balance_remaining=10.0,
),
):
with pytest.raises(HTTPException) as exc_info:
await pipeline._authenticate_client(mock_request, mock_db, mock_adapter)
assert exc_info.value.status_code == 401
@pytest.mark.asyncio
async def test_authenticate_client_requery_detects_locked_key(
self, pipeline: ApiRequestPipeline
) -> None:
mock_user = MagicMock()
mock_user.id = "user-123"
mock_api_key = MagicMock()
mock_api_key.id = "key-123"
mock_request = MagicMock()
mock_request.headers = {"Authorization": "Bearer sk-test"}
mock_request.url.path = "/v1/messages"
mock_request.state = MagicMock()
mock_adapter = MagicMock()
mock_adapter.extract_api_key = MagicMock(return_value="sk-test")
db_user = MagicMock()
db_user.id = "user-123"
db_user.is_active = True
db_user.is_deleted = False
db_api_key = MagicMock()
db_api_key.id = "key-123"
db_api_key.user_id = "user-123"
db_api_key.is_active = True
db_api_key.is_locked = True
db_api_key.is_standalone = False
db_api_key.expires_at = None
mock_db = MagicMock()
user_query = MagicMock()
user_query.filter.return_value.first.return_value = db_user
api_key_query = MagicMock()
api_key_query.filter.return_value.first.return_value = db_api_key
mock_db.query.side_effect = [user_query, api_key_query]
with patch.object(
pipeline.auth_service,
"authenticate_api_key_threadsafe",
new_callable=AsyncMock,
return_value=MagicMock(
user=mock_user,
api_key=mock_api_key,
access_allowed=True,
balance_remaining=10.0,
),
):
with pytest.raises(HTTPException) as exc_info:
await pipeline._authenticate_client(mock_request, mock_db, mock_adapter)
assert exc_info.value.status_code == 403
assert "锁定" in str(exc_info.value.detail)
@pytest.mark.asyncio
async def test_authenticate_client_reuses_trusted_gateway_auth_context(
self, pipeline: ApiRequestPipeline
) -> None:
mock_request = MagicMock()
mock_request.headers = {
"x-aether-gateway": "rust-phase3b",
"x-aether-execution-path": "public_proxy_after_executor_miss",
"x-aether-auth-user-id": "user-123",
"x-aether-auth-api-key-id": "key-123",
"x-aether-auth-balance-remaining": "42.5",
"x-aether-auth-access-allowed": "true",
}
mock_request.client = SimpleNamespace(host="127.0.0.1")
mock_request.url.path = "/v1/chat/completions"
mock_request.state = MagicMock()
db_user = MagicMock()
db_user.id = "user-123"
db_user.is_active = True
db_user.is_deleted = False
db_api_key = MagicMock()
db_api_key.id = "key-123"
db_api_key.user_id = "user-123"
db_api_key.is_active = True
db_api_key.is_locked = False
db_api_key.is_standalone = False
db_api_key.expires_at = None
mock_db = MagicMock()
user_query = MagicMock()
user_query.filter.return_value.first.return_value = db_user
api_key_query = MagicMock()
api_key_query.filter.return_value.first.return_value = db_api_key
mock_db.query.side_effect = [user_query, api_key_query]
mock_adapter = MagicMock()
mock_adapter.extract_api_key.side_effect = AssertionError(
"trusted auth should short-circuit"
)
with patch.object(
pipeline.auth_service,
"authenticate_api_key_threadsafe",
new_callable=AsyncMock,
side_effect=AssertionError("threadsafe auth should not be called"),
):
user, api_key = await pipeline._authenticate_client(mock_request, mock_db, mock_adapter)
assert user == db_user
assert api_key == db_api_key
assert mock_request.state.user_id == "user-123"
assert mock_request.state.api_key_id == "key-123"
assert mock_request.state.prefetched_balance_remaining == 42.5
assert mock_request.state.gateway_execution_path == "public_proxy_after_executor_miss"
@pytest.mark.asyncio
async def test_authenticate_client_trusted_gateway_balance_denied(
self, pipeline: ApiRequestPipeline
) -> None:
mock_request = MagicMock()
mock_request.headers = {
"x-aether-gateway": "rust-phase3b",
"x-aether-auth-user-id": "user-123",
"x-aether-auth-api-key-id": "key-123",
"x-aether-auth-balance-remaining": "0",
"x-aether-auth-access-allowed": "false",
}
mock_request.client = SimpleNamespace(host="127.0.0.1")
mock_request.url.path = "/v1/chat/completions"
mock_request.state = MagicMock()
db_user = MagicMock()
db_user.id = "user-123"
db_user.is_active = True
db_user.is_deleted = False
db_api_key = MagicMock()
db_api_key.id = "key-123"
db_api_key.user_id = "user-123"
db_api_key.is_active = True
db_api_key.is_locked = False
db_api_key.is_standalone = False
db_api_key.expires_at = None
mock_db = MagicMock()
user_query = MagicMock()
user_query.filter.return_value.first.return_value = db_user
api_key_query = MagicMock()
api_key_query.filter.return_value.first.return_value = db_api_key
mock_db.query.side_effect = [user_query, api_key_query]
mock_adapter = MagicMock()
mock_adapter.extract_api_key.side_effect = AssertionError(
"trusted auth should short-circuit"
)
from src.core.exceptions import BalanceInsufficientException
with pytest.raises(BalanceInsufficientException):
await pipeline._authenticate_client(mock_request, mock_db, mock_adapter)
@pytest.mark.asyncio
async def test_authenticate_client_gateway_marker_without_trusted_ids_falls_back_to_legacy(
self, pipeline: ApiRequestPipeline
) -> None:
mock_user = MagicMock()
mock_user.id = "user-123"
mock_api_key = MagicMock()
mock_api_key.id = "key-123"
mock_request = MagicMock()
mock_request.headers = {"x-aether-gateway": "rust-phase3b"}
mock_request.client = SimpleNamespace(host="127.0.0.1")
mock_request.url.path = "/v1/chat/completions"
mock_request.state = MagicMock()
db_user = MagicMock()
db_user.id = "user-123"
db_user.is_active = True
db_user.is_deleted = False
db_api_key = MagicMock()
db_api_key.id = "key-123"
db_api_key.user_id = "user-123"
db_api_key.is_active = True
db_api_key.is_locked = False
db_api_key.is_standalone = False
db_api_key.expires_at = None
mock_db = MagicMock()
user_query = MagicMock()
user_query.filter.return_value.first.return_value = db_user
api_key_query = MagicMock()
api_key_query.filter.return_value.first.return_value = db_api_key
mock_db.query.side_effect = [user_query, api_key_query]
mock_adapter = MagicMock()
mock_adapter.extract_api_key = MagicMock(return_value="sk-test")
with patch.object(
pipeline.auth_service,
"authenticate_api_key_threadsafe",
new_callable=AsyncMock,
return_value=MagicMock(
user=mock_user,
api_key=mock_api_key,
access_allowed=True,
balance_remaining=12.5,
),
) as mock_auth:
user, api_key = await pipeline._authenticate_client(mock_request, mock_db, mock_adapter)
assert user == db_user
assert api_key == db_api_key
mock_adapter.extract_api_key.assert_called_once_with(mock_request)
mock_auth.assert_awaited_once_with("sk-test")
assert mock_request.state.prefetched_balance_remaining == 12.5
perf: 优化请求鉴权链路并批量化统计/调度查询 - 为 Pipeline/Context 增加按需读取请求体能力,支持 async 懒加载 JSON body - 为 chat/cli/video/claude/openai-cli 适配器关闭默认预读,减少无效 body 读取与超时风险 - 将本地登录、JWT 用户加载、API Key 鉴权迁移到线程池隔离会话执行,避免阻塞事件循环 - 为 API Key 鉴权返回结构化余额结果,并在主请求会话中重新绑定 user/api_key 后再校验状态、过期和锁定信息 - 为 management/user token 前缀认证引入独立会话与结果回绑,避免跨会话对象写入失效 - 为 Usage 余额检查补充结构化返回,统一透出 remaining 与欠费/不可用文案映射 - 为用户与管理端活跃请求查询增加 maintain_status 开关,避免轮询指定 id 时误触发状态修复 - 重写 user_me usage 汇总逻辑,支持 group_by=None 的粗粒度聚合 - 修正 provider 维度成功率与平均响应时间统计,基于 success_count 和成功响应耗时汇总计算 - 前端 Usage 轮询由 setInterval 改为串行 setTimeout,避免并发轮询叠加 - 为 StatsAggregator 增加按本地日期批量计算百分位能力,替代逐天 fan-out 查询 - 为混合统计查询合并连续实时日期区间,并批量读取 StatsDaily,减少逐日查询次数 - 为用户日统计增加批量聚合入口,替代逐用户循环聚合 - 为系统配置导出改用 selectinload 预加载 provider 关联数据,减少 N+1 查询 - 为管理员用户列表增加钱包批量查询,避免逐用户回表 - 为调度器增加 provider 轻量引用预过滤,先按 allowed_providers 缩小范围再加载完整 provider 图 - 为 CandidateBuilder 增加 provider refs/provider_ids 查询能力,保留分页顺序 - 为模型缓存增加 provider_model_mappings 索引缓存与 model_mappings 规则缓存,减少重复全量扫描 - 为请求候选中间态改为 flush/batch commit,降低 pending/streaming 状态切换的事务往返 - 为钱包访问结果补充 balance_snapshot,并抽取余额快照复用逻辑 - 补充 pipeline、auth、admin users、user_me usage、stats aggregator、model cache、 scheduler、wallet、request candidate 等回归与契约测试
2026-03-09 22:57:23 +08:00
class TestPipelineUserRateLimit:
@pytest.fixture
def pipeline(self) -> ApiRequestPipeline:
return ApiRequestPipeline()
@pytest.mark.asyncio
async def test_apply_legacy_request_guards_skips_when_rust_completed_preflight(
self, pipeline: ApiRequestPipeline
) -> None:
request = MagicMock()
request.headers = {
"x-aether-gateway": "rust-phase3b",
"x-aether-rate-limit-preflight": "true",
}
request.client = SimpleNamespace(host="127.0.0.1")
request.state = MagicMock()
db = MagicMock()
user = MagicMock(id="user-1")
api_key = MagicMock(id="key-1")
auth_state = SimpleNamespace(user=user, api_key=api_key, management_token=None)
pipeline._check_user_rate_limit = AsyncMock(
side_effect=AssertionError("trusted rust preflight should skip legacy limiter")
)
await pipeline._apply_legacy_request_guards(
request,
db,
mode=ApiMode.STANDARD,
auth_state=auth_state,
)
@pytest.mark.asyncio
async def test_check_user_rate_limit_uses_system_default_for_user_scope(
self, pipeline: ApiRequestPipeline, monkeypatch: pytest.MonkeyPatch
) -> None:
request = MagicMock()
request.state = MagicMock()
db = MagicMock()
user = MagicMock(id="user-1", rate_limit=None)
api_key = MagicMock(id="key-1", is_standalone=False, rate_limit=0)
limiter = MagicMock()
limiter.get_user_rpm_key.return_value = "rpm:user:user-1:1"
limiter.get_key_rpm_key.return_value = "rpm:key:key-1:1"
limiter.check_and_consume = AsyncMock(return_value=RpmCheckResult(allowed=True))
monkeypatch.setattr(
"src.api.base.pipeline.get_user_rpm_limiter",
AsyncMock(return_value=limiter),
)
monkeypatch.setattr(
"src.api.base.pipeline.SystemConfigService.get_config",
lambda *_a, **_k: 60,
)
await pipeline._check_user_rate_limit(request, db, user, api_key)
limiter.check_and_consume.assert_awaited_once_with(
user_rpm_key="rpm:user:user-1:1",
user_rpm_limit=60,
key_rpm_key="rpm:key:key-1:1",
key_rpm_limit=0,
)
@pytest.mark.asyncio
async def test_check_user_rate_limit_returns_429_with_scope_header(
self, pipeline: ApiRequestPipeline, monkeypatch: pytest.MonkeyPatch
) -> None:
request = MagicMock()
request.state = MagicMock()
db = MagicMock()
user = MagicMock(id="user-1", rate_limit=100)
api_key = MagicMock(id="key-1", is_standalone=False, rate_limit=10)
limiter = MagicMock()
limiter.get_user_rpm_key.return_value = "rpm:user:user-1:1"
limiter.get_key_rpm_key.return_value = "rpm:key:key-1:1"
limiter.get_retry_after.return_value = 17
limiter.check_and_consume = AsyncMock(
return_value=RpmCheckResult(
allowed=False,
scope="key",
limit=10,
remaining=0,
retry_after=17,
)
)
monkeypatch.setattr(
"src.api.base.pipeline.get_user_rpm_limiter",
AsyncMock(return_value=limiter),
)
monkeypatch.setattr(
"src.api.base.pipeline.SystemConfigService.get_config",
lambda *_a, **_k: 60,
)
with pytest.raises(HTTPException) as exc_info:
await pipeline._check_user_rate_limit(request, db, user, api_key)
assert exc_info.value.status_code == 429
assert exc_info.value.headers == {
"Retry-After": "17",
"X-RateLimit-Limit": "10",
"X-RateLimit-Remaining": "0",
"X-RateLimit-Scope": "key",
}
@pytest.mark.asyncio
async def test_check_user_rate_limit_uses_system_default_for_standalone_key(
self, pipeline: ApiRequestPipeline, monkeypatch: pytest.MonkeyPatch
) -> None:
request = MagicMock()
request.state = MagicMock()
db = MagicMock()
user = MagicMock(id="user-1", rate_limit=999)
api_key = MagicMock(id="standalone-1", is_standalone=True, rate_limit=None)
limiter = MagicMock()
limiter.get_standalone_rpm_key.return_value = "rpm:ukey:standalone-1:1"
limiter.get_key_rpm_key.return_value = "rpm:key:standalone-1:1"
limiter.check_and_consume = AsyncMock(return_value=RpmCheckResult(allowed=True))
monkeypatch.setattr(
"src.api.base.pipeline.get_user_rpm_limiter",
AsyncMock(return_value=limiter),
)
monkeypatch.setattr(
"src.api.base.pipeline.SystemConfigService.get_config",
lambda *_a, **_k: 60,
)
await pipeline._check_user_rate_limit(request, db, user, api_key)
limiter.check_and_consume.assert_awaited_once_with(
user_rpm_key="rpm:ukey:standalone-1:1",
user_rpm_limit=60,
key_rpm_key="rpm:key:standalone-1:1",
key_rpm_limit=0,
)
@pytest.mark.asyncio
async def test_check_user_rate_limit_returns_429_with_user_scope_header(
self, pipeline: ApiRequestPipeline, monkeypatch: pytest.MonkeyPatch
) -> None:
request = MagicMock()
request.state = MagicMock()
db = MagicMock()
user = MagicMock(id="user-1", rate_limit=3)
api_key = MagicMock(id="key-1", is_standalone=False, rate_limit=10)
limiter = MagicMock()
limiter.get_user_rpm_key.return_value = "rpm:user:user-1:1"
limiter.get_key_rpm_key.return_value = "rpm:key:key-1:1"
limiter.get_retry_after.return_value = 23
limiter.check_and_consume = AsyncMock(
return_value=RpmCheckResult(
allowed=False,
scope="user",
limit=3,
remaining=0,
retry_after=23,
)
)
monkeypatch.setattr(
"src.api.base.pipeline.get_user_rpm_limiter",
AsyncMock(return_value=limiter),
)
monkeypatch.setattr(
"src.api.base.pipeline.SystemConfigService.get_config",
lambda *_a, **_k: 60,
)
with pytest.raises(HTTPException) as exc_info:
await pipeline._check_user_rate_limit(request, db, user, api_key)
assert exc_info.value.status_code == 429
assert exc_info.value.headers == {
"Retry-After": "23",
"X-RateLimit-Limit": "3",
"X-RateLimit-Remaining": "0",
"X-RateLimit-Scope": "user",
}
perf: 优化请求鉴权链路并批量化统计/调度查询 - 为 Pipeline/Context 增加按需读取请求体能力,支持 async 懒加载 JSON body - 为 chat/cli/video/claude/openai-cli 适配器关闭默认预读,减少无效 body 读取与超时风险 - 将本地登录、JWT 用户加载、API Key 鉴权迁移到线程池隔离会话执行,避免阻塞事件循环 - 为 API Key 鉴权返回结构化余额结果,并在主请求会话中重新绑定 user/api_key 后再校验状态、过期和锁定信息 - 为 management/user token 前缀认证引入独立会话与结果回绑,避免跨会话对象写入失效 - 为 Usage 余额检查补充结构化返回,统一透出 remaining 与欠费/不可用文案映射 - 为用户与管理端活跃请求查询增加 maintain_status 开关,避免轮询指定 id 时误触发状态修复 - 重写 user_me usage 汇总逻辑,支持 group_by=None 的粗粒度聚合 - 修正 provider 维度成功率与平均响应时间统计,基于 success_count 和成功响应耗时汇总计算 - 前端 Usage 轮询由 setInterval 改为串行 setTimeout,避免并发轮询叠加 - 为 StatsAggregator 增加按本地日期批量计算百分位能力,替代逐天 fan-out 查询 - 为混合统计查询合并连续实时日期区间,并批量读取 StatsDaily,减少逐日查询次数 - 为用户日统计增加批量聚合入口,替代逐用户循环聚合 - 为系统配置导出改用 selectinload 预加载 provider 关联数据,减少 N+1 查询 - 为管理员用户列表增加钱包批量查询,避免逐用户回表 - 为调度器增加 provider 轻量引用预过滤,先按 allowed_providers 缩小范围再加载完整 provider 图 - 为 CandidateBuilder 增加 provider refs/provider_ids 查询能力,保留分页顺序 - 为模型缓存增加 provider_model_mappings 索引缓存与 model_mappings 规则缓存,减少重复全量扫描 - 为请求候选中间态改为 flush/batch commit,降低 pending/streaming 状态切换的事务往返 - 为钱包访问结果补充 balance_snapshot,并抽取余额快照复用逻辑 - 补充 pipeline、auth、admin users、user_me usage、stats aggregator、model cache、 scheduler、wallet、request candidate 等回归与契约测试
2026-03-09 22:57:23 +08:00
class TestPipelineTokenPrefixAuth:
"""Tests token-prefix auth isolation."""
@pytest.fixture
def pipeline(self) -> ApiRequestPipeline:
return ApiRequestPipeline()
@pytest.mark.asyncio
async def test_try_token_prefix_auth_uses_isolated_session(
self, pipeline: ApiRequestPipeline
) -> None:
mock_request = MagicMock()
mock_request.headers = {}
mock_request.client = MagicMock(host="127.0.0.1")
route_db = MagicMock()
auth_db = MagicMock()
mock_user = MagicMock()
mock_token = MagicMock()
async def authenticate(db: Any, token: str, client_ip: str) -> tuple[Any, Any]:
assert db is auth_db
assert token == "ae_test"
assert client_ip == "127.0.0.1"
return mock_user, mock_token
with patch("src.api.base.pipeline.create_session", return_value=auth_db):
with patch("src.utils.request_utils.get_client_ip", return_value="127.0.0.1"):
with patch("src.core.modules.hooks.get_hook_dispatcher") as mock_get_dispatcher:
dispatcher = MagicMock()
dispatcher.dispatch = AsyncMock(
return_value=[
{
"prefix": "ae_",
"module": "management_tokens",
"authenticate": authenticate,
}
]
)
mock_get_dispatcher.return_value = dispatcher
result = await pipeline._try_token_prefix_auth(
"ae_test", mock_request, route_db
)
assert result == (mock_user, mock_token)
dispatcher.dispatch.assert_awaited_once_with(AUTH_TOKEN_PREFIX_AUTHENTICATORS)
auth_db.expunge.assert_any_call(mock_user)
auth_db.expunge.assert_any_call(mock_token)
auth_db.close.assert_called_once()
class TestPipelineAdminAuth:
"""测试管理员认证"""
@pytest.fixture
def pipeline(self) -> ApiRequestPipeline:
return ApiRequestPipeline()
@pytest.mark.asyncio
async def test_authenticate_admin_missing_token(self, pipeline: ApiRequestPipeline) -> None:
"""测试缺少管理员令牌"""
mock_request = MagicMock()
mock_request.headers = {}
mock_db = MagicMock()
with pytest.raises(HTTPException) as exc_info:
await pipeline._authenticate_admin(mock_request, mock_db)
assert exc_info.value.status_code == 401
assert "管理员凭证" in exc_info.value.detail
@pytest.mark.asyncio
async def test_authenticate_admin_invalid_token(self, pipeline: ApiRequestPipeline) -> None:
"""测试无效的管理员令牌"""
mock_request = MagicMock()
mock_request.headers = {"authorization": "Bearer invalid-token"}
mock_db = MagicMock()
with patch.object(
pipeline.auth_service,
"verify_token",
side_effect=HTTPException(status_code=401, detail="Invalid token"),
):
with pytest.raises(HTTPException) as exc_info:
await pipeline._authenticate_admin(mock_request, mock_db)
assert exc_info.value.status_code == 401
@pytest.mark.asyncio
async def test_authenticate_admin_success(self, pipeline: ApiRequestPipeline) -> None:
"""测试管理员认证成功"""
created_at = datetime.now(timezone.utc)
mock_session = MagicMock()
mock_session.id = "session-123"
mock_user = MagicMock()
mock_user.id = "admin-123"
mock_user.is_active = True
mock_user.is_deleted = False
mock_user.role = UserRole.ADMIN
mock_user.email = "admin@example.com"
mock_user.created_at = created_at
mock_request = MagicMock()
mock_request.headers = {
"authorization": "Bearer valid-token",
"X-Client-Device-Id": "device-admin-123",
}
mock_request.state = MagicMock()
mock_db = MagicMock()
mock_db.query.return_value.filter.return_value.first.return_value = mock_user
with (
patch.object(
pipeline.auth_service,
"verify_token",
new_callable=AsyncMock,
return_value={
"user_id": "admin-123",
"created_at": created_at.isoformat(),
"session_id": "session-123",
},
),
patch(
"src.api.base.pipeline.SessionService.get_active_session",
return_value=mock_session,
),
patch("src.api.base.pipeline.SessionService.touch_session", return_value=True),
patch("src.api.base.pipeline.SessionService.assert_session_device_matches"),
):
user, management_token = await pipeline._authenticate_admin(mock_request, mock_db)
assert user == mock_user
assert management_token is None
assert mock_request.state.user_id == "admin-123"
assert mock_request.state.user_session_id == "session-123"
mock_db.commit.assert_called_once()
@pytest.mark.asyncio
async def test_authenticate_admin_lowercase_bearer(self, pipeline: ApiRequestPipeline) -> None:
"""测试 bearer (小写) 前缀也能正确解析"""
created_at = datetime.now(timezone.utc)
mock_session = MagicMock()
mock_session.id = "session-123"
mock_user = MagicMock()
mock_user.id = "admin-123"
mock_user.is_active = True
mock_user.is_deleted = False
mock_user.role = UserRole.ADMIN
mock_user.email = "admin@example.com"
mock_user.created_at = created_at
mock_request = MagicMock()
mock_request.headers = {
"authorization": "bearer valid-token",
"X-Client-Device-Id": "device-admin-123",
}
mock_request.state = MagicMock()
mock_db = MagicMock()
mock_db.query.return_value.filter.return_value.first.return_value = mock_user
with (
patch.object(
pipeline.auth_service,
"verify_token",
new_callable=AsyncMock,
return_value={
"user_id": "admin-123",
"created_at": created_at.isoformat(),
"session_id": "session-123",
},
) as mock_verify,
patch(
"src.api.base.pipeline.SessionService.get_active_session",
return_value=mock_session,
),
patch("src.api.base.pipeline.SessionService.touch_session", return_value=True),
patch("src.api.base.pipeline.SessionService.assert_session_device_matches"),
):
user, management_token = await pipeline._authenticate_admin(mock_request, mock_db)
mock_verify.assert_awaited_once_with("valid-token", token_type="access")
assert user == mock_user
assert management_token is None
@pytest.mark.asyncio
async def test_authenticate_admin_rejects_legacy_token_without_session_id(
self, pipeline: ApiRequestPipeline
) -> None:
created_at = datetime.now(timezone.utc)
mock_request = MagicMock()
mock_request.headers = {
"authorization": "Bearer valid-token",
"X-Client-Device-Id": "device-admin-legacy",
}
mock_request.state = MagicMock()
mock_user = MagicMock()
mock_user.id = "admin-123"
mock_user.is_active = True
mock_user.is_deleted = False
mock_user.role = UserRole.ADMIN
mock_user.email = "admin@example.com"
mock_user.created_at = created_at
mock_db = MagicMock()
mock_db.query.return_value.filter.return_value.first.return_value = mock_user
with (
patch.object(
pipeline.auth_service,
"verify_token",
new_callable=AsyncMock,
return_value={"user_id": "admin-123", "created_at": created_at.isoformat()},
),
patch.object(
pipeline.auth_service,
"token_identity_matches_user",
return_value=True,
),
):
with pytest.raises(HTTPException, match="登录会话已失效,请重新登录"):
await pipeline._authenticate_admin(mock_request, mock_db)
@pytest.mark.asyncio
async def test_authenticate_admin_rollback_on_session_touch_commit_failure(
self, pipeline: ApiRequestPipeline
) -> None:
created_at = datetime.now(timezone.utc)
mock_session = MagicMock()
mock_session.id = "session-123"
mock_user = MagicMock()
mock_user.id = "admin-123"
mock_user.is_active = True
mock_user.is_deleted = False
mock_user.role = UserRole.ADMIN
mock_user.email = "admin@example.com"
mock_user.created_at = created_at
mock_request = MagicMock()
mock_request.headers = {
"authorization": "Bearer valid-token",
"X-Client-Device-Id": "device-admin-123",
}
mock_request.state = MagicMock()
mock_db = MagicMock()
mock_db.query.return_value.filter.return_value.first.return_value = mock_user
mock_db.commit.side_effect = RuntimeError("lock timeout")
with (
patch.object(
pipeline.auth_service,
"verify_token",
new_callable=AsyncMock,
return_value={
"user_id": "admin-123",
"created_at": created_at.isoformat(),
"session_id": "session-123",
},
),
patch(
"src.api.base.pipeline.SessionService.get_active_session",
return_value=mock_session,
),
patch("src.api.base.pipeline.SessionService.touch_session", return_value=True),
patch("src.api.base.pipeline.SessionService.assert_session_device_matches"),
):
user, management_token = await pipeline._authenticate_admin(mock_request, mock_db)
assert user == mock_user
assert management_token is None
mock_db.rollback.assert_called_once()
@pytest.mark.asyncio
async def test_authenticate_admin_skips_commit_when_session_touch_not_needed(
self, pipeline: ApiRequestPipeline
) -> None:
created_at = datetime.now(timezone.utc)
mock_session = MagicMock()
mock_session.id = "session-123"
mock_user = MagicMock()
mock_user.id = "admin-123"
mock_user.is_active = True
mock_user.is_deleted = False
mock_user.role = UserRole.ADMIN
mock_user.email = "admin@example.com"
mock_user.created_at = created_at
mock_request = MagicMock()
mock_request.headers = {
"authorization": "Bearer valid-token",
"X-Client-Device-Id": "device-admin-123",
}
mock_request.state = MagicMock()
mock_db = MagicMock()
mock_db.query.return_value.filter.return_value.first.return_value = mock_user
with (
patch.object(
pipeline.auth_service,
"verify_token",
new_callable=AsyncMock,
return_value={
"user_id": "admin-123",
"created_at": created_at.isoformat(),
"session_id": "session-123",
},
),
patch(
"src.api.base.pipeline.SessionService.get_active_session",
return_value=mock_session,
),
patch(
"src.api.base.pipeline.SessionService.touch_session",
return_value=False,
),
patch("src.api.base.pipeline.SessionService.assert_session_device_matches"),
):
user, management_token = await pipeline._authenticate_admin(mock_request, mock_db)
assert user == mock_user
assert management_token is None
mock_db.commit.assert_not_called()
@pytest.mark.asyncio
async def test_authenticate_admin_uses_identity_helper_for_session_touch(
self, pipeline: ApiRequestPipeline
) -> None:
created_at = datetime.now(timezone.utc)
mock_session = MagicMock()
mock_session.id = "session-123"
mock_user = MagicMock()
mock_user.id = "admin-123"
mock_user.is_active = True
mock_user.is_deleted = False
mock_user.role = UserRole.ADMIN
mock_user.email = "admin@example.com"
mock_user.created_at = created_at
mock_request = MagicMock()
mock_request.headers = {
"authorization": "Bearer valid-token",
"X-Client-Device-Id": "device-admin-123",
}
mock_request.state = MagicMock()
mock_db = MagicMock()
mock_db.query.return_value.filter.return_value.first.return_value = mock_user
with (
patch.object(
pipeline.auth_service,
"verify_token",
new_callable=AsyncMock,
return_value={
"user_id": "admin-123",
"created_at": created_at.isoformat(),
"session_id": "session-123",
},
),
patch(
"src.api.base.pipeline.get_request_identity_metadata",
return_value=MagicMock(client_ip="203.0.113.10", user_agent="admin-agent/1.0"),
),
patch(
"src.api.base.pipeline.SessionService.get_active_session",
return_value=mock_session,
),
patch(
"src.api.base.pipeline.SessionService.touch_session",
return_value=True,
) as mock_touch,
patch("src.api.base.pipeline.SessionService.assert_session_device_matches"),
):
await pipeline._authenticate_admin(mock_request, mock_db)
mock_touch.assert_called_once_with(
mock_session,
client_ip="203.0.113.10",
user_agent="admin-agent/1.0",
)
class TestPipelineUserAuth:
"""测试普通用户 JWT 认证"""
@pytest.fixture
def pipeline(self) -> ApiRequestPipeline:
return ApiRequestPipeline()
@pytest.mark.asyncio
async def test_authenticate_user_lowercase_bearer(self, pipeline: ApiRequestPipeline) -> None:
"""测试 bearer (小写) 前缀也能正确解析"""
created_at = datetime.now(timezone.utc)
mock_session = MagicMock()
mock_session.id = "session-456"
mock_user = MagicMock()
mock_user.id = "user-123"
mock_user.is_active = True
mock_user.is_deleted = False
mock_user.email = "user@example.com"
mock_user.created_at = created_at
mock_request = MagicMock()
mock_request.headers = {
"authorization": "bearer valid-token",
"X-Client-Device-Id": "device-user-456",
}
mock_request.state = MagicMock()
mock_db = MagicMock()
mock_db.query.return_value.filter.return_value.first.return_value = mock_user
with (
patch.object(
pipeline.auth_service,
"verify_token",
new_callable=AsyncMock,
return_value={
"user_id": "user-123",
"created_at": created_at.isoformat(),
"session_id": "session-456",
},
) as mock_verify,
patch(
"src.api.base.pipeline.SessionService.get_active_session",
return_value=mock_session,
),
patch("src.api.base.pipeline.SessionService.touch_session", return_value=True),
patch("src.api.base.pipeline.SessionService.assert_session_device_matches"),
):
user, management_token = await pipeline._authenticate_user(mock_request, mock_db)
mock_verify.assert_awaited_once_with("valid-token", token_type="access")
assert user == mock_user
assert management_token is None
assert mock_request.state.user_session_id == "session-456"
mock_db.commit.assert_called_once()
@pytest.mark.asyncio
async def test_authenticate_user_rejects_legacy_token_without_session_id(
self, pipeline: ApiRequestPipeline
) -> None:
"""历史 JWT 无 session_id 时应拒绝。"""
created_at = datetime.now(timezone.utc)
mock_request = MagicMock()
mock_request.headers = {
"authorization": "Bearer valid-token",
"X-Client-Device-Id": "device-user-legacy",
}
mock_request.state = MagicMock()
mock_user = MagicMock()
mock_user.id = "user-123"
mock_user.is_active = True
mock_user.is_deleted = False
mock_db = MagicMock()
mock_db.query.return_value.filter.return_value.first.return_value = mock_user
with (
patch.object(
pipeline.auth_service,
"verify_token",
new_callable=AsyncMock,
return_value={"user_id": "user-123", "created_at": created_at.isoformat()},
),
patch.object(
pipeline.auth_service,
"token_identity_matches_user",
return_value=True,
),
):
with pytest.raises(HTTPException, match="登录会话已失效,请重新登录"):
await pipeline._authenticate_user(mock_request, mock_db)
@pytest.mark.asyncio
async def test_authenticate_user_requires_device_id(self, pipeline: ApiRequestPipeline) -> None:
created_at = datetime.now(timezone.utc)
mock_request = MagicMock()
mock_request.headers = {"authorization": "Bearer valid-token"}
mock_request.query_params = {}
mock_request.state = MagicMock()
mock_user = MagicMock()
mock_user.id = "user-123"
mock_user.is_active = True
mock_user.is_deleted = False
mock_user.created_at = created_at
mock_session = MagicMock()
mock_session.id = "session-456"
mock_db = MagicMock()
mock_db.query.return_value.filter.return_value.first.return_value = mock_user
with (
patch.object(
pipeline.auth_service,
"verify_token",
new_callable=AsyncMock,
return_value={
"user_id": "user-123",
"created_at": created_at.isoformat(),
"session_id": "session-456",
},
),
patch(
"src.api.base.pipeline.SessionService.get_active_session",
return_value=mock_session,
),
patch.object(
pipeline.auth_service,
"token_identity_matches_user",
return_value=True,
),
):
with pytest.raises(HTTPException, match="缺少或无效的设备标识"):
await pipeline._authenticate_user(mock_request, mock_db)
@pytest.mark.asyncio
async def test_authenticate_user_skips_commit_when_session_touch_not_needed(
self, pipeline: ApiRequestPipeline
) -> None:
created_at = datetime.now(timezone.utc)
mock_session = MagicMock()
mock_session.id = "session-456"
mock_user = MagicMock()
mock_user.id = "user-123"
mock_user.is_active = True
mock_user.is_deleted = False
mock_user.email = "user@example.com"
mock_user.created_at = created_at
mock_request = MagicMock()
mock_request.headers = {
"authorization": "Bearer valid-token",
"X-Client-Device-Id": "device-user-456",
}
mock_request.state = MagicMock()
mock_db = MagicMock()
mock_db.query.return_value.filter.return_value.first.return_value = mock_user
with (
patch.object(
pipeline.auth_service,
"verify_token",
new_callable=AsyncMock,
return_value={
"user_id": "user-123",
"created_at": created_at.isoformat(),
"session_id": "session-456",
},
),
patch(
"src.api.base.pipeline.SessionService.get_active_session",
return_value=mock_session,
),
patch(
"src.api.base.pipeline.SessionService.touch_session",
return_value=False,
),
patch("src.api.base.pipeline.SessionService.assert_session_device_matches"),
):
user, management_token = await pipeline._authenticate_user(mock_request, mock_db)
assert user == mock_user
assert management_token is None
mock_db.commit.assert_not_called()
@pytest.mark.asyncio
async def test_authenticate_user_uses_identity_helper_for_session_touch(
self, pipeline: ApiRequestPipeline
) -> None:
created_at = datetime.now(timezone.utc)
mock_session = MagicMock()
mock_session.id = "session-456"
mock_user = MagicMock()
mock_user.id = "user-123"
mock_user.is_active = True
mock_user.is_deleted = False
mock_user.email = "user@example.com"
mock_user.created_at = created_at
mock_request = MagicMock()
mock_request.headers = {
"authorization": "Bearer valid-token",
"X-Client-Device-Id": "device-user-456",
}
mock_request.state = MagicMock()
mock_db = MagicMock()
mock_db.query.return_value.filter.return_value.first.return_value = mock_user
with (
patch.object(
pipeline.auth_service,
"verify_token",
new_callable=AsyncMock,
return_value={
"user_id": "user-123",
"created_at": created_at.isoformat(),
"session_id": "session-456",
},
),
patch(
"src.api.base.pipeline.get_request_identity_metadata",
return_value=MagicMock(client_ip="198.51.100.25", user_agent="user-agent/2.0"),
),
patch(
"src.api.base.pipeline.SessionService.get_active_session",
return_value=mock_session,
),
patch(
"src.api.base.pipeline.SessionService.touch_session",
return_value=True,
) as mock_touch,
patch("src.api.base.pipeline.SessionService.assert_session_device_matches"),
):
await pipeline._authenticate_user(mock_request, mock_db)
mock_touch.assert_called_once_with(
mock_session,
client_ip="198.51.100.25",
user_agent="user-agent/2.0",
)