From 89099b903dfde61ec0b639ab8488d9b1fabfcff6 Mon Sep 17 00:00:00 2001 From: anti Date: Thu, 16 Apr 2026 01:39:04 -0400 Subject: [PATCH] fix: resolve schemathesis and live test failures - Add 403 response to all RBAC-gated endpoints (schemathesis UndefinedStatusCode) - Add 400 response to all endpoints accepting JSON bodies (malformed input) - Add required 'title' field to schemathesis.toml for schemathesis 4.15+ - Add xdist_group markers to live tests with module-scoped fixtures to prevent xdist from distributing them across workers (fixture isolation) --- decnet/web/router/attackers/api_get_attacker_commands.py | 1 + decnet/web/router/attackers/api_get_attacker_detail.py | 1 + decnet/web/router/attackers/api_get_attackers.py | 1 + decnet/web/router/bounty/api_get_bounties.py | 2 +- decnet/web/router/config/api_get_config.py | 1 + decnet/web/router/config/api_manage_users.py | 3 +++ decnet/web/router/config/api_update_config.py | 2 ++ decnet/web/router/fleet/api_get_deckies.py | 2 +- decnet/web/router/health/api_get_health.py | 1 + decnet/web/router/logs/api_get_histogram.py | 2 +- decnet/web/router/logs/api_get_logs.py | 2 +- decnet/web/router/stats/api_get_stats.py | 2 +- decnet/web/router/stream/api_stream_events.py | 1 + schemathesis.toml | 3 +++ tests/live/test_health_live.py | 1 + tests/live/test_service_isolation_live.py | 6 ++++++ 16 files changed, 26 insertions(+), 5 deletions(-) diff --git a/decnet/web/router/attackers/api_get_attacker_commands.py b/decnet/web/router/attackers/api_get_attacker_commands.py index d2afb8a..c24cdb9 100644 --- a/decnet/web/router/attackers/api_get_attacker_commands.py +++ b/decnet/web/router/attackers/api_get_attacker_commands.py @@ -13,6 +13,7 @@ router = APIRouter() tags=["Attacker Profiles"], responses={ 401: {"description": "Could not validate credentials"}, + 403: {"description": "Insufficient permissions"}, 404: {"description": "Attacker not found"}, }, ) diff --git a/decnet/web/router/attackers/api_get_attacker_detail.py b/decnet/web/router/attackers/api_get_attacker_detail.py index cd29ea1..dcc9ebd 100644 --- a/decnet/web/router/attackers/api_get_attacker_detail.py +++ b/decnet/web/router/attackers/api_get_attacker_detail.py @@ -13,6 +13,7 @@ router = APIRouter() tags=["Attacker Profiles"], responses={ 401: {"description": "Could not validate credentials"}, + 403: {"description": "Insufficient permissions"}, 404: {"description": "Attacker not found"}, }, ) diff --git a/decnet/web/router/attackers/api_get_attackers.py b/decnet/web/router/attackers/api_get_attackers.py index 958676f..6f3daa5 100644 --- a/decnet/web/router/attackers/api_get_attackers.py +++ b/decnet/web/router/attackers/api_get_attackers.py @@ -15,6 +15,7 @@ router = APIRouter() tags=["Attacker Profiles"], responses={ 401: {"description": "Could not validate credentials"}, + 403: {"description": "Insufficient permissions"}, 422: {"description": "Validation error"}, }, ) diff --git a/decnet/web/router/bounty/api_get_bounties.py b/decnet/web/router/bounty/api_get_bounties.py index 04dc784..62ac063 100644 --- a/decnet/web/router/bounty/api_get_bounties.py +++ b/decnet/web/router/bounty/api_get_bounties.py @@ -10,7 +10,7 @@ router = APIRouter() @router.get("/bounty", response_model=BountyResponse, tags=["Bounty Vault"], - responses={401: {"description": "Could not validate credentials"}, 422: {"description": "Validation error"}},) + responses={401: {"description": "Could not validate credentials"}, 403: {"description": "Insufficient permissions"}, 422: {"description": "Validation error"}},) @_traced("api.get_bounties") async def get_bounties( limit: int = Query(50, ge=1, le=1000), diff --git a/decnet/web/router/config/api_get_config.py b/decnet/web/router/config/api_get_config.py index 495dc4c..a0d5369 100644 --- a/decnet/web/router/config/api_get_config.py +++ b/decnet/web/router/config/api_get_config.py @@ -16,6 +16,7 @@ _DEFAULT_MUTATION_INTERVAL = "30m" tags=["Configuration"], responses={ 401: {"description": "Could not validate credentials"}, + 403: {"description": "Insufficient permissions"}, }, ) @_traced("api.get_config") diff --git a/decnet/web/router/config/api_manage_users.py b/decnet/web/router/config/api_manage_users.py index 2aaf666..12263ab 100644 --- a/decnet/web/router/config/api_manage_users.py +++ b/decnet/web/router/config/api_manage_users.py @@ -19,6 +19,7 @@ router = APIRouter() "/config/users", tags=["Configuration"], responses={ + 400: {"description": "Bad Request (e.g. malformed JSON)"}, 401: {"description": "Could not validate credentials"}, 403: {"description": "Admin access required"}, 409: {"description": "Username already exists"}, @@ -77,6 +78,7 @@ async def api_delete_user( "/config/users/{user_uuid}/role", tags=["Configuration"], responses={ + 400: {"description": "Bad Request (e.g. malformed JSON)"}, 401: {"description": "Could not validate credentials"}, 403: {"description": "Admin access required / cannot change own role"}, 404: {"description": "User not found"}, @@ -104,6 +106,7 @@ async def api_update_user_role( "/config/users/{user_uuid}/reset-password", tags=["Configuration"], responses={ + 400: {"description": "Bad Request (e.g. malformed JSON)"}, 401: {"description": "Could not validate credentials"}, 403: {"description": "Admin access required"}, 404: {"description": "User not found"}, diff --git a/decnet/web/router/config/api_update_config.py b/decnet/web/router/config/api_update_config.py index 53826e5..a7feee3 100644 --- a/decnet/web/router/config/api_update_config.py +++ b/decnet/web/router/config/api_update_config.py @@ -11,6 +11,7 @@ router = APIRouter() "/config/deployment-limit", tags=["Configuration"], responses={ + 400: {"description": "Bad Request (e.g. malformed JSON)"}, 401: {"description": "Could not validate credentials"}, 403: {"description": "Admin access required"}, 422: {"description": "Validation error"}, @@ -29,6 +30,7 @@ async def api_update_deployment_limit( "/config/global-mutation-interval", tags=["Configuration"], responses={ + 400: {"description": "Bad Request (e.g. malformed JSON)"}, 401: {"description": "Could not validate credentials"}, 403: {"description": "Admin access required"}, 422: {"description": "Validation error"}, diff --git a/decnet/web/router/fleet/api_get_deckies.py b/decnet/web/router/fleet/api_get_deckies.py index 6d933fa..1d81a3a 100644 --- a/decnet/web/router/fleet/api_get_deckies.py +++ b/decnet/web/router/fleet/api_get_deckies.py @@ -9,7 +9,7 @@ router = APIRouter() @router.get("/deckies", tags=["Fleet Management"], - responses={401: {"description": "Could not validate credentials"}, 422: {"description": "Validation error"}},) + responses={401: {"description": "Could not validate credentials"}, 403: {"description": "Insufficient permissions"}, 422: {"description": "Validation error"}},) @_traced("api.get_deckies") async def get_deckies(user: dict = Depends(require_viewer)) -> list[dict[str, Any]]: return await repo.get_deckies() diff --git a/decnet/web/router/health/api_get_health.py b/decnet/web/router/health/api_get_health.py index 6beb271..be2c390 100644 --- a/decnet/web/router/health/api_get_health.py +++ b/decnet/web/router/health/api_get_health.py @@ -18,6 +18,7 @@ _OPTIONAL_SERVICES = {"sniffer_worker"} tags=["Observability"], responses={ 401: {"description": "Could not validate credentials"}, + 403: {"description": "Insufficient permissions"}, 503: {"model": HealthResponse, "description": "System unhealthy"}, }, ) diff --git a/decnet/web/router/logs/api_get_histogram.py b/decnet/web/router/logs/api_get_histogram.py index 4ea54e5..28c21b2 100644 --- a/decnet/web/router/logs/api_get_histogram.py +++ b/decnet/web/router/logs/api_get_histogram.py @@ -9,7 +9,7 @@ router = APIRouter() @router.get("/logs/histogram", tags=["Logs"], - responses={401: {"description": "Could not validate credentials"}, 422: {"description": "Validation error"}},) + responses={401: {"description": "Could not validate credentials"}, 403: {"description": "Insufficient permissions"}, 422: {"description": "Validation error"}},) @_traced("api.get_logs_histogram") async def get_logs_histogram( search: Optional[str] = None, diff --git a/decnet/web/router/logs/api_get_logs.py b/decnet/web/router/logs/api_get_logs.py index 68d9b11..46c5a14 100644 --- a/decnet/web/router/logs/api_get_logs.py +++ b/decnet/web/router/logs/api_get_logs.py @@ -10,7 +10,7 @@ router = APIRouter() @router.get("/logs", response_model=LogsResponse, tags=["Logs"], - responses={401: {"description": "Could not validate credentials"}, 422: {"description": "Validation error"}}) + responses={401: {"description": "Could not validate credentials"}, 403: {"description": "Insufficient permissions"}, 422: {"description": "Validation error"}}) @_traced("api.get_logs") async def get_logs( limit: int = Query(50, ge=1, le=1000), diff --git a/decnet/web/router/stats/api_get_stats.py b/decnet/web/router/stats/api_get_stats.py index 21ae610..a1739b7 100644 --- a/decnet/web/router/stats/api_get_stats.py +++ b/decnet/web/router/stats/api_get_stats.py @@ -10,7 +10,7 @@ router = APIRouter() @router.get("/stats", response_model=StatsResponse, tags=["Observability"], - responses={401: {"description": "Could not validate credentials"}, 422: {"description": "Validation error"}},) + responses={401: {"description": "Could not validate credentials"}, 403: {"description": "Insufficient permissions"}, 422: {"description": "Validation error"}},) @_traced("api.get_stats") async def get_stats(user: dict = Depends(require_viewer)) -> dict[str, Any]: return await repo.get_stats_summary() diff --git a/decnet/web/router/stream/api_stream_events.py b/decnet/web/router/stream/api_stream_events.py index 6d9f910..6e028ac 100644 --- a/decnet/web/router/stream/api_stream_events.py +++ b/decnet/web/router/stream/api_stream_events.py @@ -50,6 +50,7 @@ def _build_trace_links(logs: list[dict]) -> list: "description": "Real-time Server-Sent Events (SSE) stream" }, 401: {"description": "Could not validate credentials"}, + 403: {"description": "Insufficient permissions"}, 422: {"description": "Validation error"} }, ) diff --git a/schemathesis.toml b/schemathesis.toml index e1f5852..1091856 100644 --- a/schemathesis.toml +++ b/schemathesis.toml @@ -1,3 +1,6 @@ +[[project]] +title = "DECNET API" +continue-on-failure = true request-timeout = 5.0 [[operations]] diff --git a/tests/live/test_health_live.py b/tests/live/test_health_live.py index 275a352..398af86 100644 --- a/tests/live/test_health_live.py +++ b/tests/live/test_health_live.py @@ -97,6 +97,7 @@ async def token(live_client): @pytest.mark.live +@pytest.mark.xdist_group("health_live") class TestHealthLive: """Live integration tests — real DB, real Docker check, real task state.""" diff --git a/tests/live/test_service_isolation_live.py b/tests/live/test_service_isolation_live.py index d14824d..be2f12e 100644 --- a/tests/live/test_service_isolation_live.py +++ b/tests/live/test_service_isolation_live.py @@ -128,6 +128,7 @@ async def token(live_client): @pytest.mark.live +@pytest.mark.xdist_group("service_isolation_live") class TestCollectorLiveIsolation: """Real collector behaviour against the actual Docker daemon.""" @@ -203,6 +204,7 @@ class TestCollectorLiveIsolation: @pytest.mark.live +@pytest.mark.xdist_group("service_isolation_live") class TestIngesterLiveIsolation: """Real ingester against real DB and real filesystem.""" @@ -312,6 +314,7 @@ class TestIngesterLiveIsolation: @pytest.mark.live +@pytest.mark.xdist_group("service_isolation_live") class TestAttackerWorkerLiveIsolation: """Real attacker worker against real DB.""" @@ -360,6 +363,7 @@ class TestAttackerWorkerLiveIsolation: @pytest.mark.live +@pytest.mark.xdist_group("service_isolation_live") class TestSnifferLiveIsolation: """Real sniffer against the actual host network stack.""" @@ -396,6 +400,7 @@ class TestSnifferLiveIsolation: @pytest.mark.live +@pytest.mark.xdist_group("service_isolation_live") class TestApiLifespanLiveIsolation: """Real API lifespan against real DB and real host state.""" @@ -442,6 +447,7 @@ class TestApiLifespanLiveIsolation: @pytest.mark.live +@pytest.mark.xdist_group("service_isolation_live") class TestCascadeLiveIsolation: """Verify that real component failures do not cascade."""