diff --git a/server/src/api/lifecycle.py b/server/src/api/lifecycle.py index cd4e7ca4b..e2deb800c 100644 --- a/server/src/api/lifecycle.py +++ b/server/src/api/lifecycle.py @@ -54,6 +54,7 @@ @router.post( "/sandboxes", response_model=CreateSandboxResponse, + response_model_exclude_none=True, status_code=status.HTTP_202_ACCEPTED, responses={ 202: {"description": "Sandbox creation accepted for asynchronous provisioning"}, @@ -92,6 +93,7 @@ async def create_sandbox( @router.get( "/sandboxes", response_model=ListSandboxesResponse, + response_model_exclude_none=True, responses={ 200: {"description": "Paginated collection of sandboxes"}, 400: {"model": ErrorResponse, "description": "The request was invalid or malformed"}, @@ -155,6 +157,7 @@ async def list_sandboxes( @router.get( "/sandboxes/{sandbox_id}", response_model=Sandbox, + response_model_exclude_none=True, responses={ 200: {"description": "Sandbox current state and metadata"}, 401: {"model": ErrorResponse, "description": "Authentication credentials are missing or invalid"}, diff --git a/server/src/main.py b/server/src/main.py index be7b068d7..46bf0cff7 100644 --- a/server/src/main.py +++ b/server/src/main.py @@ -72,7 +72,7 @@ from src.api.lifecycle import router # noqa: E402 from src.api.pool import router as pool_router # noqa: E402 -from src.api.lifecycle import router, sandbox_service # noqa: E402 +from src.api.lifecycle import sandbox_service # noqa: E402 from src.api.proxy import router as proxy_router # noqa: E402 from src.integrations.renew_intent.proxy_renew import ProxyRenewCoordinator # noqa: E402 from src.middleware.auth import AuthMiddleware # noqa: E402 diff --git a/server/tests/k8s/test_pool_service.py b/server/tests/k8s/test_pool_service.py index 32f6e09df..366cee109 100644 --- a/server/tests/k8s/test_pool_service.py +++ b/server/tests/k8s/test_pool_service.py @@ -19,7 +19,7 @@ """ import pytest -from unittest.mock import MagicMock, call +from unittest.mock import MagicMock from kubernetes.client import ApiException from src.api.schema import ( diff --git a/server/tests/test_pool_api.py b/server/tests/test_pool_api.py index 3917ee434..807fc78ee 100644 --- a/server/tests/test_pool_api.py +++ b/server/tests/test_pool_api.py @@ -19,7 +19,6 @@ so no real cluster connection is needed. """ -import pytest from unittest.mock import MagicMock, patch from fastapi.testclient import TestClient from fastapi import HTTPException, status as http_status diff --git a/server/tests/test_routes.py b/server/tests/test_routes.py index 73df44037..6150f3063 100644 --- a/server/tests/test_routes.py +++ b/server/tests/test_routes.py @@ -164,14 +164,14 @@ def test_get_sandbox_success( """ pass - def test_get_sandbox_preserves_nullable_expires_at( + def test_get_sandbox_omits_null_optional_fields( self, client: TestClient, auth_headers: dict, monkeypatch, ): """ - Ensure expiresAt is returned as null for manual-cleanup sandboxes. + Ensure optional null fields are omitted from JSON (manual-cleanup sandboxes). """ now = datetime.now(timezone.utc) sandbox = Sandbox( @@ -195,16 +195,15 @@ def get_sandbox(sandbox_id: str) -> Sandbox: assert response.status_code == 200 payload = response.json() - assert payload["metadata"] is None + assert "metadata" not in payload assert payload["id"] == "sandbox-123" assert payload["entrypoint"] == ["python"] - assert "expiresAt" in payload - assert payload["expiresAt"] is None + assert "expiresAt" not in payload assert "createdAt" in payload assert payload["status"]["state"] == "Running" - assert payload["status"]["reason"] is None - assert payload["status"]["message"] is None - assert payload["status"]["lastTransitionAt"] is None + assert "reason" not in payload["status"] + assert "message" not in payload["status"] + assert "lastTransitionAt" not in payload["status"] def test_get_sandbox_not_found( self, diff --git a/server/tests/test_routes_create_delete.py b/server/tests/test_routes_create_delete.py index 00a255bb2..7aac39809 100644 --- a/server/tests/test_routes_create_delete.py +++ b/server/tests/test_routes_create_delete.py @@ -60,7 +60,7 @@ async def create_sandbox(request) -> CreateSandboxResponse: assert calls[0].image.uri == "python:3.11" -def test_create_sandbox_manual_cleanup_returns_null_expiration( +def test_create_sandbox_manual_cleanup_omits_null_optional_fields( client: TestClient, auth_headers: dict, sample_sandbox_request: dict, @@ -91,11 +91,11 @@ async def create_sandbox(request) -> CreateSandboxResponse: assert response.status_code == 202 payload = response.json() - assert payload["expiresAt"] is None - assert payload["metadata"] is None - assert payload["status"]["reason"] is None - assert payload["status"]["message"] is None - assert payload["status"]["lastTransitionAt"] is None + assert "expiresAt" not in payload + assert "metadata" not in payload + assert "reason" not in payload["status"] + assert "message" not in payload["status"] + assert "lastTransitionAt" not in payload["status"] def test_create_sandbox_rejects_invalid_request( diff --git a/server/tests/test_routes_list_sandboxes.py b/server/tests/test_routes_list_sandboxes.py index 046a4ea87..2b2b73351 100644 --- a/server/tests/test_routes_list_sandboxes.py +++ b/server/tests/test_routes_list_sandboxes.py @@ -132,7 +132,7 @@ def list_sandboxes(request) -> ListSandboxesResponse: assert captured_requests[0].filter.metadata == {"team": "infra", "note": ""} -def test_list_sandboxes_preserves_only_nullable_expires_at( +def test_list_sandboxes_omits_null_optional_fields( client: TestClient, auth_headers: dict, monkeypatch, @@ -169,11 +169,11 @@ def list_sandboxes(request) -> ListSandboxesResponse: assert response.status_code == 200 item = response.json()["items"][0] - assert item["expiresAt"] is None - assert item["metadata"] is None - assert item["status"]["reason"] is None - assert item["status"]["message"] is None - assert item["status"]["lastTransitionAt"] is None + assert "expiresAt" not in item + assert "metadata" not in item + assert "reason" not in item["status"] + assert "message" not in item["status"] + assert "lastTransitionAt" not in item["status"] def test_list_sandboxes_validates_page_bounds(