Files
wehub-resource-sync e0e362d700
SDK Tests / changes (push) Successful in 2m29s
Real E2E Tests / changes (push) Successful in 2m29s
Deploy Docs Pages / build (push) Has been cancelled
Deploy Docs Pages / deploy (push) Has been cancelled
Real E2E Tests / JavaScript E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Python E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Java E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / C# E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Go E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Real E2E CI (push) Has been cancelled
SDK Tests / SDK CI (push) Has been cancelled
SDK Tests / CLI Tests (push) Has been cancelled
SDK Tests / Python SDK Quality (code-interpreter) (push) Has been cancelled
SDK Tests / Python SDK Quality (sandbox) (push) Has been cancelled
SDK Tests / Python SDK Tests (code-interpreter) (push) Has been cancelled
SDK Tests / JavaScript SDK Quality And Tests (code-interpreter) (push) Has been cancelled
SDK Tests / JavaScript SDK Quality And Tests (sandbox) (push) Has been cancelled
SDK Tests / Python SDK Tests (sandbox) (push) Has been cancelled
SDK Tests / CLI Quality (push) Has been cancelled
SDK Tests / Kotlin SDK Quality And Tests (sandbox) (push) Has been cancelled
SDK Tests / Kotlin SDK Quality And Tests (code-interpreter) (push) Has been cancelled
SDK Tests / C# SDK Quality And Tests (code-interpreter) (push) Has been cancelled
SDK Tests / C# SDK Quality And Tests (sandbox) (push) Has been cancelled
SDK Tests / Go SDK Quality And Tests (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:39:33 +08:00

357 lines
14 KiB
Python

# Copyright 2025 Alibaba Group Holding Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Integration-style tests for Pool API routes (opensandbox_server/api/pool.py).
Routes are exercised via FastAPI TestClient. The K8s PoolService is patched
so no real cluster connection is needed.
"""
from unittest.mock import MagicMock, patch
from fastapi.testclient import TestClient
from fastapi import HTTPException, status as http_status
from opensandbox_server.api.schema import (
CreatePoolRequest,
ListPoolsResponse,
PoolCapacitySpec,
PoolResponse,
PoolStatus,
UpdatePoolRequest,
)
from opensandbox_server.services.constants import SandboxErrorCodes
_POOL_SERVICE_PATCH = "opensandbox_server.api.pool._get_pool_service"
def _cap(buffer_max=3, buffer_min=1, pool_max=10, pool_min=0) -> PoolCapacitySpec:
return PoolCapacitySpec(
bufferMax=buffer_max,
bufferMin=buffer_min,
poolMax=pool_max,
poolMin=pool_min,
)
def _pool_response(
name: str = "test-pool",
buffer_max: int = 3,
pool_max: int = 10,
total: int = 2,
allocated: int = 1,
available: int = 1,
) -> PoolResponse:
return PoolResponse(
name=name,
capacitySpec=_cap(buffer_max=buffer_max, pool_max=pool_max),
status=PoolStatus(
total=total,
allocated=allocated,
available=available,
revision="rev-1",
),
)
def _create_request_body(name: str = "test-pool") -> dict:
return {
"name": name,
"template": {
"spec": {
"containers": [
{
"name": "sandbox",
"image": "python:3.11",
"command": ["tail", "-f", "/dev/null"],
}
]
}
},
"capacitySpec": {
"bufferMax": 3,
"bufferMin": 1,
"poolMax": 10,
"poolMin": 0,
},
}
class TestPoolAuthentication:
def test_list_pools_without_api_key_returns_401(self, client: TestClient):
response = client.get("/pools")
assert response.status_code == 401
def test_pool_routes_exist_on_v1_prefix(self, client: TestClient, auth_headers: dict):
"""Verify the /v1/pools routes are registered (even if they return 501 on docker runtime)."""
with patch(_POOL_SERVICE_PATCH) as mock_svc_factory:
mock_svc_factory.side_effect = HTTPException(
status_code=http_status.HTTP_501_NOT_IMPLEMENTED,
detail={"code": "X", "message": "y"},
)
response = client.get("/v1/pools", headers=auth_headers)
assert response.status_code == 501
class TestPoolNotSupportedOnDockerRuntime:
"""Pool endpoints return 501 when PoolService raises 501 (non-k8s runtime)."""
def test_list_pools_returns_501(self, client: TestClient, auth_headers: dict):
with patch(_POOL_SERVICE_PATCH, side_effect=HTTPException(
status_code=501,
detail={"code": SandboxErrorCodes.K8S_POOL_NOT_SUPPORTED, "message": "not k8s"},
)):
response = client.get("/pools", headers=auth_headers)
assert response.status_code == 501
assert SandboxErrorCodes.K8S_POOL_NOT_SUPPORTED in response.json()["code"]
def test_create_pool_returns_501(self, client: TestClient, auth_headers: dict):
with patch(_POOL_SERVICE_PATCH, side_effect=HTTPException(
status_code=501,
detail={"code": SandboxErrorCodes.K8S_POOL_NOT_SUPPORTED, "message": "not k8s"},
)):
response = client.post("/pools", json=_create_request_body(), headers=auth_headers)
assert response.status_code == 501
class TestCreatePoolRoute:
def test_create_pool_success_returns_201(self, client: TestClient, auth_headers: dict):
mock_svc = MagicMock()
mock_svc.create_pool.return_value = _pool_response(name="new-pool")
with patch(_POOL_SERVICE_PATCH, return_value=mock_svc):
response = client.post("/pools", json=_create_request_body("new-pool"), headers=auth_headers)
assert response.status_code == 201
body = response.json()
assert body["name"] == "new-pool"
assert body["capacitySpec"]["bufferMax"] == 3
assert body["status"]["total"] == 2
def test_create_pool_missing_name_returns_422(self, client: TestClient, auth_headers: dict):
body = _create_request_body()
del body["name"]
with patch(_POOL_SERVICE_PATCH, return_value=MagicMock()):
response = client.post("/pools", json=body, headers=auth_headers)
assert response.status_code == 422
def test_create_pool_missing_template_returns_422(self, client: TestClient, auth_headers: dict):
body = _create_request_body()
del body["template"]
with patch(_POOL_SERVICE_PATCH, return_value=MagicMock()):
response = client.post("/pools", json=body, headers=auth_headers)
assert response.status_code == 422
def test_create_pool_missing_capacity_spec_returns_422(self, client: TestClient, auth_headers: dict):
body = _create_request_body()
del body["capacitySpec"]
with patch(_POOL_SERVICE_PATCH, return_value=MagicMock()):
response = client.post("/pools", json=body, headers=auth_headers)
assert response.status_code == 422
def test_create_pool_invalid_name_pattern_returns_422(self, client: TestClient, auth_headers: dict):
"""Pool name must be a valid k8s name (no uppercase, no spaces)."""
body = _create_request_body("Invalid_Name")
with patch(_POOL_SERVICE_PATCH, return_value=MagicMock()):
response = client.post("/pools", json=body, headers=auth_headers)
assert response.status_code == 422
def test_create_pool_negative_buffer_max_returns_422(self, client: TestClient, auth_headers: dict):
body = _create_request_body()
body["capacitySpec"]["bufferMax"] = -1
with patch(_POOL_SERVICE_PATCH, return_value=MagicMock()):
response = client.post("/pools", json=body, headers=auth_headers)
assert response.status_code == 422
def test_create_pool_duplicate_returns_409(self, client: TestClient, auth_headers: dict):
mock_svc = MagicMock()
mock_svc.create_pool.side_effect = HTTPException(
status_code=409,
detail={
"code": SandboxErrorCodes.K8S_POOL_ALREADY_EXISTS,
"message": "Pool 'dup-pool' already exists.",
},
)
with patch(_POOL_SERVICE_PATCH, return_value=mock_svc):
response = client.post("/pools", json=_create_request_body("dup-pool"), headers=auth_headers)
assert response.status_code == 409
assert SandboxErrorCodes.K8S_POOL_ALREADY_EXISTS in response.json()["code"]
def test_create_pool_passes_request_to_service(self, client: TestClient, auth_headers: dict):
mock_svc = MagicMock()
mock_svc.create_pool.return_value = _pool_response()
with patch(_POOL_SERVICE_PATCH, return_value=mock_svc):
client.post("/pools", json=_create_request_body("my-pool"), headers=auth_headers)
call_args = mock_svc.create_pool.call_args
req: CreatePoolRequest = call_args.args[0]
assert req.name == "my-pool"
assert req.capacity_spec.buffer_max == 3
assert req.capacity_spec.pool_max == 10
class TestListPoolsRoute:
def test_list_pools_returns_200_and_items(self, client: TestClient, auth_headers: dict):
mock_svc = MagicMock()
mock_svc.list_pools.return_value = ListPoolsResponse(
items=[_pool_response("pool-a"), _pool_response("pool-b")]
)
with patch(_POOL_SERVICE_PATCH, return_value=mock_svc):
response = client.get("/pools", headers=auth_headers)
assert response.status_code == 200
body = response.json()
assert len(body["items"]) == 2
names = {p["name"] for p in body["items"]}
assert names == {"pool-a", "pool-b"}
def test_list_pools_response_has_status_fields(self, client: TestClient, auth_headers: dict):
mock_svc = MagicMock()
mock_svc.list_pools.return_value = ListPoolsResponse(
items=[_pool_response("p", total=5, allocated=3, available=2)]
)
with patch(_POOL_SERVICE_PATCH, return_value=mock_svc):
response = client.get("/pools", headers=auth_headers)
pool = response.json()["items"][0]
assert pool["status"]["total"] == 5
assert pool["status"]["allocated"] == 3
assert pool["status"]["available"] == 2
class TestGetPoolRoute:
def test_get_pool_success_returns_200(self, client: TestClient, auth_headers: dict):
mock_svc = MagicMock()
mock_svc.get_pool.return_value = _pool_response(name="my-pool")
with patch(_POOL_SERVICE_PATCH, return_value=mock_svc):
response = client.get("/pools/my-pool", headers=auth_headers)
assert response.status_code == 200
assert response.json()["name"] == "my-pool"
def test_get_pool_not_found_returns_404(self, client: TestClient, auth_headers: dict):
mock_svc = MagicMock()
mock_svc.get_pool.side_effect = HTTPException(
status_code=404,
detail={
"code": SandboxErrorCodes.K8S_POOL_NOT_FOUND,
"message": "Pool 'ghost' not found.",
},
)
with patch(_POOL_SERVICE_PATCH, return_value=mock_svc):
response = client.get("/pools/ghost", headers=auth_headers)
assert response.status_code == 404
assert SandboxErrorCodes.K8S_POOL_NOT_FOUND in response.json()["code"]
class TestUpdatePoolRoute:
def _update_body(self, buffer_max=5, pool_max=20) -> dict:
return {
"capacitySpec": {
"bufferMax": buffer_max,
"bufferMin": 1,
"poolMax": pool_max,
"poolMin": 0,
}
}
def test_update_pool_success_returns_200(self, client: TestClient, auth_headers: dict):
mock_svc = MagicMock()
mock_svc.update_pool.return_value = _pool_response(buffer_max=5, pool_max=20)
with patch(_POOL_SERVICE_PATCH, return_value=mock_svc):
response = client.put("/pools/my-pool", json=self._update_body(), headers=auth_headers)
assert response.status_code == 200
assert response.json()["capacitySpec"]["bufferMax"] == 5
def test_update_pool_calls_service_with_name_and_request(
self, client: TestClient, auth_headers: dict
):
mock_svc = MagicMock()
mock_svc.update_pool.return_value = _pool_response()
with patch(_POOL_SERVICE_PATCH, return_value=mock_svc):
client.put("/pools/target", json=self._update_body(buffer_max=9), headers=auth_headers)
call_args = mock_svc.update_pool.call_args
assert call_args.args[0] == "target"
req: UpdatePoolRequest = call_args.args[1]
assert req.capacity_spec.buffer_max == 9
def test_update_pool_not_found_returns_404(self, client: TestClient, auth_headers: dict):
mock_svc = MagicMock()
mock_svc.update_pool.side_effect = HTTPException(
status_code=404,
detail={
"code": SandboxErrorCodes.K8S_POOL_NOT_FOUND,
"message": "Pool 'x' not found.",
},
)
with patch(_POOL_SERVICE_PATCH, return_value=mock_svc):
response = client.put("/pools/x", json=self._update_body(), headers=auth_headers)
assert response.status_code == 404
def test_update_pool_missing_capacity_spec_returns_422(
self, client: TestClient, auth_headers: dict
):
with patch(_POOL_SERVICE_PATCH, return_value=MagicMock()):
response = client.put("/pools/p", json={}, headers=auth_headers)
assert response.status_code == 422
def test_update_pool_negative_pool_max_returns_422(
self, client: TestClient, auth_headers: dict
):
with patch(_POOL_SERVICE_PATCH, return_value=MagicMock()):
response = client.put(
"/pools/p",
json={"capacitySpec": {"bufferMax": 1, "bufferMin": 0, "poolMax": -5, "poolMin": 0}},
headers=auth_headers,
)
assert response.status_code == 422
class TestDeletePoolRoute:
def test_delete_pool_success_returns_204(self, client: TestClient, auth_headers: dict):
mock_svc = MagicMock()
mock_svc.delete_pool.return_value = None
with patch(_POOL_SERVICE_PATCH, return_value=mock_svc):
response = client.delete("/pools/my-pool", headers=auth_headers)
assert response.status_code == 204
assert response.content == b""
def test_delete_pool_not_found_returns_404(self, client: TestClient, auth_headers: dict):
mock_svc = MagicMock()
mock_svc.delete_pool.side_effect = HTTPException(
status_code=404,
detail={
"code": SandboxErrorCodes.K8S_POOL_NOT_FOUND,
"message": "Pool 'gone' not found.",
},
)
with patch(_POOL_SERVICE_PATCH, return_value=mock_svc):
response = client.delete("/pools/gone", headers=auth_headers)
assert response.status_code == 404
assert SandboxErrorCodes.K8S_POOL_NOT_FOUND in response.json()["code"]