Restructure with Turborepo
Run Tests on Branches / Backend Tests (push) Successful in 3m38s
Run Tests on Branches / Frontend Tests (push) Successful in 3m28s

This commit is contained in:
Antoni Nuñez Romeu
2026-07-06 15:51:53 +02:00
parent f66cafbbc3
commit 190b3d163d
277 changed files with 53253 additions and 0 deletions
@@ -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"