"""Tests for Settings modules: Announcement, NotificationChannel, LDAP Config.""" import json import pytest from unittest.mock import AsyncMock, patch from httpx import AsyncClient from tests.conftest import auth_header pytestmark = pytest.mark.skip(reason="Skip: awaiting Go backend alignment") # ─── Announcement Tests ──────────────────────────────────────────── @pytest.mark.asyncio async def test_create_announcement(client: AsyncClient, admin_token: str): resp = await client.post( "/api/v1/announcements", json={"title": "系统公告", "content": "# Hello\n这是公告内容"}, headers=auth_header(admin_token), ) assert resp.status_code == 201 data = resp.json() assert data["title"] == "系统公告" assert data["content"] == "# Hello\n这是公告内容" assert data["is_published"] is False @pytest.mark.asyncio async def test_list_announcements(client: AsyncClient, admin_token: str, developer_token: str): # Create two announcements for i in range(2): await client.post( "/api/v1/announcements", json={"title": f"公告{i}", "content": f"内容{i}"}, headers=auth_header(admin_token), ) # Admin can list resp = await client.get("/api/v1/announcements", headers=auth_header(admin_token)) assert resp.status_code == 200 assert len(resp.json()) == 2 # Developer can also list resp = await client.get("/api/v1/announcements", headers=auth_header(developer_token)) assert resp.status_code == 200 assert len(resp.json()) == 2 @pytest.mark.asyncio async def test_update_announcement(client: AsyncClient, admin_token: str): create = await client.post( "/api/v1/announcements", json={"title": "原始标题", "content": "原始内容"}, headers=auth_header(admin_token), ) aid = create.json()["id"] resp = await client.put( f"/api/v1/announcements/{aid}", json={"title": "更新标题", "is_published": True}, headers=auth_header(admin_token), ) assert resp.status_code == 200 data = resp.json() assert data["title"] == "更新标题" assert data["is_published"] is True assert data["published_at"] is not None @pytest.mark.asyncio async def test_delete_announcement(client: AsyncClient, admin_token: str): create = await client.post( "/api/v1/announcements", json={"title": "待删除", "content": "内容"}, headers=auth_header(admin_token), ) aid = create.json()["id"] resp = await client.delete( f"/api/v1/announcements/{aid}", headers=auth_header(admin_token), ) assert resp.status_code == 200 # Verify deleted resp = await client.get("/api/v1/announcements", headers=auth_header(admin_token)) assert all(a["id"] != aid for a in resp.json()) @pytest.mark.asyncio async def test_announcement_non_admin_forbidden(client: AsyncClient, developer_token: str): # Create forbidden resp = await client.post( "/api/v1/announcements", json={"title": "test", "content": "test"}, headers=auth_header(developer_token), ) assert resp.status_code == 403 # Update forbidden resp = await client.put( "/api/v1/announcements/1", json={"title": "test"}, headers=auth_header(developer_token), ) assert resp.status_code == 403 # Delete forbidden resp = await client.delete( "/api/v1/announcements/1", headers=auth_header(developer_token), ) assert resp.status_code == 403 # ─── NotificationChannel Tests ───────────────────────────────────── @pytest.mark.asyncio async def test_create_notification_channel(client: AsyncClient, admin_token: str): resp = await client.post( "/api/v1/notifications", json={ "name": "钉钉告警", "channel_type": "dingtalk", "config": {"webhook_url": "https://oapi.dingtalk.com/robot/send?access_token=xxx", "secret": "mysecret"}, "is_enabled": True, }, headers=auth_header(admin_token), ) assert resp.status_code == 201 data = resp.json() assert data["name"] == "钉钉告警" assert data["channel_type"] == "dingtalk" # Password/secret should be masked assert data["config"]["secret"] == "******" @pytest.mark.asyncio async def test_list_notification_channels(client: AsyncClient, admin_token: str): resp = await client.get( "/api/v1/notifications", headers=auth_header(admin_token), ) assert resp.status_code == 200 assert isinstance(resp.json(), list) @pytest.mark.asyncio async def test_update_notification_channel(client: AsyncClient, admin_token: str): create = await client.post( "/api/v1/notifications", json={ "name": "飞书通知", "channel_type": "webhook_feishu", "config": {"webhook_url": "https://open.feishu.cn/open-apis/bot/v2/hook/xxx"}, }, headers=auth_header(admin_token), ) cid = create.json()["id"] resp = await client.put( f"/api/v1/notifications/{cid}", json={"name": "飞书通知-更新", "is_enabled": False}, headers=auth_header(admin_token), ) assert resp.status_code == 200 assert resp.json()["name"] == "飞书通知-更新" assert resp.json()["is_enabled"] is False @pytest.mark.asyncio async def test_delete_notification_channel(client: AsyncClient, admin_token: str): create = await client.post( "/api/v1/notifications", json={ "name": "待删除", "channel_type": "email", "config": { "smtp_host": "smtp.example.com", "smtp_port": 465, "smtp_user": "user@example.com", "smtp_password": "pass123", "from_addr": "user@example.com", "use_tls": True, }, }, headers=auth_header(admin_token), ) cid = create.json()["id"] resp = await client.delete( f"/api/v1/notifications/{cid}", headers=auth_header(admin_token), ) assert resp.status_code == 200 @pytest.mark.asyncio @patch("app.services.notification_sender.NotificationSender.send", new_callable=AsyncMock) async def test_notification_test_endpoint(mock_send, client: AsyncClient, admin_token: str): mock_send.return_value = {"success": True, "message": "钉钉测试消息发送成功"} create = await client.post( "/api/v1/notifications", json={ "name": "钉钉测试", "channel_type": "dingtalk", "config": {"webhook_url": "https://oapi.dingtalk.com/robot/send?access_token=***"}, "is_enabled": True, }, headers=auth_header(admin_token), ) cid = create.json()["id"] resp = await client.post( f"/api/v1/notifications/{cid}/test", headers=auth_header(admin_token), ) assert resp.status_code == 200 data = resp.json() assert data["success"] is True @pytest.mark.asyncio async def test_notification_non_admin_forbidden(client: AsyncClient, developer_token: str): resp = await client.get( "/api/v1/notifications", headers=auth_header(developer_token), ) assert resp.status_code == 403 @pytest.mark.asyncio async def test_email_config_mask_password(client: AsyncClient, admin_token: str): create = await client.post( "/api/v1/notifications", json={ "name": "邮件通道", "channel_type": "email", "config": { "smtp_host": "smtp.example.com", "smtp_port": 587, "smtp_user": "admin@example.com", "smtp_password": "super_secret_pass", "from_addr": "admin@example.com", "use_tls": True, }, }, headers=auth_header(admin_token), ) data = create.json() assert data["config"]["smtp_password"] == "******" # ─── LDAP Config Tests ───────────────────────────────────────────── @pytest.mark.asyncio async def test_create_ldap_config(client: AsyncClient, admin_token: str): resp = await client.put( "/api/v1/ldap-config", json={ "ldap_host": "ldap.example.com", "ldap_port": 389, "use_ldaps": False, "admin_dn": "cn=admin,dc=example,dc=com", "admin_password": "admin_password_123", "search_filter": "(uid=%s)", "base_dn": "ou=users,dc=example,dc=com", "user_mapping": {"username": "uid", "display_name": "cn", "email": "mail"}, }, headers=auth_header(admin_token), ) assert resp.status_code == 200 data = resp.json() assert data["ldap_host"] == "ldap.example.com" assert data["ldap_port"] == 389 # Password should be masked assert data["admin_password"] == "******" assert data["user_mapping"]["username"] == "uid" @pytest.mark.asyncio async def test_get_ldap_config(client: AsyncClient, admin_token: str): # First create await client.put( "/api/v1/ldap-config", json={ "ldap_host": "ldap.test.com", "ldap_port": 636, "use_ldaps": True, "admin_dn": "cn=admin,dc=test,dc=com", "admin_password": "testpass", "base_dn": "dc=test,dc=com", }, headers=auth_header(admin_token), ) resp = await client.get( "/api/v1/ldap-config", headers=auth_header(admin_token), ) assert resp.status_code == 200 data = resp.json() assert data["ldap_host"] == "ldap.test.com" assert data["use_ldaps"] is True assert data["admin_password"] == "******" @pytest.mark.asyncio async def test_update_ldap_config(client: AsyncClient, admin_token: str): # Create await client.put( "/api/v1/ldap-config", json={ "ldap_host": "ldap.old.com", "ldap_port": 389, "admin_dn": "cn=admin,dc=old,dc=com", "admin_password": "oldpass", "base_dn": "dc=old,dc=com", }, headers=auth_header(admin_token), ) # Update resp = await client.put( "/api/v1/ldap-config", json={ "ldap_host": "ldap.new.com", "ldap_port": 636, "use_ldaps": True, "admin_dn": "cn=admin,dc=new,dc=com", "admin_password": "newpass", "base_dn": "dc=new,dc=com", }, headers=auth_header(admin_token), ) assert resp.status_code == 200 assert resp.json()["ldap_host"] == "ldap.new.com" assert resp.json()["use_ldaps"] is True @pytest.mark.asyncio async def test_ldap_config_not_found(client: AsyncClient, admin_token: str): resp = await client.get( "/api/v1/ldap-config", headers=auth_header(admin_token), ) assert resp.status_code == 404 @pytest.mark.asyncio async def test_ldap_test_connection(client: AsyncClient, admin_token: str): # Create config first await client.put( "/api/v1/ldap-config", json={ "ldap_host": "ldap.example.com", "ldap_port": 389, "admin_dn": "cn=admin,dc=example,dc=com", "admin_password": "admin123", "base_dn": "dc=example,dc=com", }, headers=auth_header(admin_token), ) resp = await client.post( "/api/v1/ldap-config/test", json={"test_user": "testuser", "test_password": "testpass"}, headers=auth_header(admin_token), ) assert resp.status_code == 200 data = resp.json() # 测试环境无真实 LDAP 服务器,应返回失败 assert data["success"] is False @pytest.mark.asyncio async def test_ldap_non_admin_forbidden(client: AsyncClient, developer_token: str): resp = await client.get( "/api/v1/ldap-config", headers=auth_header(developer_token), ) assert resp.status_code == 403