fix: close platform-check silent-success loophole — raise WebsiteError on None list, missing account, or stale platform after update
This commit is contained in:
@@ -48,7 +48,7 @@ from app.schemas.website import (
|
|||||||
SyncUpstreamModelsResponse,
|
SyncUpstreamModelsResponse,
|
||||||
)
|
)
|
||||||
|
|
||||||
from app.services.website_client import Sub2ApiWebsiteClient, _extract_id
|
from app.services.website_client import Sub2ApiWebsiteClient, WebsiteError, _extract_id
|
||||||
from app.services.website_sync import (
|
from app.services.website_sync import (
|
||||||
binding_sources,
|
binding_sources,
|
||||||
sync_binding,
|
sync_binding,
|
||||||
@@ -1826,9 +1826,13 @@ def organize_website_groups(
|
|||||||
remote_acc["group_ids"] = new_group_ids
|
remote_acc["group_ids"] = new_group_ids
|
||||||
if "platform" in update_payload:
|
if "platform" in update_payload:
|
||||||
# 主动核对防线:防止子站忽略 platform 修改而发生静默失败
|
# 主动核对防线:防止子站忽略 platform 修改而发生静默失败
|
||||||
fresh_accs = c.list_accounts() or []
|
fresh_accs = c.list_accounts()
|
||||||
|
if fresh_accs is None:
|
||||||
|
raise WebsiteError("复查失败:拉取远端账号列表返回空,无法核对平台修改是否生效")
|
||||||
fresh_acc = next((a for a in fresh_accs if str(c.extract_id(a)) == str(old_account_id)), None)
|
fresh_acc = next((a for a in fresh_accs if str(c.extract_id(a)) == str(old_account_id)), None)
|
||||||
if fresh_acc:
|
if not fresh_acc:
|
||||||
|
raise WebsiteError(f"复查失败:在远端账号列表中未找到要更新的账号 {old_account_id}")
|
||||||
|
|
||||||
remote_account_map[str(old_account_id)] = fresh_acc
|
remote_account_map[str(old_account_id)] = fresh_acc
|
||||||
new_platform = fresh_acc.get("platform")
|
new_platform = fresh_acc.get("platform")
|
||||||
if str(new_platform).lower() != str(platform).lower():
|
if str(new_platform).lower() != str(platform).lower():
|
||||||
|
|||||||
@@ -582,3 +582,124 @@ def test_organize_groups_corrects_mismatched_platform_instead_of_recreating(db_s
|
|||||||
assert updated_accounts[0][0] == "ACC-G1"
|
assert updated_accounts[0][0] == "ACC-G1"
|
||||||
assert updated_accounts[0][1]["platform"] == "anthropic"
|
assert updated_accounts[0][1]["platform"] == "anthropic"
|
||||||
assert "平台从 openai 修正为 anthropic" in response.items[0].message
|
assert "平台从 openai 修正为 anthropic" in response.items[0].message
|
||||||
|
|
||||||
|
|
||||||
|
def _seed_platform_mismatch(db_session, monkeypatch, list_accounts_fn):
|
||||||
|
"""公用工厂:搭建平台错配场景并注入可定制的 list_accounts。"""
|
||||||
|
from app.models.snapshot import UpstreamRateSnapshot
|
||||||
|
|
||||||
|
w = Website(
|
||||||
|
name="W-PlatChk",
|
||||||
|
site_type="sub2api",
|
||||||
|
base_url="http://wp",
|
||||||
|
enabled=True,
|
||||||
|
auth_config_json="{}",
|
||||||
|
timeout_seconds=30,
|
||||||
|
)
|
||||||
|
u1 = Upstream(name="U-PlatChk", base_url="http://up")
|
||||||
|
db_session.add_all([w, u1])
|
||||||
|
db_session.commit()
|
||||||
|
db_session.refresh(w)
|
||||||
|
db_session.refresh(u1)
|
||||||
|
|
||||||
|
b1 = WebsiteGroupBinding(
|
||||||
|
website_id=w.id,
|
||||||
|
target_group_id="TG1",
|
||||||
|
target_group_name="TG1-Group",
|
||||||
|
source_groups_json=json.dumps([{"upstream_id": u1.id, "group_id": "G1"}]),
|
||||||
|
enabled=True,
|
||||||
|
)
|
||||||
|
db_session.add(b1)
|
||||||
|
|
||||||
|
k1 = UpstreamGeneratedKey(
|
||||||
|
upstream_id=u1.id,
|
||||||
|
group_id="G1",
|
||||||
|
group_name="G1-Group",
|
||||||
|
key_name="Key-G1",
|
||||||
|
key_value="sk-g1-secret",
|
||||||
|
status="imported",
|
||||||
|
imported_website_id=w.id,
|
||||||
|
imported_account_id="ACC-G1",
|
||||||
|
imported_target_group_id="TG1",
|
||||||
|
)
|
||||||
|
db_session.add(k1)
|
||||||
|
|
||||||
|
snapshot = UpstreamRateSnapshot(
|
||||||
|
upstream_id=u1.id,
|
||||||
|
snapshot_json=json.dumps({
|
||||||
|
"groups": {
|
||||||
|
"G1": {"group_name": "G1-Group", "rate": 0.1, "platform": "anthropic"}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
db_session.add(snapshot)
|
||||||
|
db_session.commit()
|
||||||
|
|
||||||
|
call_count = {"n": 0}
|
||||||
|
|
||||||
|
class MockClient:
|
||||||
|
def __init__(self, **kwargs): pass
|
||||||
|
def __enter__(self): return self
|
||||||
|
def __exit__(self, *a): pass
|
||||||
|
def get_groups(self, *a, **kw):
|
||||||
|
return [{"id": "TG1", "name": "TG1-Group"}]
|
||||||
|
def list_accounts(self):
|
||||||
|
call_count["n"] += 1
|
||||||
|
return list_accounts_fn(call_count["n"])
|
||||||
|
def extract_id(self, val):
|
||||||
|
return val.get("id") if isinstance(val, dict) else str(val)
|
||||||
|
def update_account(self, account_id, body):
|
||||||
|
return {"id": account_id}
|
||||||
|
|
||||||
|
monkeypatch.setattr("app.routers.websites.Sub2ApiWebsiteClient", MockClient)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"app.routers.websites.sync_account_priorities_for_website", lambda db, wid: []
|
||||||
|
)
|
||||||
|
return w
|
||||||
|
|
||||||
|
|
||||||
|
def test_organize_groups_platform_update_remote_silently_ignores(db_session, monkeypatch):
|
||||||
|
"""复查时远端仍返回旧平台值 → 必须报错,不能静默认为成功。"""
|
||||||
|
|
||||||
|
def list_accounts_fn(call_n):
|
||||||
|
# 第 1 次初始列表;第 2 次复查:仍是 openai(忽略了 platform 写入)
|
||||||
|
return [{"id": "ACC-G1", "name": "SmartUp-G1", "group_ids": ["TG1"], "platform": "openai"}]
|
||||||
|
|
||||||
|
w = _seed_platform_mismatch(db_session, monkeypatch, list_accounts_fn)
|
||||||
|
response = organize_website_groups(wid=w.id, db=db_session)
|
||||||
|
|
||||||
|
failed_items = [item for item in response.items if item.status == "failed"]
|
||||||
|
assert len(failed_items) == 1, f"期望 1 个 failed 项,实际:{response.items}"
|
||||||
|
assert "不支持修改平台属性" in failed_items[0].message
|
||||||
|
|
||||||
|
|
||||||
|
def test_organize_groups_platform_update_list_returns_none(db_session, monkeypatch):
|
||||||
|
"""复查时 list_accounts() 返回 None → 必须报错,不能静默成功。"""
|
||||||
|
|
||||||
|
def list_accounts_fn(call_n):
|
||||||
|
if call_n == 1:
|
||||||
|
return [{"id": "ACC-G1", "name": "SmartUp-G1", "group_ids": ["TG1"], "platform": "openai"}]
|
||||||
|
return None # 复查时列表拉取失败
|
||||||
|
|
||||||
|
w = _seed_platform_mismatch(db_session, monkeypatch, list_accounts_fn)
|
||||||
|
response = organize_website_groups(wid=w.id, db=db_session)
|
||||||
|
|
||||||
|
failed_items = [item for item in response.items if item.status == "failed"]
|
||||||
|
assert len(failed_items) == 1, f"期望 1 个 failed 项,实际:{response.items}"
|
||||||
|
assert "复查失败" in failed_items[0].message
|
||||||
|
|
||||||
|
|
||||||
|
def test_organize_groups_platform_update_account_missing_from_list(db_session, monkeypatch):
|
||||||
|
"""复查时账号从列表中消失 → 必须报错,不能静默成功。"""
|
||||||
|
|
||||||
|
def list_accounts_fn(call_n):
|
||||||
|
if call_n == 1:
|
||||||
|
return [{"id": "ACC-G1", "name": "SmartUp-G1", "group_ids": ["TG1"], "platform": "openai"}]
|
||||||
|
return [] # 复查时账号消失
|
||||||
|
|
||||||
|
w = _seed_platform_mismatch(db_session, monkeypatch, list_accounts_fn)
|
||||||
|
response = organize_website_groups(wid=w.id, db=db_session)
|
||||||
|
|
||||||
|
failed_items = [item for item in response.items if item.status == "failed"]
|
||||||
|
assert len(failed_items) == 1, f"期望 1 个 failed 项,实际:{response.items}"
|
||||||
|
assert "复查失败" in failed_items[0].message
|
||||||
|
|||||||
Reference in New Issue
Block a user