/ server / tests / test_routes_create_delete.py
test_routes_create_delete.py
  1  # Copyright 2025 Alibaba Group Holding Ltd.
  2  #
  3  # Licensed under the Apache License, Version 2.0 (the "License");
  4  # you may not use this file except in compliance with the License.
  5  # You may obtain a copy of the License at
  6  #
  7  #     http://www.apache.org/licenses/LICENSE-2.0
  8  #
  9  # Unless required by applicable law or agreed to in writing, software
 10  # distributed under the License is distributed on an "AS IS" BASIS,
 11  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12  # See the License for the specific language governing permissions and
 13  # limitations under the License.
 14  
 15  from datetime import datetime, timedelta, timezone
 16  
 17  from fastapi.testclient import TestClient
 18  
 19  from opensandbox_server.api import lifecycle
 20  from opensandbox_server.api.schema import CreateSandboxResponse, SandboxStatus
 21  
 22  
 23  def test_create_sandbox_returns_202_and_service_payload(
 24      client: TestClient,
 25      auth_headers: dict,
 26      sample_sandbox_request: dict,
 27      monkeypatch,
 28  ) -> None:
 29      now = datetime.now(timezone.utc)
 30      calls: list[object] = []
 31  
 32      class StubService:
 33          @staticmethod
 34          async def create_sandbox(request) -> CreateSandboxResponse:
 35              calls.append(request)
 36              return CreateSandboxResponse(
 37                  id="sbx-001",
 38                  status=SandboxStatus(state="Pending"),
 39                  metadata={"project": "test-project"},
 40                  expiresAt=now + timedelta(hours=1),
 41                  createdAt=now,
 42                  entrypoint=["python", "-c", "print('Hello from sandbox')"],
 43              )
 44  
 45      monkeypatch.setattr(lifecycle, "sandbox_service", StubService())
 46  
 47      response = client.post(
 48          "/v1/sandboxes",
 49          headers=auth_headers,
 50          json=sample_sandbox_request,
 51      )
 52  
 53      assert response.status_code == 202
 54      payload = response.json()
 55      assert payload["id"] == "sbx-001"
 56      assert payload["status"]["state"] == "Pending"
 57      assert payload["metadata"]["project"] == "test-project"
 58      assert payload["entrypoint"] == ["python", "-c", "print('Hello from sandbox')"]
 59      assert len(calls) == 1
 60      assert calls[0].image.uri == "python:3.11"
 61  
 62  
 63  def test_create_sandbox_manual_cleanup_omits_none_fields(
 64      client: TestClient,
 65      auth_headers: dict,
 66      sample_sandbox_request: dict,
 67      monkeypatch,
 68  ) -> None:
 69      now = datetime.now(timezone.utc)
 70  
 71      class StubService:
 72          @staticmethod
 73          async def create_sandbox(request) -> CreateSandboxResponse:
 74              return CreateSandboxResponse(
 75                  id="sbx-manual",
 76                  status=SandboxStatus(state="Pending"),
 77                  metadata=None,
 78                  expiresAt=None,
 79                  createdAt=now,
 80                  entrypoint=["python", "-c", "print('Hello from sandbox')"],
 81              )
 82  
 83      monkeypatch.setattr(lifecycle, "sandbox_service", StubService())
 84      sample_sandbox_request.pop("timeout", None)
 85  
 86      response = client.post(
 87          "/v1/sandboxes",
 88          headers=auth_headers,
 89          json=sample_sandbox_request,
 90      )
 91  
 92      assert response.status_code == 202
 93      payload = response.json()
 94      assert "expiresAt" not in payload
 95      assert "metadata" not in payload
 96      assert "reason" not in payload["status"]
 97      assert "message" not in payload["status"]
 98      assert "lastTransitionAt" not in payload["status"]
 99  
100  
101  def test_create_sandbox_rejects_invalid_request(
102      client: TestClient,
103      auth_headers: dict,
104  ) -> None:
105      response = client.post(
106          "/v1/sandboxes",
107          headers=auth_headers,
108          json={"timeout": 10},
109      )
110  
111      assert response.status_code == 422
112  
113  
114  def test_delete_sandbox_returns_204_and_calls_service(
115      client: TestClient,
116      auth_headers: dict,
117      monkeypatch,
118  ) -> None:
119      calls: list[str] = []
120  
121      class StubService:
122          @staticmethod
123          def delete_sandbox(sandbox_id: str) -> None:
124              calls.append(sandbox_id)
125  
126      monkeypatch.setattr(lifecycle, "sandbox_service", StubService())
127  
128      response = client.delete("/v1/sandboxes/sbx-001", headers=auth_headers)
129  
130      assert response.status_code == 204
131      assert response.text == ""
132      assert calls == ["sbx-001"]
133  
134  
135  def test_delete_sandbox_requires_api_key(client: TestClient) -> None:
136      response = client.delete("/v1/sandboxes/sbx-001")
137  
138      assert response.status_code == 401
139      assert response.json()["code"] == "MISSING_API_KEY"