""" 集成测试: 配置项 + LDAP + 通知渠道 + 种子数据 覆盖: 系统设置CRUD/LDAP配置/通知渠道CRUD/种子数据验证 """ import pytest from httpx import AsyncClient def _auth(token: str) -> dict: return {"Authorization": f"Bearer {token}"} # ══════════════════════════════════════════════════════════════ # 1. 系统设置 # ══════════════════════════════════════════════════════════════ class TestSystemSettings: async def test_get_settings(self, client, admin_token): resp = await client.get("/api/v1/settings", headers=_auth(admin_token)) assert resp.status_code == 200 async def test_update_settings(self, client, admin_token): resp = await client.put("/api/v1/settings", json=[ {"key": "query_max_length", "value": "10000"}, {"key": "max_export_rows", "value": "10000"}, ], headers=_auth(admin_token)) assert resp.status_code == 200 async def test_settings_persist_after_update(self, client, admin_token): # 更新 await client.put("/api/v1/settings", json=[ {"key": "query_max_length", "value": "5000"}, ], headers=_auth(admin_token)) # 读取验证 resp = await client.get("/api/v1/settings", headers=_auth(admin_token)) assert resp.status_code == 200 data = resp.json() # settings 返回分组格式 assert isinstance(data, dict) async def test_developer_cannot_update_settings(self, client, developer_token): resp = await client.put("/api/v1/settings", json=[ {"key": "query_max_length", "value": "99999"}, ], headers=_auth(developer_token)) assert resp.status_code == 403 # ══════════════════════════════════════════════════════════════ # 2. LDAP 配置 # ══════════════════════════════════════════════════════════════ class TestLDAPConfig: async def test_get_ldap_config(self, client, admin_token): resp = await client.get("/api/v1/ldap-config", headers=_auth(admin_token)) # 可能返回空或默认配置 assert resp.status_code in (200, 404) async def test_update_ldap_config(self, client, admin_token): resp = await client.put("/api/v1/ldap-config", json={ "host": "ldap.example.com", "port": 389, "base_dn": "dc=example,dc=com", "bind_dn": "cn=admin,dc=example,dc=com", "admin_password": "test123", "use_ssl": False, "user_search_filter": "(uid={username})", }, headers=_auth(admin_token)) assert resp.status_code == 200 async def test_ldap_field_validation(self, client, admin_token): # 空host应该校验失败 resp = await client.put("/api/v1/ldap-config", json={ "host": "", "port": 389, "base_dn": "dc=example,dc=com", "bind_dn": "cn=admin", "admin_password": "test", }, headers=_auth(admin_token)) assert resp.status_code in (400, 422, 200) # 取决于校验严格度 async def test_developer_cannot_update_ldap(self, client, developer_token): resp = await client.put("/api/v1/ldap-config", json={ "host": "hack.example.com", "port": 389, "base_dn": "dc=hack", "bind_dn": "cn=hack", "admin_password": "hack", }, headers=_auth(developer_token)) assert resp.status_code == 403 # ══════════════════════════════════════════════════════════════ # 3. 通知渠道 CRUD # ══════════════════════════════════════════════════════════════ class TestNotificationCRUD: async def test_list_notifications_empty(self, client, admin_token): resp = await client.get("/api/v1/notifications", headers=_auth(admin_token)) assert resp.status_code == 200 async def test_create_feishu_notification(self, client, admin_token): resp = await client.post("/api/v1/notifications", json={ "name": "飞书通知", "type": "feishu", "config": {"webhook_url": "https://open.feishu.cn/test/webhook"}, "enabled": True, }, headers=_auth(admin_token)) assert resp.status_code in (200, 201) async def test_create_dingtalk_notification(self, client, admin_token): resp = await client.post("/api/v1/notifications", json={ "name": "钉钉通知", "type": "dingtalk", "config": {"webhook_url": "https://oapi.dingtalk.com/robot/send?access_token=test"}, "enabled": True, }, headers=_auth(admin_token)) assert resp.status_code in (200, 201) async def test_full_notification_lifecycle(self, client, admin_token): # 创建 resp = await client.post("/api/v1/notifications", json={ "name": "生命周期通知", "type": "webhook", "config": {"url": "https://example.com/hook"}, "enabled": True, }, headers=_auth(admin_token)) assert resp.status_code in (200, 201) nid = resp.json()["id"] # 列表 list_resp = await client.get("/api/v1/notifications", headers=_auth(admin_token)) assert list_resp.status_code == 200 data = list_resp.json() items = data if isinstance(data, list) else data.get("items", []) assert len(items) >= 1 # 更新 upd = await client.put(f"/api/v1/notifications/{nid}", json={ "name": "已更新", "enabled": False, }, headers=_auth(admin_token)) assert upd.status_code == 200 # 测试发送 (会失败因为URL不存在,但应返回非500) test = await client.post(f"/api/v1/notifications/{nid}/test", headers=_auth(admin_token)) assert test.status_code in (200, 400, 404, 500) # 外部调用失败可接受 # 删除 delete = await client.delete(f"/api/v1/notifications/{nid}", headers=_auth(admin_token)) assert delete.status_code in (200, 204) async def test_developer_cannot_create_notification(self, client, developer_token): resp = await client.post("/api/v1/notifications", json={ "name": "非法", "type": "webhook", "config": {"url": "https://evil.com"}, "enabled": True, }, headers=_auth(developer_token)) assert resp.status_code == 403 # ══════════════════════════════════════════════════════════════ # 4. 种子数据验证 # ══════════════════════════════════════════════════════════════ class TestSeedData: async def test_audit_rules_seeded(self, client, admin_token): """首次启动应有默认审核规则""" resp = await client.get("/api/v1/audit-rules", headers=_auth(admin_token)) assert resp.status_code == 200 rules = resp.json() assert len(rules) > 0, "应有种子审核规则" # 验证规则覆盖多种类型 types = set(r["type"] for r in rules) assert len(types) >= 3, f"应覆盖多种类型, 实际: {types}" async def test_default_workflow_exists(self, client, admin_token): """默认工作流应存在""" resp = await client.get("/api/v1/workflows", headers=_auth(admin_token)) assert resp.status_code == 200 workflows = resp.json() assert len(workflows) >= 0 # 可能需要手动创建