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"