Files
SmartUp/backend/test_upstream_key_account_import.py
T
liujing df7e400e7b feat: track imported accounts per upstream key across platforms
Add upstream_key_account_links table mapping generated keys to remote
accounts per website/platform, surface imported_accounts on key
responses, and update website sync/routers to manage the links.
2026-07-23 17:52:58 +08:00

666 lines
23 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import json
import sys
from pathlib import Path
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.pool import StaticPool
sys.path.insert(0, str(Path(__file__).resolve().parent))
from app.database import Base
from app.models.upstream import Upstream
from app.models.upstream_key import UpstreamGeneratedKey
from app.models.website import Website
from app.routers import websites as websites_router
from app.schemas.website import ImportAccountsRequest
@pytest.fixture()
def db_session():
engine = create_engine(
"sqlite://",
connect_args={"check_same_thread": False},
poolclass=StaticPool,
)
Base.metadata.create_all(bind=engine)
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
db = TestingSessionLocal()
try:
yield db
finally:
db.close()
Base.metadata.drop_all(bind=engine)
def seed_account_import_rows(db_session):
website = Website(
name="My Sub2API",
site_type="sub2api",
base_url="http://sub2api.local",
api_prefix="/api/v1/admin",
auth_type="api_key",
auth_config_json=json.dumps({"key": "admin-key", "header": "x-api-key"}),
groups_endpoint="/groups",
group_update_endpoint="/groups/{id}",
)
upstream = Upstream(
name="Packy",
base_url="http://packy.local",
api_prefix="/api/v1",
auth_type="login_password",
auth_config_json="{}",
)
db_session.add_all([website, upstream])
db_session.commit()
db_session.refresh(website)
db_session.refresh(upstream)
generated = UpstreamGeneratedKey(
upstream_id=upstream.id,
group_id="vip",
group_name="VIP",
key_id="up-key-id",
key_name="SmartUp-VIP",
key_value="sk-upstream-generated",
masked_key="sk-u...ated",
raw_json="{}",
status="created",
)
db_session.add(generated)
db_session.commit()
db_session.refresh(generated)
return website, generated
def test_import_upstream_key_creates_sub2api_account_management_apikey(monkeypatch, db_session):
website, generated = seed_account_import_rows(db_session)
account_bodies = []
class FakeWebsiteClient:
def __init__(self, **kwargs):
self.kwargs = kwargs
def __enter__(self):
return self
def __exit__(self, exc_type, exc, tb):
return False
def get_groups(self, endpoint="/groups"):
return [{"id": "7", "name": "Target", "platform": "anthropic"}]
def list_accounts(self):
return [{"id": "101", "name": "Existing", "platform": "anthropic", "group_ids": [7]}]
def create_account(self, body, endpoint="/accounts"):
account_bodies.append((endpoint, body))
return {"id": 101, "name": body["name"]}
def account_exists(self, account_id):
return True
@staticmethod
def extract_id(data):
return str(data.get("id"))
monkeypatch.setattr(websites_router, "Sub2ApiWebsiteClient", FakeWebsiteClient)
response = websites_router.import_upstream_keys_as_accounts(
website.id,
ImportAccountsRequest(
upstream_key_ids=[generated.id],
target_group_map={"vip": "7"},
account_name_prefix="SmartUp",
default_platform="anthropic",
),
db_session,
object(),
)
assert "新建 1" in response.message
assert len(account_bodies) == 1
endpoint, body = account_bodies[0]
assert endpoint == "/accounts"
assert body["type"] == "apikey"
assert body["platform"] == "anthropic"
assert body["credentials"]["api_key"] == "sk-upstream-generated"
assert body["credentials"]["base_url"] == "http://packy.local"
assert body["group_ids"] == [7]
assert body["concurrency"] == 100
assert body["priority"] == 1
assert body["credentials"]["base_url"] == "http://packy.local"
db_session.refresh(generated)
assert generated.status == "imported"
assert generated.imported_website_id == website.id
assert generated.imported_account_id == "101"
def test_import_upstream_key_idempotent_skips_already_imported(monkeypatch, db_session):
"""已导入过的 Key 再次调用不会重复创建。"""
website, generated = seed_account_import_rows(db_session)
generated.imported_website_id = website.id
generated.imported_account_id = "101"
db_session.commit()
create_called = False
class FakeWebsiteClient:
def __init__(self, **kwargs):
self.kwargs = kwargs
def __enter__(self):
return self
def __exit__(self, exc_type, exc, tb):
return False
def get_groups(self, endpoint="/groups"):
return [{"id": "7", "name": "Target", "platform": "anthropic"}]
def list_accounts(self):
return [{"id": "101", "name": "Existing", "platform": "anthropic", "group_ids": [7]}]
def create_account(self, body, endpoint="/accounts"):
nonlocal create_called
create_called = True
return {"id": 999, "name": body["name"]}
def account_exists(self, account_id):
return True # 模拟远端账号仍存在
@staticmethod
def extract_id(data):
return str(data.get("id"))
monkeypatch.setattr(websites_router, "Sub2ApiWebsiteClient", FakeWebsiteClient)
response = websites_router.import_upstream_keys_as_accounts(
website.id,
ImportAccountsRequest(
upstream_key_ids=[generated.id],
target_group_map={"vip": "7"},
account_name_prefix="SmartUp",
default_platform="anthropic",
),
db_session,
object(),
)
# create_account 不应被调用
assert not create_called, "create_account should NOT be called for already imported key"
# 返回 exists
assert len(response.items) == 1
assert response.items[0].status == "exists"
assert response.items[0].account_id == "101"
assert response.items[0].message == "已存在并已对齐目标分组"
# success 应为 true(没有 failed
assert response.success
def test_sync_imported_upstream_keys_efficient_and_correct(monkeypatch, db_session):
"""验证 sync_imported_upstream_keys 优化:只拉一次账号列表,处理 exists 和 stale_cleared。"""
from app.routers import websites as websites_router
from app.schemas.website import SyncImportStatusRequest
website, generated1 = seed_account_import_rows(db_session)
generated1.imported_website_id = website.id
generated1.imported_account_id = "101"
# 增加另一个已导入行以测试单次请求 & 多个状态
generated2 = UpstreamGeneratedKey(
upstream_id=generated1.upstream_id,
group_id="normal",
group_name="normal",
key_name="normal-key",
key_value="sk-normal",
imported_website_id=website.id,
imported_account_id="102",
status="imported",
)
db_session.add(generated2)
db_session.commit()
list_accounts_calls = 0
class FakeClient:
def __init__(self, **kwargs):
pass
def __enter__(self):
return self
def __exit__(self, *a):
return False
def get_groups(self, endpoint="/groups"):
return [{"id": "7", "name": "Target", "platform": "anthropic"}]
def list_accounts(self):
nonlocal list_accounts_calls
list_accounts_calls += 1
# 远端只有 A1 (id: 101),没有 A2 (id: 102)
return [
{"id": "101", "name": "SmartUp-A1", "platform": "anthropic", "group_ids": [7]}
]
@staticmethod
def extract_id(data):
return str(data.get("id"))
monkeypatch.setattr(websites_router, "Sub2ApiWebsiteClient", lambda **kw: FakeClient(**kw))
response = websites_router.sync_imported_upstream_keys(
website.id, SyncImportStatusRequest(upstream_id=generated1.upstream_id),
db_session, object(),
)
# 1. 验证 list_accounts() 只被调用了 1 次
assert list_accounts_calls == 1
# 2. 检查结果
assert len(response.items) == 2
item_101 = next(item for item in response.items if item.account_id == "101")
item_102 = next(item for item in response.items if item.account_id == "102")
# 101 仍存在
assert item_101.status == "exists"
# 102 已被清除
assert item_102.status == "stale_cleared"
# 3. 校验 DB 反映
db_session.refresh(generated1)
db_session.refresh(generated2)
assert generated1.imported_account_id == "101"
assert generated2.imported_account_id is None
def test_sync_imported_upstream_keys_check_failed_on_exception(monkeypatch, db_session):
"""验证 list_accounts 报错或返回 None 时返回 check_failed,且不清空本地导入标记。"""
from app.routers import websites as websites_router
from app.schemas.website import SyncImportStatusRequest
website, generated = seed_account_import_rows(db_session)
generated.imported_website_id = website.id
generated.imported_account_id = "101"
db_session.commit()
class FakeClient:
def __init__(self, **kwargs):
pass
def __enter__(self):
return self
def __exit__(self, *a):
return False
def get_groups(self, endpoint="/groups"):
return [{"id": "7", "name": "Target", "platform": "anthropic"}]
def list_accounts(self):
raise Exception("List accounts failure")
@staticmethod
def extract_id(data):
return str(data.get("id"))
monkeypatch.setattr(websites_router, "Sub2ApiWebsiteClient", lambda **kw: FakeClient(**kw))
response = websites_router.sync_imported_upstream_keys(
website.id, SyncImportStatusRequest(upstream_id=generated.upstream_id),
db_session, object(),
)
assert len(response.items) == 1
assert response.items[0].status == "check_failed"
db_session.refresh(generated)
assert generated.imported_account_id == "101"
def test_import_rebuilds_when_remote_deleted(monkeypatch, db_session):
"""导入接口:远端账号已删除时自动清标记并重新创建。"""
from app.routers import websites as websites_router
from app.schemas.website import ImportAccountsRequest
website, generated = seed_account_import_rows(db_session)
generated.imported_website_id = website.id
generated.imported_account_id = "101"
db_session.commit()
class FakeClient:
def __init__(self, **kwargs):
self.kwargs = kwargs
def __enter__(self):
return self
def __exit__(self, *a):
return False
def get_groups(self, endpoint="/groups"):
return [{"id": "7", "name": "Target", "platform": "anthropic"}]
def list_accounts(self):
return []
def account_exists(self, account_id):
return False
def create_account(self, body, endpoint="/accounts"):
return {"id": 202, "name": body["name"]}
@staticmethod
def extract_id(data):
return str(data.get("id"))
monkeypatch.setattr(websites_router, "Sub2ApiWebsiteClient", lambda **kw: FakeClient(**kw))
response = websites_router.import_upstream_keys_as_accounts(
website.id,
ImportAccountsRequest(upstream_key_ids=[generated.id], target_group_map={"vip": "7"},
account_name_prefix="SmartUp", default_platform="openai"),
db_session, object(),
)
assert len(response.items) == 1
assert response.items[0].status == "created", f"expected created, got {response.items[0].status}"
db_session.refresh(generated)
assert generated.imported_account_id == "202"
def test_import_skips_on_check_failed(monkeypatch, db_session):
"""导入接口:校验失败时保守跳过,不创建也不清除。"""
from app.routers import websites as websites_router
from app.schemas.website import ImportAccountsRequest
website, generated = seed_account_import_rows(db_session)
generated.imported_website_id = website.id
generated.imported_account_id = "101"
db_session.commit()
class FakeClient:
def __init__(self, **kwargs):
self.kwargs = kwargs
def __enter__(self):
return self
def __exit__(self, *a):
return False
def get_groups(self, endpoint="/groups"):
return [{"id": "7", "name": "Target", "platform": "openai"}]
def list_accounts(self):
return None
def account_exists(self, account_id):
return None
def create_account(self, body, endpoint="/accounts"):
raise RuntimeError("should not be called")
@staticmethod
def extract_id(data):
return str(data.get("id"))
monkeypatch.setattr(websites_router, "Sub2ApiWebsiteClient", lambda **kw: FakeClient(**kw))
response = websites_router.import_upstream_keys_as_accounts(
website.id,
ImportAccountsRequest(upstream_key_ids=[generated.id], target_group_map={"vip": "7"},
account_name_prefix="SmartUp", default_platform="openai"),
db_session, object(),
)
assert len(response.items) == 1
assert response.items[0].status == "check_failed"
db_session.refresh(generated)
assert generated.imported_account_id == "101" # 未被清除
assert not response.success
def test_import_upstream_key_with_custom_concurrency_and_priority(monkeypatch, db_session):
website, generated = seed_account_import_rows(db_session)
account_bodies = []
class FakeWebsiteClient:
def __init__(self, **kwargs):
self.kwargs = kwargs
def __enter__(self):
return self
def __exit__(self, exc_type, exc, tb):
return False
def get_groups(self, endpoint="/groups"):
return [{"id": "7", "name": "Target", "platform": "openai"}]
def create_account(self, body, endpoint="/accounts"):
account_bodies.append((endpoint, body))
return {"id": 202, "name": body["name"]}
def account_exists(self, account_id):
return True
@staticmethod
def extract_id(data):
return str(data.get("id"))
monkeypatch.setattr(websites_router, "Sub2ApiWebsiteClient", FakeWebsiteClient)
websites_router.import_upstream_keys_as_accounts(
website.id,
ImportAccountsRequest(
upstream_key_ids=[generated.id],
target_group_map={"vip": "7"},
account_name_prefix="SmartUp",
default_platform="openai",
concurrency=20,
priority=5,
),
db_session,
object(),
)
assert len(account_bodies) == 1
_, body = account_bodies[0]
assert body["concurrency"] == 20
assert body["priority"] == 5
assert body["credentials"]["base_url"] == "http://packy.local"
def test_import_platform_from_snapshot(monkeypatch, db_session):
"""验证快照 platform=anthropic 且名称不含 Claude 时,创建账号平台必须为 anthropic"""
from app.models.snapshot import UpstreamRateSnapshot
website, generated = seed_account_import_rows(db_session)
generated.key_name = "MyCustomKey" # 不含 Claude
# 写入快照指定平台为 anthropic
snapshot = UpstreamRateSnapshot(
upstream_id=generated.upstream_id,
snapshot_json=json.dumps({
"groups": {
"vip": {
"group_name": "VIP",
"rate": 0.1,
"platform": "anthropic"
}
}
})
)
db_session.add(snapshot)
db_session.commit()
account_bodies = []
class FakeWebsiteClient:
def __init__(self, **kwargs):
pass
def __enter__(self): return self
def __exit__(self, *args): return False
def get_groups(self, endpoint="/groups"):
return [{"id": "7", "name": "Target", "platform": "anthropic"}]
def create_account(self, body, endpoint="/accounts"):
account_bodies.append(body)
return {"id": 101, "name": body["name"]}
def account_exists(self, account_id): return True
@staticmethod
def extract_id(data): return "101"
monkeypatch.setattr(websites_router, "Sub2ApiWebsiteClient", FakeWebsiteClient)
websites_router.import_upstream_keys_as_accounts(
website.id,
ImportAccountsRequest(
upstream_key_ids=[generated.id],
target_group_map={"vip": "7"},
default_platform="openai",
platform_mode="auto",
),
db_session,
object(),
)
assert len(account_bodies) == 1
assert account_bodies[0]["platform"] == "anthropic"
def test_import_platform_fallback_to_name(monkeypatch, db_session):
"""验证快照无 platform 但名称含 Claude 时,仍兜底为 anthropic"""
from app.models.snapshot import UpstreamRateSnapshot
website, generated = seed_account_import_rows(db_session)
generated.key_name = "Claude-Key"
# 写入快照但不含 platform 字段
snapshot = UpstreamRateSnapshot(
upstream_id=generated.upstream_id,
snapshot_json=json.dumps({
"groups": {
"vip": {
"group_name": "VIP",
"rate": 0.1,
}
}
})
)
db_session.add(snapshot)
db_session.commit()
account_bodies = []
class FakeWebsiteClient:
def __init__(self, **kwargs):
pass
def __enter__(self): return self
def __exit__(self, *args): return False
def get_groups(self, endpoint="/groups"):
return [{"id": "7", "name": "Target", "platform": "anthropic"}]
def create_account(self, body, endpoint="/accounts"):
account_bodies.append(body)
return {"id": 101, "name": body["name"]}
def account_exists(self, account_id): return True
@staticmethod
def extract_id(data): return "101"
monkeypatch.setattr(websites_router, "Sub2ApiWebsiteClient", FakeWebsiteClient)
websites_router.import_upstream_keys_as_accounts(
website.id,
ImportAccountsRequest(
upstream_key_ids=[generated.id],
target_group_map={"vip": "7"},
default_platform="openai",
platform_mode="auto",
),
db_session,
object(),
)
assert len(account_bodies) == 1
assert account_bodies[0]["platform"] == "anthropic"
def test_import_platform_manual_override(monkeypatch, db_session):
"""手动模式下不使用快照平台,仍按用户指定平台"""
from app.models.snapshot import UpstreamRateSnapshot
website, generated = seed_account_import_rows(db_session)
# 快照虽然指定为 anthropic
snapshot = UpstreamRateSnapshot(
upstream_id=generated.upstream_id,
snapshot_json=json.dumps({
"groups": {
"vip": {
"group_name": "VIP",
"rate": 0.1,
"platform": "anthropic"
}
}
})
)
db_session.add(snapshot)
db_session.commit()
account_bodies = []
class FakeWebsiteClient:
def __init__(self, **kwargs):
pass
def __enter__(self): return self
def __exit__(self, *args): return False
def get_groups(self, endpoint="/groups"):
return [{"id": "7", "name": "Target", "platform": "gemini"}]
def create_account(self, body, endpoint="/accounts"):
account_bodies.append(body)
return {"id": 101, "name": body["name"]}
def account_exists(self, account_id): return True
@staticmethod
def extract_id(data): return "101"
monkeypatch.setattr(websites_router, "Sub2ApiWebsiteClient", FakeWebsiteClient)
websites_router.import_upstream_keys_as_accounts(
website.id,
ImportAccountsRequest(
upstream_key_ids=[generated.id],
target_group_map={"vip": "7"},
default_platform="gemini",
platform_mode="manual", # 手动模式
),
db_session,
object(),
)
assert len(account_bodies) == 1
assert account_bodies[0]["platform"] == "gemini"
def test_same_key_can_be_imported_into_two_platform_groups(monkeypatch, db_session):
"""同一 Key 的不同平台映射独立创建,迁移其中一平台不影响另一平台。"""
website, generated = seed_account_import_rows(db_session)
class FakeClient:
accounts = {}
next_id = 100
def __init__(self, **kwargs):
pass
def __enter__(self):
return self
def __exit__(self, *args):
return False
def get_groups(self, endpoint="/groups"):
return [
{"id": "7", "name": "OpenAI", "platform": "openai"},
{"id": "8", "name": "Claude", "platform": "anthropic"},
{"id": "9", "name": "OpenAI 2", "platform": "openai"},
]
def list_accounts(self):
return list(self.accounts.values())
def create_account(self, body, endpoint="/accounts"):
type(self).next_id += 1
account = {"id": str(type(self).next_id), **body}
type(self).accounts[account["id"]] = account
return account
def update_account(self, account_id, body):
type(self).accounts[str(account_id)].update(body)
return type(self).accounts[str(account_id)]
@staticmethod
def extract_id(data):
return str(data.get("id"))
monkeypatch.setattr(websites_router, "Sub2ApiWebsiteClient", FakeClient)
monkeypatch.setattr(websites_router, "reconcile_upstream_keys_full", lambda db, uid: None)
def do_import(target_group_id):
return websites_router.import_upstream_keys_as_accounts(
website.id,
ImportAccountsRequest(
upstream_key_ids=[generated.id],
target_group_map={"vip": target_group_id},
auto_priority_by_rate=False,
),
db_session,
object(),
)
first = do_import("7")
second = do_import("8")
third = do_import("9")
assert first.items[0].status == "created"
assert second.items[0].status == "created"
assert third.items[0].status == "exists"
assert third.items[0].platform == "openai"
links = db_session.query(websites_router.UpstreamKeyAccountLink).filter_by(
upstream_key_id=generated.id,
website_id=website.id,
status="active",
).all()
assert {(link.platform, link.target_group) for link in links} == {
("openai", "9"),
("anthropic", "8"),
}
assert len(FakeClient.accounts) == 2