"""Integration tests for channels pairing_router endpoints.""" from __future__ import annotations import httpx import pytest from .conftest import ( BASE_URL, DEFAULT_CHANNEL_TYPE, ISO_TIME_END, ISO_TIME_START, NON_EXISTENT_ACCOUNT_ID, NON_EXISTENT_PAIRING_ID, NON_EXISTENT_PEER_ID, ) pytestmark = [pytest.mark.asyncio, pytest.mark.integration] PAIRINGS_URL = f"{BASE_URL}/pairings" def _make_create_pairing_body() -> dict: """Build a minimal CreatePairingRequest body.""" return { "channel_type": DEFAULT_CHANNEL_TYPE, "account_id": NON_EXISTENT_ACCOUNT_ID, "peer_id": NON_EXISTENT_PEER_ID, "peer_name": "pytest_peer", "expires_in_seconds": 604800, } # ============================================================================= # === Auth three-tier for list pairings === # ============================================================================= async def test_list_pairings_requires_auth(test_client: httpx.AsyncClient): # Act response = await test_client.get(PAIRINGS_URL) # Assert assert response.status_code == 401, response.text async def test_list_pairings_forbids_standard_user( test_client: httpx.AsyncClient, standard_user ): # Act response = await test_client.get(PAIRINGS_URL, headers=standard_user["headers"]) # Assert assert response.status_code == 403, response.text async def test_admin_can_list_pairings(test_client: httpx.AsyncClient, admin_headers): # Act response = await test_client.get(PAIRINGS_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) # handler _pairingList 返回 {"pairings": [...]}(与前端 usePairingApproval 契约一致) assert "pairings" in payload["data"] # ============================================================================= # === Pagination + filter validation for list pairings === # ============================================================================= async def test_list_pairings_rejects_limit_zero(test_client: httpx.AsyncClient, admin_headers): # Act — limit=0 violates ge=1 response = await test_client.get( PAIRINGS_URL, params={"limit": 0}, headers=admin_headers, ) # Assert assert response.status_code == 422, response.text async def test_list_pairings_rejects_limit_over_max(test_client: httpx.AsyncClient, admin_headers): # Act — limit=1001 violates le=1000 response = await test_client.get( PAIRINGS_URL, params={"limit": 1001}, headers=admin_headers, ) # Assert assert response.status_code == 422, response.text async def test_list_pairings_rejects_invalid_status(test_client: httpx.AsyncClient, admin_headers): # Act — status 不在枚举范围内 response = await test_client.get( PAIRINGS_URL, params={"status": "INVALID_STATUS"}, headers=admin_headers, ) # Assert assert response.status_code == 422, response.text # ============================================================================= # === create pairing === # ============================================================================= async def test_create_pairing_requires_auth(test_client: httpx.AsyncClient): # Arrange body = _make_create_pairing_body() # Act response = await test_client.post(PAIRINGS_URL, json=body) # Assert assert response.status_code == 401, response.text async def test_create_pairing_forbids_standard_user( test_client: httpx.AsyncClient, standard_user ): # Arrange body = _make_create_pairing_body() # Act response = await test_client.post( PAIRINGS_URL, json=body, headers=standard_user["headers"], ) # Assert assert response.status_code == 403, response.text async def test_create_pairing_non_existent_account_returns_404( test_client: httpx.AsyncClient, admin_headers ): # Arrange body = _make_create_pairing_body() # Act response = await test_client.post(PAIRINGS_URL, json=body, headers=admin_headers) # Assert — account_id 不存在:NOT_FOUND (404) 或适配器能力未实现 (501) assert response.status_code in (404, 501), response.text async def test_create_pairing_rejects_expires_zero(test_client: httpx.AsyncClient, admin_headers): # Arrange — expires_in_seconds=0 低于下限 1 body = _make_create_pairing_body() body["expires_in_seconds"] = 0 # Act response = await test_client.post(PAIRINGS_URL, json=body, headers=admin_headers) # Assert assert response.status_code == 422, response.text async def test_create_pairing_rejects_expires_over_max( test_client: httpx.AsyncClient, admin_headers ): # Arrange — expires_in_seconds > 2592000 超过上限 body = _make_create_pairing_body() body["expires_in_seconds"] = 2592001 # Act response = await test_client.post(PAIRINGS_URL, json=body, headers=admin_headers) # Assert assert response.status_code == 422, response.text # ============================================================================= # === batch approve / reject === # ============================================================================= async def test_batch_approve_pairings_rejects_empty_ids( test_client: httpx.AsyncClient, admin_headers ): # Arrange — pairing_ids 为空违反 min_items=1 body = {"pairing_ids": []} # Act response = await test_client.post( f"{PAIRINGS_URL}/batch-approve", json=body, headers=admin_headers, ) # Assert assert response.status_code == 422, response.text async def test_batch_approve_pairings_rejects_too_many_ids( test_client: httpx.AsyncClient, admin_headers ): # Arrange — pairing_ids 超过 max_items=500 body = {"pairing_ids": [f"id_{i}" for i in range(501)]} # Act response = await test_client.post( f"{PAIRINGS_URL}/batch-approve", json=body, headers=admin_headers, ) # Assert assert response.status_code == 422, response.text async def test_batch_reject_pairings_rejects_empty_ids( test_client: httpx.AsyncClient, admin_headers ): # Arrange body = {"pairing_ids": []} # Act response = await test_client.post( f"{PAIRINGS_URL}/batch-reject", json=body, headers=admin_headers, ) # Assert assert response.status_code == 422, response.text # ============================================================================= # === clean expired === # ============================================================================= async def test_clean_expired_pairings_rejects_max_count_zero( test_client: httpx.AsyncClient, admin_headers ): # Arrange — max_count=0 低于下限 1 body = {"max_count": 0} # Act response = await test_client.post( f"{PAIRINGS_URL}/clean-expired", json=body, headers=admin_headers, ) # Assert assert response.status_code == 422, response.text async def test_clean_expired_pairings_rejects_max_count_over_max( test_client: httpx.AsyncClient, admin_headers ): # Arrange — max_count=1001 超过上限 1000 body = {"max_count": 1001} # Act response = await test_client.post( f"{PAIRINGS_URL}/clean-expired", json=body, headers=admin_headers, ) # Assert assert response.status_code == 422, response.text # ============================================================================= # === pairings stats === # ============================================================================= async def test_get_pairings_stats_returns_200(test_client: httpx.AsyncClient, admin_headers): # Act response = await test_client.get( f"{PAIRINGS_URL}/stats", params={"start_time": ISO_TIME_START, "end_time": ISO_TIME_END}, headers=admin_headers, ) # Assert assert response.status_code == 200, response.text payload = response.json() assert payload["success"] is True assert isinstance(payload["data"], dict) # ============================================================================= # === get pairing detail === # ============================================================================= async def test_get_pairing_non_existent_returns_404( test_client: httpx.AsyncClient, admin_headers ): # Act response = await test_client.get( f"{PAIRINGS_URL}/{NON_EXISTENT_PAIRING_ID}", headers=admin_headers, ) # Assert assert response.status_code == 404, response.text # ============================================================================= # === approve pairing === # ============================================================================= async def test_approve_pairing_non_existent_returns_404( test_client: httpx.AsyncClient, admin_headers ): # Arrange body = {"approver_id": "admin_xxx", "reason": "pytest"} # Act response = await test_client.post( f"{PAIRINGS_URL}/{NON_EXISTENT_PAIRING_ID}/approve", json=body, headers=admin_headers, ) # Assert assert response.status_code == 404, response.text # ============================================================================= # === reject pairing === # ============================================================================= async def test_reject_pairing_non_existent_returns_404( test_client: httpx.AsyncClient, admin_headers ): # Arrange body = {"approver_id": "admin_xxx", "reason": "pytest"} # Act response = await test_client.post( f"{PAIRINGS_URL}/{NON_EXISTENT_PAIRING_ID}/reject", json=body, headers=admin_headers, ) # Assert assert response.status_code == 404, response.text # ============================================================================= # === revoke pairing === # ============================================================================= async def test_revoke_pairing_non_existent_returns_404( test_client: httpx.AsyncClient, admin_headers ): # Arrange body = {"reason": "pytest"} # Act response = await test_client.post( f"{PAIRINGS_URL}/{NON_EXISTENT_PAIRING_ID}/revoke", json=body, headers=admin_headers, ) # Assert assert response.status_code == 404, response.text