First implementation API for PIP (Proyecto: Pharmacy Integration Platform)
Run Tests on Branches / Backend Tests (push) Successful in 3m32s
Run Tests on Branches / Frontend Tests (push) Successful in 3m27s

This commit is contained in:
Antoni Nuñez Romeu
2026-07-01 16:46:52 +02:00
parent d935db4b3e
commit c5df0ec0b2
121 changed files with 9575 additions and 0 deletions
View File
+94
View File
@@ -0,0 +1,94 @@
import pytest
from httpx import AsyncClient
from src.services.auth.security import create_access_token, hash_password, verify_password
from src.domain.entities import UserRole
class TestPasswordHashing:
def test_hash_and_verify(self):
password = "SecurePass123!"
hashed = hash_password(password)
assert hashed != password
assert verify_password(password, hashed)
def test_verify_wrong_password(self):
hashed = hash_password("CorrectPass123!")
assert not verify_password("WrongPass123!", hashed)
class TestJWT:
def test_create_access_token(self):
token = create_access_token(
subject="test-user-id",
role=UserRole.ADMIN,
scopes=["*"],
)
assert isinstance(token, str)
assert len(token) > 0
def test_decode_access_token(self):
from src.services.auth.security import decode_token, validate_token_type, TokenData
token = create_access_token(
subject="test-user-id",
role=UserRole.VIEWER,
scopes=["pharmacies:read"],
)
payload = decode_token(token)
assert payload["sub"] == "test-user-id"
assert payload["role"] == "viewer"
assert payload["type"] == "access"
validated = validate_token_type(payload, "access")
assert validated is not None
def test_invalid_token_raises(self):
from src.core.exceptions import UnauthorizedException
from src.services.auth.security import decode_token
with pytest.raises(UnauthorizedException):
decode_token("invalid.token.here")
class TestRBAC:
def test_superadmin_can_access_all(self):
from src.services.auth.security import RBACGuard, TokenData
token_data = TokenData({"sub": "1", "role": "superadmin", "scopes": ["*"], "type": "access", "iss": "pip-platform", "jti": "test"})
assert RBACGuard.has_role(token_data, UserRole.ADMIN)
assert RBACGuard.has_role(token_data, UserRole.VIEWER)
assert RBACGuard.has_role(token_data, UserRole.OPERATOR)
def test_viewer_cannot_access_admin(self):
from src.services.auth.security import RBACGuard, TokenData
token_data = TokenData({"sub": "2", "role": "viewer", "scopes": [], "type": "access", "iss": "pip-platform", "jti": "test"})
assert not RBACGuard.has_role(token_data, UserRole.ADMIN)
def test_wildcard_scope_grants_all(self):
from src.services.auth.security import RBACGuard, TokenData
token_data = TokenData({"sub": "1", "role": "superadmin", "scopes": ["*"], "type": "access", "iss": "pip-platform", "jti": "test"})
assert RBACGuard.has_scope(token_data, "pharmacies:read")
assert RBACGuard.has_scope(token_data, "pharmacies:write")
assert RBACGuard.has_scope(token_data, "anything:anything")
def test_specific_scope(self):
from src.services.auth.security import RBACGuard, TokenData
token_data = TokenData({"sub": "2", "role": "viewer", "scopes": ["pharmacies:read"], "type": "access", "iss": "pip-platform", "jti": "test"})
assert RBACGuard.has_scope(token_data, "pharmacies:read")
assert not RBACGuard.has_scope(token_data, "pharmacies:write")
class TestAPIKey:
def test_generate_api_key(self):
from src.services.auth.security import generate_api_key, verify_api_key
raw_key, prefix, key_hash = generate_api_key()
assert len(raw_key) > 20
assert len(prefix) == 8
assert raw_key[:8] == prefix
assert verify_api_key(raw_key, key_hash)
assert not verify_api_key("wrong-key", key_hash)
@@ -0,0 +1,102 @@
from uuid import uuid4
import pytest
from src.connectors.registry import ConnectorRegistry, _BUILTIN_CONNECTORS
from src.connectors.mock.mock_connector import MockConnector
from src.connectors.base import BaseInventoryConnector
from src.core.exceptions import ConnectorException
@pytest.fixture
def registry() -> ConnectorRegistry:
ConnectorRegistry._instance = None
reg = ConnectorRegistry.get_registry()
return reg
class TestConnectorRegistrySingleton:
def test_singleton(self, registry: ConnectorRegistry):
reg2 = ConnectorRegistry.get_registry()
assert reg2 is registry
def test_new_instance_after_reset(self, registry: ConnectorRegistry):
ConnectorRegistry._instance = None
reg2 = ConnectorRegistry.get_registry()
assert reg2 is not registry
class TestConnectorRegistryBuiltinTypes:
def test_builtin_connectors_include_mock(self):
assert "mock" in _BUILTIN_CONNECTORS
def test_builtin_connectors_include_farmatic(self):
assert "farmatic" in _BUILTIN_CONNECTORS
def test_builtin_connectors_include_custom_rest(self):
assert "custom_rest" in _BUILTIN_CONNECTORS
class TestConnectorRegistryGetClass:
def test_get_mock_connector_class(self, registry: ConnectorRegistry):
cls = registry.get_connector_class("mock")
assert cls is MockConnector
def test_get_unknown_type_raises(self, registry: ConnectorRegistry):
with pytest.raises(ConnectorException, match="No connector registered"):
registry.get_connector_class("nonexistent_type")
class TestConnectorRegistryCreateInstance:
def test_create_mock_instance(self, registry: ConnectorRegistry):
instance = registry.create_instance("mock")
assert isinstance(instance, BaseInventoryConnector)
assert instance.connector_type == "mock"
def test_create_instance_with_config(self, registry: ConnectorRegistry):
instance = registry.create_instance("mock", {"simulate_latency": False, "error_rate": 0.0})
assert instance._simulate_latency is False
def test_create_unknown_type_raises(self, registry: ConnectorRegistry):
with pytest.raises(ConnectorException):
registry.create_instance("nonexistent")
class TestConnectorRegistryInstanceCache:
def test_get_or_create_returns_cached(self, registry: ConnectorRegistry):
cid = uuid4()
inst1 = registry.get_or_create_instance(cid, "mock")
inst2 = registry.get_or_create_instance(cid, "mock")
assert inst1 is inst2
def test_remove_instance(self, registry: ConnectorRegistry):
cid = uuid4()
registry.get_or_create_instance(cid, "mock")
registry.remove_instance(cid)
assert cid not in registry._instances
def test_remove_nonexistent_instance_no_error(self, registry: ConnectorRegistry):
registry.remove_instance(uuid4())
class TestConnectorRegistryListTypes:
def test_list_registered_types(self, registry: ConnectorRegistry):
types = registry.list_registered_types()
assert "mock" in types
assert "farmatic" in types
assert "custom_rest" in types
def test_register_custom_type(self, registry: ConnectorRegistry):
mock = MockConnector()
mock.connector_type = "test_custom"
registry.register("test_custom", mock)
types = registry.list_registered_types()
assert "test_custom" in types
def test_unregister_type(self, registry: ConnectorRegistry):
mock = MockConnector()
mock.connector_type = "temp_type"
registry.register("temp_type", mock)
registry.unregister("temp_type")
types = registry.list_registered_types()
assert "temp_type" not in types
@@ -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"
+111
View File
@@ -0,0 +1,111 @@
import pytest
from src.domain.entities import (
PharmacyEntity,
PharmacyStatus,
MedicationEntity,
ERPType,
SyncStrategy,
UserRole,
ConnectorEntity,
ConnectorStatus,
ReservationEntity,
ReservationStatus,
AuditLogEntity,
)
from uuid import uuid4
class TestPharmacyEntity:
def test_create_pharmacy(self):
entity = PharmacyEntity(
code="PH-001",
name="Farmacia Central",
address="Calle Mayor 1",
city="Madrid",
status=PharmacyStatus.ACTIVE,
erp_type=ERPType.FARMATIC,
sync_strategy=SyncStrategy.INCREMENTAL,
)
assert entity.code == "PH-001"
assert entity.name == "Farmacia Central"
assert entity.status == PharmacyStatus.ACTIVE
assert entity.erp_type == ERPType.FARMATIC
assert entity.sync_strategy == SyncStrategy.INCREMENTAL
def test_pharmacy_default_values(self):
entity = PharmacyEntity(code="PH-002", name="Test")
assert entity.country == "ES"
assert entity.status == PharmacyStatus.PENDING
assert entity.sync_strategy == SyncStrategy.INCREMENTAL
assert entity.sync_interval_minutes == 30
def test_pharmacy_status_enum(self):
assert PharmacyStatus.ACTIVE.value == "active"
assert PharmacyStatus.INACTIVE.value == "inactive"
assert PharmacyStatus.SUSPENDED.value == "suspended"
assert PharmacyStatus.PENDING.value == "pending"
class TestMedicationEntity:
def test_create_medication(self):
entity = MedicationEntity(
nregistro="12345",
name="Ibuprofeno 400mg",
active_ingredient="Ibuprofeno",
dosage="400mg",
form="Comprimidos",
prescription_required=False,
)
assert entity.nregistro == "12345"
assert entity.name == "Ibuprofeno 400mg"
assert entity.prescription_required is False
class TestConnectorEntity:
def test_create_connector(self):
entity = ConnectorEntity(
erp_system_id=uuid4(),
pharmacy_id=uuid4(),
name="Farmatic Connector",
connector_type=ERPType.FARMATIC,
)
assert entity.connector_type == ERPType.FARMATIC
assert entity.version == "1.0.0"
assert entity.status == ConnectorStatus.PENDING
class TestReservationEntity:
def test_create_reservation(self):
entity = ReservationEntity(
pharmacy_id=uuid4(),
medication_id=uuid4(),
quantity=5,
idempotency_key="unique-key-123",
)
assert entity.quantity == 5
assert entity.status == ReservationStatus.PENDING
assert entity.idempotency_key == "unique-key-123"
class TestAuditLogEntity:
def test_create_audit_log(self):
entity = AuditLogEntity(
actor_id=uuid4(),
actor_type="technical_user",
action="pharmacy.create",
resource_type="pharmacy",
resource_id="PH-001",
ip_address="192.168.1.1",
)
assert entity.action == "pharmacy.create"
assert entity.resource_type == "pharmacy"
def test_enums(self):
assert ERPType.FARMATIC.value == "farmatic"
assert ERPType.NIXFARMA.value == "nixfarma"
assert ERPType.UNYCOP.value == "unycop"
assert ERPType.MOCK.value == "mock"
assert SyncStrategy.FULL.value == "full"
assert SyncStrategy.HYBRID.value == "hybrid"
assert UserRole.SUPERADMIN.value == "superadmin"
@@ -0,0 +1,66 @@
import pytest
from src.core.exceptions import (
PIPException,
NotFoundException,
ConflictException,
ValidationException,
UnauthorizedException,
ForbiddenException,
RateLimitException,
ServiceUnavailableException,
ConnectorException,
SyncException,
)
class TestPIPException:
def test_base_exception(self):
exc = PIPException("Something failed", code="TEST_ERROR", status_code=500)
assert exc.message == "Something failed"
assert exc.code == "TEST_ERROR"
assert exc.status_code == 500
assert "timestamp" in exc.to_dict()["error"]
def test_not_found(self):
exc = NotFoundException("Pharmacy", "PH-001")
assert exc.status_code == 404
assert exc.code == "NOT_FOUND"
d = exc.to_dict()
assert "Pharmacy" in d["error"]["message"]
def test_conflict(self):
exc = ConflictException("Duplicate")
assert exc.status_code == 409
def test_validation(self):
exc = ValidationException("Invalid input")
assert exc.status_code == 422
def test_unauthorized(self):
exc = UnauthorizedException()
assert exc.status_code == 401
def test_forbidden(self):
exc = ForbiddenException(required_role="admin")
assert exc.status_code == 403
assert exc.details["required_role"] == "admin"
def test_rate_limit(self):
exc = RateLimitException()
assert exc.status_code == 429
def test_service_unavailable(self):
exc = ServiceUnavailableException("redis")
assert exc.status_code == 503
assert "redis" in exc.details["service"]
def test_connector_error(self):
exc = ConnectorException("farmatic", "Connection refused")
assert exc.status_code == 502
assert exc.details["connector_type"] == "farmatic"
def test_sync_error(self):
exc = SyncException("pharmacy-1", "Timeout")
assert exc.status_code == 502
assert exc.details["pharmacy_id"] == "pharmacy-1"
@@ -0,0 +1,138 @@
import asyncio
from uuid import UUID, uuid4
import pytest
from src.connectors.mock.mock_connector import MockConnector
from src.connectors.base import ConnectorResult
from src.core.exceptions import ConnectorException
@pytest.fixture
def mock_connector() -> MockConnector:
return MockConnector()
@pytest.fixture
def pharmacy_id() -> UUID:
return uuid4()
@pytest.fixture
def medication_id() -> UUID:
return uuid4()
class TestMockConnectorConfiguration:
def test_default_configuration(self, mock_connector: MockConnector):
assert mock_connector._simulate_latency is True
assert mock_connector._error_rate == 0.0
def test_configure_latency(self, mock_connector: MockConnector):
mock_connector.configure({"simulate_latency": False, "latency_range_ms": [5, 10]})
assert mock_connector._simulate_latency is False
assert mock_connector._latency_range_ms == (5, 10)
def test_configure_error_rate(self, mock_connector: MockConnector):
mock_connector.configure({"error_rate": 0.5})
assert mock_connector._error_rate == 0.5
class TestMockConnectorGetStock:
@pytest.mark.asyncio
async def test_get_stock_all(self, mock_connector: MockConnector, pharmacy_id: UUID):
mock_connector.configure({"simulate_latency": False})
result = await mock_connector.get_stock(pharmacy_id)
assert result.success is True
assert "items" in result.data
assert result.data["total"] == 10
@pytest.mark.asyncio
async def test_get_stock_by_nregistro(self, mock_connector: MockConnector, pharmacy_id: UUID):
mock_connector.configure({"simulate_latency": False})
result = await mock_connector.get_stock(pharmacy_id, nregistro="00001")
assert result.success is True
assert result.data["nregistro"] == "00001"
assert result.data["name"] == "Ibuprofeno 600mg"
@pytest.mark.asyncio
async def test_get_stock_not_found(self, mock_connector: MockConnector, pharmacy_id: UUID):
mock_connector.configure({"simulate_latency": False})
result = await mock_connector.get_stock(pharmacy_id, nregistro="99999")
assert result.success is False
assert "not found" in result.error
class TestMockConnectorReserve:
@pytest.mark.asyncio
async def test_reserve(self, mock_connector: MockConnector, pharmacy_id: UUID, medication_id: UUID):
mock_connector.configure({"simulate_latency": False})
result = await mock_connector.reserve(pharmacy_id, medication_id, 5)
assert result.success is True
assert result.data["status"] == "confirmed"
assert result.data["quantity"] == 5
@pytest.mark.asyncio
async def test_reserve_idempotency(self, mock_connector: MockConnector, pharmacy_id: UUID, medication_id: UUID):
mock_connector.configure({"simulate_latency": False})
key = "idem-key-123"
result1 = await mock_connector.reserve(pharmacy_id, medication_id, 3, idempotency_key=key)
result2 = await mock_connector.reserve(pharmacy_id, medication_id, 3, idempotency_key=key)
assert result1.success is True
assert result2.success is True
class TestMockConnectorCancelReservation:
@pytest.mark.asyncio
async def test_cancel_reservation(self, mock_connector: MockConnector, pharmacy_id: UUID):
mock_connector.configure({"simulate_latency": False})
reserve_result = await mock_connector.reserve(pharmacy_id, uuid4(), 2)
reservation_id = reserve_result.data["reservation_id"]
result = await mock_connector.cancel_reservation(pharmacy_id, reservation_id)
assert result.success is True
assert result.data["status"] == "cancelled"
@pytest.mark.asyncio
async def test_cancel_nonexistent_reservation(self, mock_connector: MockConnector, pharmacy_id: UUID):
mock_connector.configure({"simulate_latency": False})
result = await mock_connector.cancel_reservation(pharmacy_id, "nonexistent-id")
assert result.success is False
class TestMockConnectorPharmacyInfo:
@pytest.mark.asyncio
async def test_pharmacy_information(self, mock_connector: MockConnector, pharmacy_id: UUID):
mock_connector.configure({"simulate_latency": False})
result = await mock_connector.pharmacy_information(pharmacy_id)
assert result.success is True
assert "FARM-001" in result.data["code"]
class TestMockConnectorSynchronize:
@pytest.mark.asyncio
async def test_synchronize(self, mock_connector: MockConnector, pharmacy_id: UUID):
mock_connector.configure({"simulate_latency": False, "error_rate": 0.0})
result = await mock_connector.synchronize(pharmacy_id, strategy="full")
assert result.success is True
assert result.data["items_total"] == 10
assert result.data["items_failed"] == 0
class TestMockConnectorHeartbeat:
@pytest.mark.asyncio
async def test_heartbeat(self, mock_connector: MockConnector, pharmacy_id: UUID):
result = await mock_connector.heartbeat(pharmacy_id)
assert result.success is True
assert result.data["status"] == "healthy"
assert result.data["connector_type"] == "mock"
class TestMockConnectorErrorSimulation:
def test_high_error_rate_triggers_exception(self, mock_connector: MockConnector):
mock_connector.configure({"error_rate": 1.0})
with pytest.raises(ConnectorException):
mock_connector._maybe_error()
def test_zero_error_rate_no_exception(self, mock_connector: MockConnector):
mock_connector.configure({"error_rate": 0.0})
mock_connector._maybe_error()