fix(txn+security): 仓储改 flush + 启动校验/兜底 + job.error 脱敏 + SSE 异常硬化

P0-1 SqlCredentialStore/save_draft 由自提交改 flush,端点/服务统一 commit
  (新增 CredentialStore.commit() 统一提交点;token 刷新落库显式提交);
  补多凭据一请求中途失败整体回滚集成测试。
P0-2 启动校验 _fernet(enc_key) 快速失败 + catch-all Exception → ErrorEnvelope;
  credential_enc_key 改 SecretStr。
P0-3 run_job 异常分类:AppError 存 code+message,其余存通用文案不泄 str(exc)。
P0-4 评审/正文 SSE 失败先发 error 事件,尾部 commit 包 try/except。
P1-4 max_version 加 FOR UPDATE 行锁消除 TOCTOU。
P1-5 scan_overdue 谓词下推 + 批量 UPDATE RETURNING。
P1-10 移除 OAuth user_code 日志。
P2 provider_deps 改调网关 build_adapter;accept_service Committable Protocol;
  CORS 白名单收窄;request_id 安全字符集白名单;stdlib 日志接管;读端点 404 校验;
  httpx timeout;测试用合法 Fernet key;类型化响应模型(JobResponse/DimensionEntry/
  ReviewConflictView/selling_points)+路由 ErrorEnvelope responses(供 codegen)。
This commit is contained in:
Yaojia Wang
2026-06-21 19:32:24 +02:00
parent 2282d4fd24
commit 345cc73965
37 changed files with 737 additions and 115 deletions

View File

@@ -0,0 +1,138 @@
"""P0-1 原子性:`PUT /settings/providers` 多凭据一请求,中途失败整体回滚。
不变量:仓储写方法只 `flush()`,端点末尾**统一一次** `commit()`。若某条 upsert 中途
抛错,端点绝不到达 `commit()` → 请求 session 回滚DB 不留半更新(无部分提交)。
用可注入 fakestore 在第 N 次 upsert 抛错fake session 记录 `commit()` 调用次数。
断言:① 请求返回 500兜底信封② `session.commit()` **从未被调用**(无半提交)。
"""
from __future__ import annotations
import os
import uuid
from typing import Any
import httpx
import pytest
from cryptography.fernet import Fernet
from fastapi import FastAPI
from ww_api.services.credentials import StoredRouting
class _ExplodingStore:
"""凭据 store fake前 N 次 upsert 成功,第 fail_at 次1-based抛错。
模拟「多凭据一请求」里第 K 条写入失败——验证端点不会半提交。
"""
def __init__(self, *, fail_at: int) -> None:
self._fail_at = fail_at
self._upsert_calls = 0
self.creds: dict[tuple[uuid.UUID, str], bytes] = {}
async def list_credentials(self, owner_id: uuid.UUID) -> list[Any]:
return []
async def list_routing(self) -> list[Any]:
return []
async def get_credential(self, owner_id: uuid.UUID, provider: str) -> Any:
return None
async def upsert_credential(
self, owner_id: uuid.UUID, provider: str, api_key_enc: bytes
) -> None:
self._upsert_calls += 1
if self._upsert_calls == self._fail_at:
raise RuntimeError("simulated mid-request write failure")
self.creds[(owner_id, provider)] = api_key_enc
async def upsert_oauth_credential(
self, owner_id: uuid.UUID, provider: str, oauth_enc: bytes
) -> None: # pragma: no cover - 不用于本测试
raise NotImplementedError
async def delete_credential(self, owner_id: uuid.UUID, provider: str) -> bool:
return False
async def upsert_routing(self, routing: StoredRouting) -> None:
self.creds[(uuid.UUID(int=0), routing.tier)] = b""
class _RecordingSession:
"""fake AsyncSession只记 commit/rollback 调用次数(端点统一提交的探针)。"""
def __init__(self) -> None:
self.commits = 0
self.rollbacks = 0
async def commit(self) -> None:
self.commits += 1
async def rollback(self) -> None:
self.rollbacks += 1
def _build_app(store: _ExplodingStore, session: _RecordingSession) -> FastAPI:
os.environ.setdefault("CREDENTIAL_ENC_KEY", Fernet.generate_key().decode())
from ww_config import get_settings
get_settings.cache_clear()
from ww_api.main import create_app
from ww_api.services.provider_deps import get_credential_store
from ww_db import get_session
app = create_app()
app.dependency_overrides[get_credential_store] = lambda: store
app.dependency_overrides[get_session] = lambda: session
return app
@pytest.mark.asyncio
async def test_upsert_providers_rolls_back_on_mid_request_failure() -> None:
# Arrange第 2 条凭据写入时抛错(前一条已 flush 到 session但尚未 commit
store = _ExplodingStore(fail_at=2)
session = _RecordingSession()
app = _build_app(store, session)
payload = {
"credentials": [
{"provider": "deepseek", "api_key": "sk-a"},
{"provider": "kimi", "api_key": "sk-b"},
{"provider": "qwen", "api_key": "sk-c"},
]
}
transport = httpx.ASGITransport(app=app, raise_app_exceptions=False)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
resp = await client.put("/settings/providers", json=payload)
# Assert兜底信封catch-all → 500 INTERNAL不回显原始异常且**从未提交**(无半提交)。
assert resp.status_code == 500
assert resp.json()["error"]["code"] == "INTERNAL"
assert "simulated mid-request write failure" not in resp.text
assert session.commits == 0
@pytest.mark.asyncio
async def test_upsert_providers_commits_once_on_success() -> None:
# Arrange全部成功fail_at 远大于条数)。
store = _ExplodingStore(fail_at=99)
session = _RecordingSession()
app = _build_app(store, session)
payload = {
"credentials": [
{"provider": "deepseek", "api_key": "sk-a"},
{"provider": "kimi", "api_key": "sk-b"},
]
}
transport = httpx.ASGITransport(app=app, raise_app_exceptions=False)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
resp = await client.put("/settings/providers", json=payload)
# Assert成功路径**恰好一次** commit所有写入统一提交
assert resp.status_code == 200
assert session.commits == 1