Restructure with Turborepo
This commit is contained in:
@@ -0,0 +1,258 @@
|
||||
import time
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from src.connectors.sdk.circuit_breaker import CircuitBreaker, CircuitState
|
||||
from src.connectors.sdk.retry import RetryPolicy, with_retry
|
||||
from src.connectors.sdk.context import ConnectorContext
|
||||
from src.connectors.sdk.auth import BasicAuthHandler, BearerAuthHandler, APIKeyAuthHandler
|
||||
from src.core.exceptions import ServiceUnavailableException
|
||||
|
||||
|
||||
class TestCircuitBreaker:
|
||||
def test_initial_state_is_closed(self):
|
||||
cb = CircuitBreaker(name="test")
|
||||
assert cb.state == CircuitState.CLOSED
|
||||
|
||||
def test_opens_after_threshold_failures(self):
|
||||
cb = CircuitBreaker(name="test", failure_threshold=3)
|
||||
for _ in range(3):
|
||||
cb._on_failure(Exception("fail"))
|
||||
assert cb._state == CircuitState.OPEN
|
||||
|
||||
def test_closed_does_not_raise(self):
|
||||
cb = CircuitBreaker(name="test")
|
||||
|
||||
async def _run():
|
||||
coro = cb.call(AsyncMock(return_value="ok"))
|
||||
return await coro
|
||||
|
||||
import asyncio
|
||||
result = asyncio.get_event_loop().run_until_complete(_run())
|
||||
assert result == "ok"
|
||||
|
||||
def test_open_raises_service_unavailable(self):
|
||||
cb = CircuitBreaker(name="test", failure_threshold=1)
|
||||
cb._on_failure(Exception("fail"))
|
||||
assert cb.state == CircuitState.OPEN
|
||||
|
||||
async def _run():
|
||||
await cb.call(AsyncMock(return_value="ok"))
|
||||
|
||||
import asyncio
|
||||
with pytest.raises(ServiceUnavailableException):
|
||||
asyncio.get_event_loop().run_until_complete(_run())
|
||||
|
||||
def test_half_open_after_recovery_timeout(self):
|
||||
cb = CircuitBreaker(name="test", failure_threshold=1, recovery_timeout=0.01)
|
||||
cb._on_failure(Exception("fail"))
|
||||
assert cb._state == CircuitState.OPEN
|
||||
time.sleep(0.02)
|
||||
assert cb.state == CircuitState.HALF_OPEN
|
||||
|
||||
def test_reset(self):
|
||||
cb = CircuitBreaker(name="test", failure_threshold=1)
|
||||
cb._on_failure(Exception("fail"))
|
||||
cb.reset()
|
||||
assert cb.state == CircuitState.CLOSED
|
||||
assert cb._failure_count == 0
|
||||
|
||||
def test_success_resets_failure_count(self):
|
||||
cb = CircuitBreaker(name="test", failure_threshold=5)
|
||||
cb._on_failure(Exception("fail"))
|
||||
cb._on_failure(Exception("fail"))
|
||||
assert cb._failure_count == 2
|
||||
cb._on_success()
|
||||
assert cb._failure_count == 0
|
||||
|
||||
def test_half_open_success_closes_circuit(self):
|
||||
cb = CircuitBreaker(name="test", failure_threshold=1, recovery_timeout=0.01)
|
||||
cb._on_failure(Exception("fail"))
|
||||
time.sleep(0.02)
|
||||
assert cb.state == CircuitState.HALF_OPEN
|
||||
cb._on_success()
|
||||
assert cb._state == CircuitState.CLOSED
|
||||
|
||||
def test_half_open_failure_reopens(self):
|
||||
cb = CircuitBreaker(name="test", failure_threshold=1, recovery_timeout=0.01)
|
||||
cb._on_failure(Exception("fail"))
|
||||
time.sleep(0.02)
|
||||
assert cb.state == CircuitState.HALF_OPEN
|
||||
cb._on_failure(Exception("fail again"))
|
||||
assert cb._state == CircuitState.OPEN
|
||||
|
||||
def test_get_status(self):
|
||||
cb = CircuitBreaker(name="test")
|
||||
status = cb.get_status()
|
||||
assert status["name"] == "test"
|
||||
assert status["state"] == "closed"
|
||||
|
||||
|
||||
class TestRetryPolicy:
|
||||
def test_default_values(self):
|
||||
policy = RetryPolicy()
|
||||
assert policy.max_attempts == 3
|
||||
assert policy.base_delay == 1.0
|
||||
assert policy.max_delay == 30.0
|
||||
|
||||
def test_custom_values(self):
|
||||
policy = RetryPolicy(max_attempts=5, base_delay=0.5, max_delay=10.0)
|
||||
assert policy.max_attempts == 5
|
||||
assert policy.base_delay == 0.5
|
||||
assert policy.max_delay == 10.0
|
||||
|
||||
|
||||
class TestWithRetry:
|
||||
@pytest.mark.asyncio
|
||||
async def test_success_first_try(self):
|
||||
call_count = 0
|
||||
|
||||
async def _success():
|
||||
nonlocal call_count
|
||||
call_count += 1
|
||||
return "ok"
|
||||
|
||||
policy = RetryPolicy(max_attempts=3)
|
||||
result = await with_retry(_success, policy=policy, connector_type="test", operation="test_op")
|
||||
assert result == "ok"
|
||||
assert call_count == 1
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_retry_on_failure_then_success(self):
|
||||
call_count = 0
|
||||
|
||||
async def _flaky():
|
||||
nonlocal call_count
|
||||
call_count += 1
|
||||
if call_count < 3:
|
||||
raise Exception("temp fail")
|
||||
return "ok"
|
||||
|
||||
policy = RetryPolicy(max_attempts=3, base_delay=0.01, max_delay=0.1)
|
||||
result = await with_retry(_flaky, policy=policy, connector_type="test", operation="flaky_op")
|
||||
assert result == "ok"
|
||||
assert call_count == 3
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_exhaust_retries(self):
|
||||
call_count = 0
|
||||
|
||||
async def _always_fail():
|
||||
nonlocal call_count
|
||||
call_count += 1
|
||||
raise Exception("always fail")
|
||||
|
||||
policy = RetryPolicy(max_attempts=2, base_delay=0.01, max_delay=0.1)
|
||||
with pytest.raises(Exception, match="always fail"):
|
||||
await with_retry(_always_fail, policy=policy, connector_type="test", operation="fail_op")
|
||||
assert call_count == 2
|
||||
|
||||
|
||||
class TestConnectorContext:
|
||||
def test_cache_key(self):
|
||||
ctx = ConnectorContext(
|
||||
connector_id=MagicMock(),
|
||||
pharmacy_id=MagicMock(),
|
||||
erp_system_id=MagicMock(),
|
||||
connector_type="farmatic",
|
||||
)
|
||||
key = ctx.cache_key("stock")
|
||||
assert "farmatic" in key
|
||||
assert "stock" in key
|
||||
|
||||
def test_get_config(self):
|
||||
ctx = ConnectorContext(
|
||||
connector_id=MagicMock(),
|
||||
pharmacy_id=MagicMock(),
|
||||
erp_system_id=MagicMock(),
|
||||
connector_type="mock",
|
||||
config={"base_url": "https://example.com"},
|
||||
)
|
||||
assert ctx.get_config("base_url") == "https://example.com"
|
||||
assert ctx.get_config("missing", "default") == "default"
|
||||
|
||||
def test_get_secret(self):
|
||||
ctx = ConnectorContext(
|
||||
connector_id=MagicMock(),
|
||||
pharmacy_id=MagicMock(),
|
||||
erp_system_id=MagicMock(),
|
||||
connector_type="mock",
|
||||
secrets={"api_key": "secret123"},
|
||||
)
|
||||
assert ctx.get_secret("api_key") == "secret123"
|
||||
assert ctx.get_secret("missing") is None
|
||||
|
||||
def test_require_secret_raises(self):
|
||||
ctx = ConnectorContext(
|
||||
connector_id=MagicMock(),
|
||||
pharmacy_id=MagicMock(),
|
||||
erp_system_id=MagicMock(),
|
||||
connector_type="mock",
|
||||
)
|
||||
with pytest.raises(ValueError, match="Required secret"):
|
||||
ctx.require_secret("missing")
|
||||
|
||||
def test_require_config_raises(self):
|
||||
ctx = ConnectorContext(
|
||||
connector_id=MagicMock(),
|
||||
pharmacy_id=MagicMock(),
|
||||
erp_system_id=MagicMock(),
|
||||
connector_type="mock",
|
||||
)
|
||||
with pytest.raises(ValueError, match="Required config"):
|
||||
ctx.require_config("missing")
|
||||
|
||||
|
||||
class TestBasicAuthHandler:
|
||||
@pytest.mark.asyncio
|
||||
async def test_apply_basic_auth(self):
|
||||
import httpx
|
||||
|
||||
handler = BasicAuthHandler()
|
||||
ctx = ConnectorContext(
|
||||
connector_id=MagicMock(),
|
||||
pharmacy_id=MagicMock(),
|
||||
erp_system_id=MagicMock(),
|
||||
connector_type="test",
|
||||
config={"username": "user1", "password": "pass1"},
|
||||
)
|
||||
request = httpx.Request("GET", "https://example.com")
|
||||
result = await handler.apply(request, ctx)
|
||||
assert "Basic" in result.headers["Authorization"]
|
||||
|
||||
|
||||
class TestBearerAuthHandler:
|
||||
@pytest.mark.asyncio
|
||||
async def test_apply_bearer_auth(self):
|
||||
import httpx
|
||||
|
||||
handler = BearerAuthHandler()
|
||||
ctx = ConnectorContext(
|
||||
connector_id=MagicMock(),
|
||||
pharmacy_id=MagicMock(),
|
||||
erp_system_id=MagicMock(),
|
||||
connector_type="test",
|
||||
config={"access_token": "token123"},
|
||||
)
|
||||
request = httpx.Request("GET", "https://example.com")
|
||||
result = await handler.apply(request, ctx)
|
||||
assert result.headers["Authorization"] == "Bearer token123"
|
||||
|
||||
|
||||
class TestAPIKeyAuthHandler:
|
||||
@pytest.mark.asyncio
|
||||
async def test_apply_api_key_auth(self):
|
||||
import httpx
|
||||
|
||||
handler = APIKeyAuthHandler(header_name="X-Custom-Key")
|
||||
ctx = ConnectorContext(
|
||||
connector_id=MagicMock(),
|
||||
pharmacy_id=MagicMock(),
|
||||
erp_system_id=MagicMock(),
|
||||
connector_type="test",
|
||||
secrets={"api_key": "key123"},
|
||||
)
|
||||
request = httpx.Request("GET", "https://example.com")
|
||||
result = await handler.apply(request, ctx)
|
||||
assert result.headers["X-Custom-Key"] == "key123"
|
||||
Reference in New Issue
Block a user