# #region TestAsyncAPIClient [C:3] [TYPE Module] [SEMANTICS test, async, client, semaphore] # @BRIEF Tests for AsyncAPIClient — contract tests, semaphore limits, timeout behavior. # @RELATION BINDS_TO -> [AsyncAPIClient] # @TEST_CONTRACT: AsyncAPIClient.request() -> Response dict # @TEST_INVARIANT: Semaphore limits concurrent requests — VERIFIED_BY: test_semaphore_limits_concurrent_requests # @TEST_INVARIANT: Semaphore timeout returns error — VERIFIED_BY: test_semaphore_timeout_returns_503 import asyncio import pytest from unittest.mock import AsyncMock, MagicMock, patch from src.core.utils.async_network import AsyncAPIClient from src.core.utils.network import SupersetAPIError _BASE_CONFIG = { "base_url": "http://superset.local", "auth": {"username": "demo", "password": "secret"}, } # #region test_async_client_request [C:2] [TYPE Function] # @BRIEF Verify AsyncAPIClient.request() returns parsed JSON on success. @pytest.mark.asyncio async def test_async_client_request(): client = AsyncAPIClient(config=_BASE_CONFIG, timeout=30) # Mock the internal httpx client mock_response = MagicMock() mock_response.status_code = 200 mock_response.json = MagicMock(return_value={"result": "ok"}) client._client = AsyncMock() client._client.request = AsyncMock(return_value=mock_response) # Mock auth client._authenticated = True client._tokens = {"access_token": "x", "csrf_token": "y"} result = await client.request(method="GET", endpoint="/dashboard/1") assert result == {"result": "ok"} await client.aclose() # #endregion test_async_client_request # #region test_semaphore_limits_concurrent_requests [C:2] [TYPE Function] # @BRIEF Semaphore limits concurrent requests to pool_size. Verify total runtime ≈ max(batch_time). @pytest.mark.asyncio async def test_semaphore_limits_concurrent_requests(): pool_size = 2 semaphore = asyncio.Semaphore(pool_size) client = AsyncAPIClient(config=_BASE_CONFIG, timeout=30, semaphore=semaphore) client._authenticated = True client._tokens = {"access_token": "x", "csrf_token": "y"} # Mock slow response — 0.1s each mock_resp = AsyncMock() mock_resp.status_code = 200 mock_resp.json = AsyncMock(return_value={"result": "ok"}) client._client = AsyncMock() client._client.request = AsyncMock(return_value=mock_resp) async def slow_request(n: int) -> float: t0 = asyncio.get_event_loop().time() await client.request(method="GET", endpoint=f"/dashboard/{n}") return asyncio.get_event_loop().time() - t0 # Launch 4 concurrent requests with pool_size=2 t0 = asyncio.get_event_loop().time() results = await asyncio.gather(*[slow_request(i) for i in range(4)]) elapsed = asyncio.get_event_loop().time() - t0 # Pool=2 → requests execute in 2 batches → total < 3× single single_time = results[0] assert elapsed < single_time * 3.5, f"Semaphore not limiting: {elapsed:.3f}s vs {single_time*2:.3f}s expected" await client.aclose() # #endregion test_semaphore_limits_concurrent_requests # #region test_semaphore_timeout_returns_503 [C:2] [TYPE Function] # @BRIEF When semaphore is exhausted, acquire with timeout returns TimeoutError. @pytest.mark.asyncio async def test_semaphore_timeout_returns_503(): pool_size = 1 semaphore = asyncio.Semaphore(pool_size) # Acquire the only slot await semaphore.acquire() # Try to acquire with very short timeout with pytest.raises(asyncio.TimeoutError): await asyncio.wait_for(semaphore.acquire(), timeout=0.05) semaphore.release() # #endregion test_semaphore_timeout_returns_503 # #endregion TestAsyncAPIClient