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)
This commit is contained in:
@@ -13,6 +13,7 @@ router = APIRouter()
|
|||||||
tags=["Attacker Profiles"],
|
tags=["Attacker Profiles"],
|
||||||
responses={
|
responses={
|
||||||
401: {"description": "Could not validate credentials"},
|
401: {"description": "Could not validate credentials"},
|
||||||
|
403: {"description": "Insufficient permissions"},
|
||||||
404: {"description": "Attacker not found"},
|
404: {"description": "Attacker not found"},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ router = APIRouter()
|
|||||||
tags=["Attacker Profiles"],
|
tags=["Attacker Profiles"],
|
||||||
responses={
|
responses={
|
||||||
401: {"description": "Could not validate credentials"},
|
401: {"description": "Could not validate credentials"},
|
||||||
|
403: {"description": "Insufficient permissions"},
|
||||||
404: {"description": "Attacker not found"},
|
404: {"description": "Attacker not found"},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ router = APIRouter()
|
|||||||
tags=["Attacker Profiles"],
|
tags=["Attacker Profiles"],
|
||||||
responses={
|
responses={
|
||||||
401: {"description": "Could not validate credentials"},
|
401: {"description": "Could not validate credentials"},
|
||||||
|
403: {"description": "Insufficient permissions"},
|
||||||
422: {"description": "Validation error"},
|
422: {"description": "Validation error"},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ router = APIRouter()
|
|||||||
|
|
||||||
|
|
||||||
@router.get("/bounty", response_model=BountyResponse, tags=["Bounty Vault"],
|
@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")
|
@_traced("api.get_bounties")
|
||||||
async def get_bounties(
|
async def get_bounties(
|
||||||
limit: int = Query(50, ge=1, le=1000),
|
limit: int = Query(50, ge=1, le=1000),
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ _DEFAULT_MUTATION_INTERVAL = "30m"
|
|||||||
tags=["Configuration"],
|
tags=["Configuration"],
|
||||||
responses={
|
responses={
|
||||||
401: {"description": "Could not validate credentials"},
|
401: {"description": "Could not validate credentials"},
|
||||||
|
403: {"description": "Insufficient permissions"},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@_traced("api.get_config")
|
@_traced("api.get_config")
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ router = APIRouter()
|
|||||||
"/config/users",
|
"/config/users",
|
||||||
tags=["Configuration"],
|
tags=["Configuration"],
|
||||||
responses={
|
responses={
|
||||||
|
400: {"description": "Bad Request (e.g. malformed JSON)"},
|
||||||
401: {"description": "Could not validate credentials"},
|
401: {"description": "Could not validate credentials"},
|
||||||
403: {"description": "Admin access required"},
|
403: {"description": "Admin access required"},
|
||||||
409: {"description": "Username already exists"},
|
409: {"description": "Username already exists"},
|
||||||
@@ -77,6 +78,7 @@ async def api_delete_user(
|
|||||||
"/config/users/{user_uuid}/role",
|
"/config/users/{user_uuid}/role",
|
||||||
tags=["Configuration"],
|
tags=["Configuration"],
|
||||||
responses={
|
responses={
|
||||||
|
400: {"description": "Bad Request (e.g. malformed JSON)"},
|
||||||
401: {"description": "Could not validate credentials"},
|
401: {"description": "Could not validate credentials"},
|
||||||
403: {"description": "Admin access required / cannot change own role"},
|
403: {"description": "Admin access required / cannot change own role"},
|
||||||
404: {"description": "User not found"},
|
404: {"description": "User not found"},
|
||||||
@@ -104,6 +106,7 @@ async def api_update_user_role(
|
|||||||
"/config/users/{user_uuid}/reset-password",
|
"/config/users/{user_uuid}/reset-password",
|
||||||
tags=["Configuration"],
|
tags=["Configuration"],
|
||||||
responses={
|
responses={
|
||||||
|
400: {"description": "Bad Request (e.g. malformed JSON)"},
|
||||||
401: {"description": "Could not validate credentials"},
|
401: {"description": "Could not validate credentials"},
|
||||||
403: {"description": "Admin access required"},
|
403: {"description": "Admin access required"},
|
||||||
404: {"description": "User not found"},
|
404: {"description": "User not found"},
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ router = APIRouter()
|
|||||||
"/config/deployment-limit",
|
"/config/deployment-limit",
|
||||||
tags=["Configuration"],
|
tags=["Configuration"],
|
||||||
responses={
|
responses={
|
||||||
|
400: {"description": "Bad Request (e.g. malformed JSON)"},
|
||||||
401: {"description": "Could not validate credentials"},
|
401: {"description": "Could not validate credentials"},
|
||||||
403: {"description": "Admin access required"},
|
403: {"description": "Admin access required"},
|
||||||
422: {"description": "Validation error"},
|
422: {"description": "Validation error"},
|
||||||
@@ -29,6 +30,7 @@ async def api_update_deployment_limit(
|
|||||||
"/config/global-mutation-interval",
|
"/config/global-mutation-interval",
|
||||||
tags=["Configuration"],
|
tags=["Configuration"],
|
||||||
responses={
|
responses={
|
||||||
|
400: {"description": "Bad Request (e.g. malformed JSON)"},
|
||||||
401: {"description": "Could not validate credentials"},
|
401: {"description": "Could not validate credentials"},
|
||||||
403: {"description": "Admin access required"},
|
403: {"description": "Admin access required"},
|
||||||
422: {"description": "Validation error"},
|
422: {"description": "Validation error"},
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ router = APIRouter()
|
|||||||
|
|
||||||
|
|
||||||
@router.get("/deckies", tags=["Fleet Management"],
|
@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")
|
@_traced("api.get_deckies")
|
||||||
async def get_deckies(user: dict = Depends(require_viewer)) -> list[dict[str, Any]]:
|
async def get_deckies(user: dict = Depends(require_viewer)) -> list[dict[str, Any]]:
|
||||||
return await repo.get_deckies()
|
return await repo.get_deckies()
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ _OPTIONAL_SERVICES = {"sniffer_worker"}
|
|||||||
tags=["Observability"],
|
tags=["Observability"],
|
||||||
responses={
|
responses={
|
||||||
401: {"description": "Could not validate credentials"},
|
401: {"description": "Could not validate credentials"},
|
||||||
|
403: {"description": "Insufficient permissions"},
|
||||||
503: {"model": HealthResponse, "description": "System unhealthy"},
|
503: {"model": HealthResponse, "description": "System unhealthy"},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ router = APIRouter()
|
|||||||
|
|
||||||
|
|
||||||
@router.get("/logs/histogram", tags=["Logs"],
|
@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")
|
@_traced("api.get_logs_histogram")
|
||||||
async def get_logs_histogram(
|
async def get_logs_histogram(
|
||||||
search: Optional[str] = None,
|
search: Optional[str] = None,
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ router = APIRouter()
|
|||||||
|
|
||||||
|
|
||||||
@router.get("/logs", response_model=LogsResponse, tags=["Logs"],
|
@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")
|
@_traced("api.get_logs")
|
||||||
async def get_logs(
|
async def get_logs(
|
||||||
limit: int = Query(50, ge=1, le=1000),
|
limit: int = Query(50, ge=1, le=1000),
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ router = APIRouter()
|
|||||||
|
|
||||||
|
|
||||||
@router.get("/stats", response_model=StatsResponse, tags=["Observability"],
|
@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")
|
@_traced("api.get_stats")
|
||||||
async def get_stats(user: dict = Depends(require_viewer)) -> dict[str, Any]:
|
async def get_stats(user: dict = Depends(require_viewer)) -> dict[str, Any]:
|
||||||
return await repo.get_stats_summary()
|
return await repo.get_stats_summary()
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ def _build_trace_links(logs: list[dict]) -> list:
|
|||||||
"description": "Real-time Server-Sent Events (SSE) stream"
|
"description": "Real-time Server-Sent Events (SSE) stream"
|
||||||
},
|
},
|
||||||
401: {"description": "Could not validate credentials"},
|
401: {"description": "Could not validate credentials"},
|
||||||
|
403: {"description": "Insufficient permissions"},
|
||||||
422: {"description": "Validation error"}
|
422: {"description": "Validation error"}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
[[project]]
|
||||||
|
title = "DECNET API"
|
||||||
|
continue-on-failure = true
|
||||||
request-timeout = 5.0
|
request-timeout = 5.0
|
||||||
|
|
||||||
[[operations]]
|
[[operations]]
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ async def token(live_client):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.live
|
@pytest.mark.live
|
||||||
|
@pytest.mark.xdist_group("health_live")
|
||||||
class TestHealthLive:
|
class TestHealthLive:
|
||||||
"""Live integration tests — real DB, real Docker check, real task state."""
|
"""Live integration tests — real DB, real Docker check, real task state."""
|
||||||
|
|
||||||
|
|||||||
@@ -128,6 +128,7 @@ async def token(live_client):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.live
|
@pytest.mark.live
|
||||||
|
@pytest.mark.xdist_group("service_isolation_live")
|
||||||
class TestCollectorLiveIsolation:
|
class TestCollectorLiveIsolation:
|
||||||
"""Real collector behaviour against the actual Docker daemon."""
|
"""Real collector behaviour against the actual Docker daemon."""
|
||||||
|
|
||||||
@@ -203,6 +204,7 @@ class TestCollectorLiveIsolation:
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.live
|
@pytest.mark.live
|
||||||
|
@pytest.mark.xdist_group("service_isolation_live")
|
||||||
class TestIngesterLiveIsolation:
|
class TestIngesterLiveIsolation:
|
||||||
"""Real ingester against real DB and real filesystem."""
|
"""Real ingester against real DB and real filesystem."""
|
||||||
|
|
||||||
@@ -312,6 +314,7 @@ class TestIngesterLiveIsolation:
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.live
|
@pytest.mark.live
|
||||||
|
@pytest.mark.xdist_group("service_isolation_live")
|
||||||
class TestAttackerWorkerLiveIsolation:
|
class TestAttackerWorkerLiveIsolation:
|
||||||
"""Real attacker worker against real DB."""
|
"""Real attacker worker against real DB."""
|
||||||
|
|
||||||
@@ -360,6 +363,7 @@ class TestAttackerWorkerLiveIsolation:
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.live
|
@pytest.mark.live
|
||||||
|
@pytest.mark.xdist_group("service_isolation_live")
|
||||||
class TestSnifferLiveIsolation:
|
class TestSnifferLiveIsolation:
|
||||||
"""Real sniffer against the actual host network stack."""
|
"""Real sniffer against the actual host network stack."""
|
||||||
|
|
||||||
@@ -396,6 +400,7 @@ class TestSnifferLiveIsolation:
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.live
|
@pytest.mark.live
|
||||||
|
@pytest.mark.xdist_group("service_isolation_live")
|
||||||
class TestApiLifespanLiveIsolation:
|
class TestApiLifespanLiveIsolation:
|
||||||
"""Real API lifespan against real DB and real host state."""
|
"""Real API lifespan against real DB and real host state."""
|
||||||
|
|
||||||
@@ -442,6 +447,7 @@ class TestApiLifespanLiveIsolation:
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.live
|
@pytest.mark.live
|
||||||
|
@pytest.mark.xdist_group("service_isolation_live")
|
||||||
class TestCascadeLiveIsolation:
|
class TestCascadeLiveIsolation:
|
||||||
"""Verify that real component failures do not cascade."""
|
"""Verify that real component failures do not cascade."""
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user