"""Integration tests for channels account_router endpoints.""" from __future__ import annotations import httpx import pytest from .conftest import ( BASE_URL, DEFAULT_CHANNEL_TYPE, NON_EXISTENT_ACCOUNT_ID, ) pytestmark = [pytest.mark.asyncio, pytest.mark.integration] ACCOUNTS_URL = f"{BASE_URL}/accounts" def _make_create_account_body(display_name: str = "pytest_account") -> dict: """Build a minimal CreateAccountRequest body.""" return { "display_name": display_name, "raw_config": {"app_id": "pytest_app", "app_secret": "pytest_secret"}, } # ============================================================================= # === Auth three-tier for list accounts === # ============================================================================= async def test_list_accounts_requires_auth(test_client: httpx.AsyncClient): # Act response = await test_client.get(ACCOUNTS_URL) # Assert assert response.status_code == 401, response.text async def test_list_accounts_forbids_standard_user(test_client: httpx.AsyncClient, standard_user): # Act response = await test_client.get(ACCOUNTS_URL, headers=standard_user["headers"]) # Assert assert response.status_code == 403, response.text async def test_admin_can_list_accounts(test_client: httpx.AsyncClient, admin_headers): # Act response = await test_client.get(ACCOUNTS_URL, headers=admin_headers) # Assert assert response.status_code == 200, response.text payload = response.json() assert payload["success"] is True assert isinstance(payload["data"], dict) assert "items" in payload["data"] # ============================================================================= # === Pagination validation for list accounts === # ============================================================================= async def test_list_accounts_rejects_limit_zero(test_client: httpx.AsyncClient, admin_headers): # Act — limit=0 violates ge=1 response = await test_client.get( ACCOUNTS_URL, params={"limit": 0}, headers=admin_headers, ) # Assert assert response.status_code == 422, response.text async def test_list_accounts_rejects_limit_over_max(test_client: httpx.AsyncClient, admin_headers): # Act — limit=1001 violates le=1000 response = await test_client.get( ACCOUNTS_URL, params={"limit": 1001}, headers=admin_headers, ) # Assert assert response.status_code == 422, response.text # ============================================================================= # === batch enable / disable === # ============================================================================= async def test_batch_enable_accounts_requires_auth(test_client: httpx.AsyncClient): # Arrange body = {"account_ids": [NON_EXISTENT_ACCOUNT_ID]} # Act response = await test_client.post(f"{ACCOUNTS_URL}/batch/enable", json=body) # Assert assert response.status_code == 401, response.text async def test_batch_enable_accounts_forbids_standard_user(test_client: httpx.AsyncClient, standard_user): # Arrange body = {"account_ids": [NON_EXISTENT_ACCOUNT_ID]} # Act response = await test_client.post( f"{ACCOUNTS_URL}/batch/enable", json=body, headers=standard_user["headers"], ) # Assert assert response.status_code == 403, response.text async def test_batch_enable_accounts_rejects_empty_body(test_client: httpx.AsyncClient, admin_headers): # Arrange — account_ids 与 filter 同时为空 body = {"account_ids": None, "filter": None} # Act response = await test_client.post( f"{ACCOUNTS_URL}/batch/enable", json=body, headers=admin_headers, ) # Assert assert response.status_code in (400, 422), response.text async def test_batch_disable_accounts_rejects_empty_body(test_client: httpx.AsyncClient, admin_headers): # Arrange body = {"account_ids": None, "filter": None} # Act response = await test_client.post( f"{ACCOUNTS_URL}/batch/disable", json=body, headers=admin_headers, ) # Assert assert response.status_code in (400, 422), response.text # ============================================================================= # === create account under channel_type === # ============================================================================= async def test_create_account_requires_auth(test_client: httpx.AsyncClient): # Arrange body = _make_create_account_body() # Act response = await test_client.post( f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/accounts", json=body, ) # Assert assert response.status_code == 401, response.text async def test_create_account_forbids_standard_user(test_client: httpx.AsyncClient, standard_user): # Arrange body = _make_create_account_body() # Act response = await test_client.post( f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/accounts", json=body, headers=standard_user["headers"], ) # Assert assert response.status_code == 403, response.text async def test_create_account_invalid_channel_type_returns_422(test_client: httpx.AsyncClient, admin_headers): # Arrange — 适配器未注册时返回 422 body = _make_create_account_body() # Act response = await test_client.post( f"{BASE_URL}/invalid_channel/accounts", json=body, headers=admin_headers, ) # Assert — 不应是 401/403;适配器不存在应返回 422 assert response.status_code == 422, response.text async def test_create_account_with_invalid_body_returns_not_4xx(test_client: httpx.AsyncClient, admin_headers): # Arrange — display_name 过长触发校验失败 body = { "display_name": "x" * 200, "raw_config": {"app_id": "pytest_app"}, } # Act response = await test_client.post( f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/accounts", json=body, headers=admin_headers, ) # Assert assert response.status_code == 422, response.text # ============================================================================= # === get / update / delete / state / runtime / rotate / test / export / clone === # ============================================================================= async def test_get_account_non_existent_returns_404(test_client: httpx.AsyncClient, admin_headers): # Act response = await test_client.get( f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/accounts/{NON_EXISTENT_ACCOUNT_ID}", headers=admin_headers, ) # Assert assert response.status_code == 404, response.text async def test_update_account_non_existent_returns_404(test_client: httpx.AsyncClient, admin_headers): # Arrange body = {"display_name": "updated_name"} # Act response = await test_client.put( f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/accounts/{NON_EXISTENT_ACCOUNT_ID}", json=body, headers=admin_headers, ) # Assert assert response.status_code == 404, response.text async def test_update_account_rejects_empty_body(test_client: httpx.AsyncClient, admin_headers): # Arrange — display_name 与 raw_config 同时为空,dispatch 校验返回 400 body = {"display_name": None, "raw_config": None} # Act response = await test_client.put( f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/accounts/{NON_EXISTENT_ACCOUNT_ID}", json=body, headers=admin_headers, ) # Assert assert response.status_code in (400, 422, 404), response.text async def test_delete_account_non_existent_returns_404(test_client: httpx.AsyncClient, admin_headers): # Act response = await test_client.delete( f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/accounts/{NON_EXISTENT_ACCOUNT_ID}", headers=admin_headers, ) # Assert assert response.status_code == 404, response.text async def test_enable_account_non_existent_returns_404(test_client: httpx.AsyncClient, admin_headers): # Act response = await test_client.post( f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/accounts/{NON_EXISTENT_ACCOUNT_ID}/enable", headers=admin_headers, ) # Assert assert response.status_code == 404, response.text async def test_disable_account_non_existent_returns_404(test_client: httpx.AsyncClient, admin_headers): # Act response = await test_client.post( f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/accounts/{NON_EXISTENT_ACCOUNT_ID}/disable", headers=admin_headers, ) # Assert assert response.status_code == 404, response.text async def test_recover_account_non_existent_returns_404(test_client: httpx.AsyncClient, admin_headers): # Act response = await test_client.post( f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/accounts/{NON_EXISTENT_ACCOUNT_ID}/recover", headers=admin_headers, ) # Assert assert response.status_code == 404, response.text async def test_get_runtime_status_non_existent_returns_404(test_client: httpx.AsyncClient, admin_headers): # Act response = await test_client.get( f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/accounts/{NON_EXISTENT_ACCOUNT_ID}/runtime-status", headers=admin_headers, ) # Assert assert response.status_code == 404, response.text async def test_rotate_credentials_non_existent_returns_404(test_client: httpx.AsyncClient, admin_headers): # Act response = await test_client.post( f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/accounts/{NON_EXISTENT_ACCOUNT_ID}/rotate-credentials", headers=admin_headers, ) # Assert assert response.status_code == 404, response.text async def test_test_connection_non_existent_returns_404(test_client: httpx.AsyncClient, admin_headers): # Act response = await test_client.post( f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/accounts/{NON_EXISTENT_ACCOUNT_ID}/test-connection", headers=admin_headers, ) # Assert assert response.status_code == 404, response.text # ============================================================================= # === export account (with include_secrets authorization) === # ============================================================================= async def test_export_account_non_existent_returns_404(test_client: httpx.AsyncClient, admin_headers): # Act response = await test_client.get( f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/accounts/{NON_EXISTENT_ACCOUNT_ID}/export", headers=admin_headers, ) # Assert assert response.status_code == 404, response.text async def test_export_account_with_secrets_non_existent_returns_404(test_client: httpx.AsyncClient, admin_headers): # Arrange — admin_headers 为 superadmin,include_secrets=True 的 superadmin # 校验在 dispatch 阶段通过;账户不存在 → 404。 # 注:include_secrets=True 拒绝非 superadmin 管理员的场景无法用 admin_headers # 验证(admin_headers 本身即 superadmin);standard_user 会被 get_admin_user # 守门拦截(403),无法抵达 include_secrets 校验。 # Act response = await test_client.get( f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/accounts/{NON_EXISTENT_ACCOUNT_ID}/export", params={"include_secrets": True}, headers=admin_headers, ) # Assert assert response.status_code == 404, response.text # ============================================================================= # === clone account === # ============================================================================= async def test_clone_account_non_existent_returns_404(test_client: httpx.AsyncClient, admin_headers): # Arrange body = {"new_display_name": "cloned_account", "include_credentials": False} # Act response = await test_client.post( f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/accounts/{NON_EXISTENT_ACCOUNT_ID}/clone", json=body, headers=admin_headers, ) # Assert assert response.status_code == 404, response.text async def test_clone_account_rejects_invalid_body(test_client: httpx.AsyncClient, admin_headers): # Arrange — new_display_name 过长触发校验失败 body = {"new_display_name": "x" * 200, "include_credentials": False} # Act response = await test_client.post( f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/accounts/{NON_EXISTENT_ACCOUNT_ID}/clone", json=body, headers=admin_headers, ) # Assert assert response.status_code == 422, response.text