mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 17:30:23 +08:00
feat: Provider 异步删除、可配置密码策略、Hub 超时优化及多项改进
- 新增 Provider 异步删除任务系统,后台分阶段删除子资源并清理残留引用 - 新增可配置密码策略等级(weak/medium/strong),支持系统设置面板调整 - aether-hub 升级至 0.1.4,idle timeout 支持禁用(设为 0),worker 默认超时调整为 120s - OAuth 手动续期增加 Redis 分布式锁,防止并发刷新冲突 - ProxyNode 心跳检测改为 asyncio.to_thread,避免阻塞事件循环 - 删除 ModelMultiSelect 和 useInvalidModels,MultiSelect 组件通用化 - 明确 allowed_providers/allowed_api_formats 的 NULL 与空数组语义 - 前端 StandaloneKeyFormDialog、UserFormDialog 等多处 UI 优化 - 新增 Alembic 迁移脚本清理 Provider 删除后的残留引用 - 补充相关测试用例
This commit is contained in:
159
tests/services/test_provider_delete_cleanup.py
Normal file
159
tests/services/test_provider_delete_cleanup.py
Normal file
@@ -0,0 +1,159 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from src.services.provider import delete_cleanup as cleanup_module
|
||||
from src.services.provider.delete_cleanup import (
|
||||
delete_provider_tree,
|
||||
prune_allowed_provider_list,
|
||||
prune_allowed_provider_refs,
|
||||
)
|
||||
|
||||
|
||||
def _query_mock(
|
||||
*,
|
||||
rows: list[object] | None = None,
|
||||
update_count: int | None = None,
|
||||
delete_count: int | None = None,
|
||||
) -> MagicMock:
|
||||
query = MagicMock()
|
||||
filtered = query.filter.return_value
|
||||
if rows is not None:
|
||||
filtered.all.return_value = rows
|
||||
if update_count is not None:
|
||||
filtered.update.return_value = update_count
|
||||
if delete_count is not None:
|
||||
filtered.delete.return_value = delete_count
|
||||
return query
|
||||
|
||||
|
||||
def test_prune_allowed_provider_list_removes_target_and_normalizes_empty() -> None:
|
||||
next_allowed, changed = prune_allowed_provider_list(["provider-a", "provider-b"], "provider-a")
|
||||
|
||||
assert changed is True
|
||||
assert next_allowed == ["provider-b"]
|
||||
|
||||
next_allowed, changed = prune_allowed_provider_list(["provider-a"], "provider-a")
|
||||
|
||||
assert changed is True
|
||||
assert next_allowed == []
|
||||
|
||||
|
||||
def test_prune_allowed_provider_refs_updates_matching_records_only() -> None:
|
||||
records = [
|
||||
SimpleNamespace(allowed_providers=["provider-a", "provider-b"]),
|
||||
SimpleNamespace(allowed_providers=["provider-b"]),
|
||||
SimpleNamespace(allowed_providers=None),
|
||||
]
|
||||
|
||||
updated = prune_allowed_provider_refs(records, "provider-a")
|
||||
|
||||
assert updated == 1
|
||||
assert records[0].allowed_providers == ["provider-b"]
|
||||
assert records[1].allowed_providers == ["provider-b"]
|
||||
assert records[2].allowed_providers is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cleanup_deleted_provider_references_cleans_large_fanout_tables(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
db = MagicMock()
|
||||
|
||||
user_records = [SimpleNamespace(allowed_providers=["provider-a", "provider-b"])]
|
||||
api_key_records = [SimpleNamespace(allowed_providers=["provider-a"])]
|
||||
key_cleanup_calls: list[list[str]] = []
|
||||
|
||||
monkeypatch.setattr(
|
||||
cleanup_module,
|
||||
"cleanup_key_references",
|
||||
lambda _db, key_ids: key_cleanup_calls.append(list(key_ids)),
|
||||
)
|
||||
|
||||
db.query.side_effect = [
|
||||
_query_mock(rows=[("endpoint-1",), ("endpoint-2",)]),
|
||||
_query_mock(rows=[("key-1",), ("key-2",)]),
|
||||
_query_mock(rows=user_records),
|
||||
_query_mock(rows=api_key_records),
|
||||
_query_mock(update_count=2),
|
||||
_query_mock(update_count=3),
|
||||
_query_mock(update_count=4),
|
||||
_query_mock(update_count=5),
|
||||
_query_mock(update_count=6),
|
||||
_query_mock(delete_count=7),
|
||||
_query_mock(delete_count=8),
|
||||
]
|
||||
|
||||
stats = cleanup_module.cleanup_deleted_provider_references(db, "provider-a")
|
||||
|
||||
assert stats == {
|
||||
"users": 1,
|
||||
"api_keys": 1,
|
||||
"user_preferences": 2,
|
||||
"usage_provider": 3,
|
||||
"usage_endpoint": 5,
|
||||
"video_tasks_provider": 4,
|
||||
"video_tasks_endpoint": 6,
|
||||
"request_candidates_provider": 8,
|
||||
"request_candidates_endpoint": 7,
|
||||
}
|
||||
assert user_records[0].allowed_providers == ["provider-b"]
|
||||
assert api_key_records[0].allowed_providers == []
|
||||
assert key_cleanup_calls == [["key-1", "key-2"]]
|
||||
|
||||
|
||||
def test_delete_provider_tree_deletes_children_before_provider(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
db = MagicMock()
|
||||
|
||||
cleanup_mock = MagicMock(
|
||||
return_value={
|
||||
"users": 1,
|
||||
"api_keys": 2,
|
||||
"user_preferences": 3,
|
||||
"usage_provider": 4,
|
||||
"usage_endpoint": 5,
|
||||
"video_tasks_provider": 6,
|
||||
"video_tasks_endpoint": 7,
|
||||
"request_candidates_provider": 8,
|
||||
"request_candidates_endpoint": 9,
|
||||
}
|
||||
)
|
||||
monkeypatch.setattr(cleanup_module, "cleanup_deleted_provider_references", cleanup_mock)
|
||||
|
||||
db.query.side_effect = [
|
||||
_query_mock(rows=[("endpoint-1",), ("endpoint-2",)]),
|
||||
_query_mock(rows=[("key-1",), ("key-2",), ("key-3",)]),
|
||||
_query_mock(delete_count=10),
|
||||
_query_mock(delete_count=11),
|
||||
_query_mock(delete_count=12),
|
||||
_query_mock(delete_count=13),
|
||||
_query_mock(delete_count=14),
|
||||
_query_mock(delete_count=1),
|
||||
]
|
||||
|
||||
stats = delete_provider_tree(db, "provider-a")
|
||||
|
||||
cleanup_mock.assert_called_once_with(
|
||||
db,
|
||||
"provider-a",
|
||||
endpoint_ids=["endpoint-1", "endpoint-2"],
|
||||
key_ids=["key-1", "key-2", "key-3"],
|
||||
)
|
||||
assert stats == {
|
||||
"cleanup": cleanup_mock.return_value,
|
||||
"deleted": {
|
||||
"api_key_mappings": 10,
|
||||
"usage_tracking": 11,
|
||||
"models": 12,
|
||||
"api_keys": 13,
|
||||
"endpoints": 14,
|
||||
"providers": 1,
|
||||
},
|
||||
"key_count": 3,
|
||||
"endpoint_count": 2,
|
||||
}
|
||||
Reference in New Issue
Block a user