"""LDAP 配置管理端点测试 — 覆盖 GET/PUT /api/v1/ldap-config 和 POST /test""" import pytest from httpx import AsyncClient from tests.conftest import auth_header pytestmark = pytest.mark.skip(reason="Skip: awaiting Go backend alignment") # ── 合法的 LDAP 配置载荷 ────────────────────────────────────────── VALID_LDAP_PAYLOAD = { "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"}, } # ──────────────────────────────────────────────────────────────────── # 1. 权限校验:非 admin 角色应被拒绝 (403) # ──────────────────────────────────────────────────────────────────── @pytest.mark.asyncio async def test_get_ldap_config_developer_forbidden( client: AsyncClient, developer_token: str ): """developer 角色访问 GET /ldap-config 应返回 403""" resp = await client.get( "/api/v1/ldap-config", headers=auth_header(developer_token), ) assert resp.status_code == 403 @pytest.mark.asyncio async def test_put_ldap_config_developer_forbidden( client: AsyncClient, developer_token: str ): """developer 角色访问 PUT /ldap-config 应返回 403""" resp = await client.put( "/api/v1/ldap-config", json=VALID_LDAP_PAYLOAD, headers=auth_header(developer_token), ) assert resp.status_code == 403 @pytest.mark.asyncio async def test_test_ldap_connection_developer_forbidden( client: AsyncClient, developer_token: str ): """developer 角色访问 POST /ldap-config/test 应返回 403""" resp = await client.post( "/api/v1/ldap-config/test", json={"test_user": "zhangsan", "test_password": "pass"}, headers=auth_header(developer_token), ) assert resp.status_code == 403 @pytest.mark.asyncio async def test_ldap_endpoints_require_authentication(client: AsyncClient): """未携带 Token 访问任何 LDAP 端点应返回 401""" resp_get = await client.get("/api/v1/ldap-config") assert resp_get.status_code in (401, 403) resp_put = await client.put("/api/v1/ldap-config", json=VALID_LDAP_PAYLOAD) assert resp_put.status_code in (401, 403) resp_test = await client.post( "/api/v1/ldap-config/test", json={"test_user": "u", "test_password": "p"}, ) assert resp_test.status_code in (401, 403) # ──────────────────────────────────────────────────────────────────── # 2. PUT 创建配置(首次 upsert) # ──────────────────────────────────────────────────────────────────── @pytest.mark.asyncio async def test_create_ldap_config_success(client: AsyncClient, admin_token: str): """admin 首次 PUT 创建 LDAP 配置,返回 200 且字段正确""" resp = await client.put( "/api/v1/ldap-config", json=VALID_LDAP_PAYLOAD, 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 assert data["use_ldaps"] is False assert data["admin_dn"] == "cn=admin,dc=example,dc=com" assert data["search_filter"] == "(uid=%s)" assert data["base_dn"] == "ou=users,dc=example,dc=com" # user_mapping 应被解析为 JSON dict assert data["user_mapping"]["username"] == "uid" assert data["user_mapping"]["display_name"] == "cn" assert data["user_mapping"]["email"] == "mail" # 时间戳字段应存在 assert data["created_at"] is not None assert data["updated_at"] is not None # ──────────────────────────────────────────────────────────────────── # 3. 密码脱敏:返回值中密码应为 ****** # ──────────────────────────────────────────────────────────────────── @pytest.mark.asyncio async def test_ldap_config_password_masked(client: AsyncClient, admin_token: str): """创建配置后返回的 admin_password 必须脱敏为 ******""" resp = await client.put( "/api/v1/ldap-config", json={ "ldap_host": "ldap.mask.com", "ldap_port": 389, "admin_dn": "cn=admin,dc=mask,dc=com", "admin_password": "super_secret_密码", "base_dn": "dc=mask,dc=com", }, headers=auth_header(admin_token), ) assert resp.status_code == 200 assert resp.json()["admin_password"] == "******" # ──────────────────────────────────────────────────────────────────── # 4. GET 获取配置(已存在 / 不存在) # ──────────────────────────────────────────────────────────────────── @pytest.mark.asyncio async def test_get_ldap_config_after_create(client: AsyncClient, admin_token: str): """先创建,再 GET 应返回相同配置""" await client.put( "/api/v1/ldap-config", json=VALID_LDAP_PAYLOAD, headers=auth_header(admin_token), ) resp = await client.get( "/api/v1/ldap-config", headers=auth_header(admin_token), ) assert resp.status_code == 200 assert resp.json()["ldap_host"] == "ldap.example.com" @pytest.mark.asyncio async def test_get_ldap_config_not_found(client: AsyncClient, admin_token: str): """未创建配置时 GET 应返回 404""" resp = await client.get( "/api/v1/ldap-config", headers=auth_header(admin_token), ) assert resp.status_code == 404 # ──────────────────────────────────────────────────────────────────── # 5. PUT 更新配置(第二次 upsert) # ──────────────────────────────────────────────────────────────────── @pytest.mark.asyncio async def test_update_existing_ldap_config(client: AsyncClient, admin_token: str): """对已有配置再次 PUT 应覆盖更新""" # 第一次创建 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), ) # 第二次更新 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", "search_filter": "(sAMAccountName=%s)", "base_dn": "dc=new,dc=com", }, headers=auth_header(admin_token), ) assert resp.status_code == 200 data = resp.json() assert data["ldap_host"] == "ldap.new.com" assert data["ldap_port"] == 636 assert data["use_ldaps"] is True assert data["search_filter"] == "(sAMAccountName=%s)" # ──────────────────────────────────────────────────────────────────── # 6. 输入校验:非法字段值应返回 422 # ──────────────────────────────────────────────────────────────────── @pytest.mark.asyncio async def test_create_ldap_config_missing_required_fields( client: AsyncClient, admin_token: str ): """缺少必填字段(ldap_host)应返回 422""" resp = await client.put( "/api/v1/ldap-config", json={ "ldap_port": 389, "admin_dn": "cn=admin,dc=example,dc=com", "admin_password": "pass", "base_dn": "dc=example,dc=com", }, headers=auth_header(admin_token), ) assert resp.status_code == 422 @pytest.mark.asyncio async def test_create_ldap_config_port_out_of_range( client: AsyncClient, admin_token: str ): """端口号超出范围 (>65535) 应返回 422""" payload = {**VALID_LDAP_PAYLOAD, "ldap_port": 99999} resp = await client.put( "/api/v1/ldap-config", json=payload, headers=auth_header(admin_token), ) assert resp.status_code == 422 @pytest.mark.asyncio async def test_create_ldap_config_empty_host(client: AsyncClient, admin_token: str): """ldap_host 为空字符串应返回 422""" payload = {**VALID_LDAP_PAYLOAD, "ldap_host": ""} resp = await client.put( "/api/v1/ldap-config", json=payload, headers=auth_header(admin_token), ) assert resp.status_code == 422 @pytest.mark.asyncio async def test_create_ldap_config_empty_password(client: AsyncClient, admin_token: str): """admin_password 为空字符串应返回 422""" payload = {**VALID_LDAP_PAYLOAD, "admin_password": ""} resp = await client.put( "/api/v1/ldap-config", json=payload, headers=auth_header(admin_token), ) assert resp.status_code in (400, 422) # ──────────────────────────────────────────────────────────────────── # 7. POST /test — 测试 LDAP 连接 # ──────────────────────────────────────────────────────────────────── @pytest.mark.asyncio async def test_ldap_connection_real_failure(client: AsyncClient, admin_token: str): """配置存在时 POST /test 应尝试真实连接,测试环境无 LDAP 服务器则返回 success=False""" await client.put( "/api/v1/ldap-config", json=VALID_LDAP_PAYLOAD, headers=auth_header(admin_token), ) resp = await client.post( "/api/v1/ldap-config/test", json={"test_user": "zhangsan", "test_password": "zhang123"}, headers=auth_header(admin_token), ) assert resp.status_code == 200 data = resp.json() # 测试环境无真实 LDAP 服务器,连接应失败 assert data["success"] is False # 消息中应包含连接错误信息 assert any(kw in data["message"] for kw in ["error", "fail", "未安装", "连接失败", "LDAP"]) @pytest.mark.asyncio async def test_ldap_connection_test_without_config( client: AsyncClient, admin_token: str ): """未创建配置时 POST /test 应返回 404""" resp = await client.post( "/api/v1/ldap-config/test", json={"test_user": "user1", "test_password": "pass1"}, headers=auth_header(admin_token), ) assert resp.status_code == 404 @pytest.mark.asyncio async def test_ldap_connection_test_missing_test_user( client: AsyncClient, admin_token: str ): """POST /test 缺少 test_user 应返回 422""" await client.put( "/api/v1/ldap-config", json=VALID_LDAP_PAYLOAD, headers=auth_header(admin_token), ) resp = await client.post( "/api/v1/ldap-config/test", json={"test_password": "pass"}, headers=auth_header(admin_token), ) assert resp.status_code == 422 # ──────────────────────────────────────────────────────────────────── # 8. 可选字段与默认值 # ──────────────────────────────────────────────────────────────────── @pytest.mark.asyncio async def test_create_ldap_config_with_defaults(client: AsyncClient, admin_token: str): """仅传必填字段,可选字段应有合理默认值""" resp = await client.put( "/api/v1/ldap-config", json={ "ldap_host": "ldap.defaults.com", "admin_dn": "cn=admin,dc=defaults,dc=com", "admin_password": "password123", "base_dn": "dc=defaults,dc=com", }, headers=auth_header(admin_token), ) assert resp.status_code == 200 data = resp.json() # ldap_port 默认 389 assert data["ldap_port"] == 389 # use_ldaps 默认 False assert data["use_ldaps"] is False # search_filter 未传,应为 None assert data["search_filter"] is None # user_mapping 未传,应为 None assert data["user_mapping"] is None @pytest.mark.asyncio async def test_ldap_config_use_ldaps_flag(client: AsyncClient, admin_token: str): """创建配置时 use_ldaps=True 应正确保存""" payload = {**VALID_LDAP_PAYLOAD, "use_ldaps": True, "ldap_port": 636} resp = await client.put( "/api/v1/ldap-config", json=payload, headers=auth_header(admin_token), ) assert resp.status_code == 200 assert resp.json()["use_ldaps"] is True assert resp.json()["ldap_port"] == 636