Files
SmartUp/backend/test_organize_groups.py
T

327 lines
12 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 pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.pool import StaticPool
from app.database import Base
from app.models.upstream import Upstream
from app.models.upstream_key import UpstreamGeneratedKey
from app.models.website import Website, WebsiteGroupBinding
from app.routers.websites import organize_website_groups
@pytest.fixture()
def db_session():
engine = create_engine(
"sqlite://",
connect_args={"check_same_thread": False},
poolclass=StaticPool,
)
# Ensure all models are created
from app.models import admin_user, upstream, snapshot, webhook_config, notification_log, custom_page, website, revoked_token, upstream_key
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 test_organize_groups_full_scenarios(db_session, monkeypatch):
"""测试一键整理分组的各种核心场景:
1. 已有 Key 未导入时,创建目标账号并绑定目标分组
2. 已导入且目标账号存在时,不重复创建(若未绑定目标分组则补齐绑定)
3. 已导入但目标账号不存在时,清理旧标记并重建
4. 绑定关系存在但没有对应 Key 时,返回 missing_key
5. 多个目标分组、多上游分组绑定时,正确分类
"""
# 1. 建立测试 Website 和 Upstream
w = Website(
name="W1",
site_type="sub2api",
base_url="http://w1",
enabled=True,
auth_config_json="{}",
timeout_seconds=30
)
u1 = Upstream(name="U1", base_url="http://u1")
db_session.add_all([w, u1])
db_session.commit()
db_session.refresh(w)
db_session.refresh(u1)
# 2. 建立绑定关系
# 绑定 1:目标分组 TG1 ↔ 上游分组 G1(有 Key 未导入),G2(没有 Key — 对应 missing_key
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"},
{"upstream_id": u1.id, "group_id": "G2"},
]),
enabled=True
)
# 绑定 2:目标分组 TG2 ↔ 上游分组 G3(已导入且账号仍存在),G4(已导入但账号已删除)
b2 = WebsiteGroupBinding(
website_id=w.id,
target_group_id="TG2",
target_group_name="TG2-Group",
source_groups_json=json.dumps([
{"upstream_id": u1.id, "group_id": "G3"},
{"upstream_id": u1.id, "group_id": "G4"},
]),
enabled=True
)
db_session.add_all([b1, b2])
db_session.commit()
# 3. 建立上游 Key 记录
# G1: 未导入 Key
k1 = UpstreamGeneratedKey(
upstream_id=u1.id,
group_id="G1",
group_name="G1-Name",
key_name="Key-G1",
key_value="sk-g1-secret",
status="created",
)
# G3: 已导入且账号存在
k3 = UpstreamGeneratedKey(
upstream_id=u1.id,
group_id="G3",
group_name="G3-Name",
key_name="Key-G3",
key_value="sk-g3-secret",
status="imported",
imported_website_id=w.id,
imported_account_id="ACC-G3",
imported_target_group_id="TG2",
)
# G4: 已导入但账号已删除
k4 = UpstreamGeneratedKey(
upstream_id=u1.id,
group_id="G4",
group_name="G4-Name",
key_name="Key-G4",
key_value="sk-g4-secret",
status="imported",
imported_website_id=w.id,
imported_account_id="ACC-G4-DELETED",
imported_target_group_id="TG2",
)
db_session.add_all([k1, k3, k4])
db_session.commit()
# 4. Mock Sub2ApiWebsiteClient 交互
created_accounts = []
updated_accounts = []
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"},
{"id": "TG2", "name": "TG2-Group"},
]
def list_accounts(self):
# ACC-G3 仍存在,ACC-G4-DELETED 不在此列表中代表已被删除
# 另外,我们假设 ACC-G3 目前绑定的 group_ids 是 [999],即不包含 TG2 (或整型数值)
return [
{
"id": "ACC-G3",
"name": "SmartUp-G3-ACC",
"group_ids": [999],
}
]
def extract_id(self, val):
return val.get("id") if isinstance(val, dict) else str(val)
def create_account(self, body):
# 模拟创建账号
acc_id = f"NEW-{body['name']}"
new_acc = {"id": acc_id, "name": body["name"], "group_ids": body["group_ids"]}
created_accounts.append(new_acc)
return new_acc
def update_account(self, account_id, body):
# 模拟更新账号的分组绑定
updated_accounts.append((account_id, body))
return {"id": account_id, "group_ids": body.get("group_ids")}
monkeypatch.setattr("app.routers.websites.Sub2ApiWebsiteClient", MockClient)
monkeypatch.setattr("app.routers.websites.sync_account_priorities_for_website", lambda db, wid: [])
# 5. 执行一键整理
response = organize_website_groups(wid=w.id, db=db_session)
# 6. 断言结果
assert response.success is True
# 整理完成:创建账号 2 (k1创建 + k4重建) / 已存在 1 (k3已存在并补齐) / 缺少 Key 1 (G2缺少)
assert "创建账号 2" in response.message
assert "已存在 1" in response.message
assert "缺少 Key 1" in response.message
items = response.items
assert len(items) == 4
# 按 target_group_id 分类校验
# TG1 分组下的 G1 (已创建)
item_g1 = next(item for item in items if item.target_group_id == "TG1" and item.source_group_id == "G1")
assert item_g1.status == "created"
assert item_g1.key_name == "Key-G1"
assert item_g1.account_id.startswith("NEW-U1-")
# TG1 分组下的 G2 (缺少 Key)
item_g2 = next(item for item in items if item.target_group_id == "TG1" and item.source_group_id == "G2")
assert item_g2.status == "missing_key"
assert item_g2.message == "请先生成上游 Key"
# TG2 分组下的 G3 (已存在并补齐)
item_g3 = next(item for item in items if item.target_group_id == "TG2" and item.source_group_id == "G3")
assert item_g3.status == "exists"
assert "已补齐目标分组绑定" in item_g3.message
# 校验是否触发了 update_account 补齐了 TG2
assert len(updated_accounts) == 1
assert updated_accounts[0][0] == "ACC-G3"
# TG2 (或者其数值) 应该在更新后的 group_ids 中
assert "TG2" in updated_accounts[0][1]["group_ids"] or 999 in updated_accounts[0][1]["group_ids"]
# TG2 分组下的 G4 (清理后重建)
item_g4 = next(item for item in items if item.target_group_id == "TG2" and item.source_group_id == "G4")
assert item_g4.status == "recreated"
assert item_g4.message == "清理后重建账号"
assert item_g4.account_id.startswith("NEW-U1-")
# 7. 检查数据库中 Key 记录的状态变化
db_session.refresh(k1)
db_session.refresh(k3)
db_session.refresh(k4)
assert k1.imported_website_id == w.id
assert k1.imported_account_id is not None
assert k1.imported_target_group_id == "TG1"
assert k1.status == "imported"
assert k3.imported_target_group_id == "TG2"
assert k4.imported_website_id == w.id
assert k4.imported_account_id is not None
assert k4.imported_target_group_id == "TG2"
assert k4.status == "imported"
def test_organize_groups_list_accounts_none_conservatively_skips(db_session, monkeypatch):
"""测试一键整理分组时,若远端账号列表拉取失败返回 None:
- 已导入的账号不得被清除或重建,而应该保守跳过(状态记为 failed,提示无法校验)。
- 未导入的账号仍能正常导入创建。
"""
w = Website(
name="W1",
site_type="sub2api",
base_url="http://w1",
enabled=True,
auth_config_json="{}",
timeout_seconds=30
)
u1 = Upstream(name="U1", base_url="http://u1")
db_session.add_all([w, u1])
db_session.commit()
db_session.refresh(w)
db_session.refresh(u1)
# 绑定关系:目标分组 TG1 ↔ 上游分组 G1(未导入),G3(已导入)
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", "group_name": "G1-SourceGroup"},
{"upstream_id": u1.id, "group_id": "G3", "group_name": "G3-SourceGroup"},
]),
enabled=True
)
db_session.add_all([b1])
db_session.commit()
# k1: 未导入 Key
k1 = UpstreamGeneratedKey(
upstream_id=u1.id,
group_id="G1",
group_name="G1-SourceGroup",
key_name="Key-G1",
key_value="sk-g1-secret",
status="created",
)
# k3: 已导入 Key
k3 = UpstreamGeneratedKey(
upstream_id=u1.id,
group_id="G3",
group_name="G3-SourceGroup",
key_name="Key-G3",
key_value="sk-g3-secret",
status="imported",
imported_website_id=w.id,
imported_account_id="ACC-G3-EXISTING",
imported_target_group_id="TG1",
)
db_session.add_all([k1, k3])
db_session.commit()
created_accounts = []
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):
# 模拟获取失败返回 None
return None
def extract_id(self, val):
return val.get("id") if isinstance(val, dict) else str(val)
def create_account(self, body):
acc_id = f"NEW-{body['name']}"
new_acc = {"id": acc_id, "name": body["name"], "group_ids": body["group_ids"]}
created_accounts.append(new_acc)
return new_acc
monkeypatch.setattr("app.routers.websites.Sub2ApiWebsiteClient", MockClient)
monkeypatch.setattr("app.routers.websites.sync_account_priorities_for_website", lambda db, wid: [])
# 执行一键整理
response = organize_website_groups(wid=w.id, db=db_session)
# 断言结果:由于其中有 1 个已导入账号校验失败,response.success 应为 False(有失败项)
assert response.success is False
# 统计信息中应该有“创建账号 1”与“失败 1”
assert "创建账号 1" in response.message
assert "失败 1" in response.message
items = response.items
assert len(items) == 2
# G1 (未导入) 仍应该成功创建账号
item_g1 = next(item for item in items if item.source_group_id == "G1")
assert item_g1.status == "created"
assert item_g1.account_id.startswith("NEW-U1-")
assert item_g1.source_group_name == "G1-SourceGroup" # 校验 group_name 优先从绑定中获取!
# G3 (已导入) 应该保守跳过
item_g3 = next(item for item in items if item.source_group_id == "G3")
assert item_g3.status == "failed"
assert item_g3.message == "无法校验目标账号状态,已保守跳过"
# 校验 DB 中的 k3 导入标记和状态绝对不能被清除/变动!
db_session.refresh(k3)
assert k3.imported_website_id == w.id
assert k3.imported_account_id == "ACC-G3-EXISTING"
assert k3.status == "imported"