feat(cloud-console): task listing, attempt history, CORS, and console SPA
Implements the cloud-console OpenSpec change: adds GET /v1/tasks (filterable,
bounded pagination, tasks:read) and GET /v1/tasks/{id}/attempts (404 on unknown
task) to the platform SDK, with matching CloudClient methods and a closed-by-
default CLOUD_CONSOLE_CORS_ORIGINS allow-list wired through CloudControlConfig.
Ships an independent Vue 3 + Vite SPA at cloud-console/ that authenticates with
an operator-supplied bearer token held in sessionStorage, renders tasks with
attempt history, device pool, host registry, and the plugin registry with a
registration form.
Backend test suite: 438 passed (-m "not integration"); cloud-console typecheck
and production build both succeed. PostgreSQL-backed repository tests and
manual end-to-end verification remain pending external infrastructure.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -608,3 +608,80 @@ def test_production_app_rejects_missing_credentials() -> None:
|
||||
database_url="postgresql://db/cloud",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def test_cors_headers_are_absent_when_allow_list_is_empty() -> None:
|
||||
app = create_app(config=CloudControlConfig(database_url="sqlite:///:memory:"))
|
||||
with TestClient(app) as client:
|
||||
response = client.options(
|
||||
"/health/live",
|
||||
headers={
|
||||
"Origin": "http://console.example",
|
||||
"Access-Control-Request-Method": "GET",
|
||||
},
|
||||
)
|
||||
assert response.status_code >= 400
|
||||
assert "access-control-allow-origin" not in {
|
||||
key.lower() for key in response.headers
|
||||
}
|
||||
|
||||
|
||||
def test_cors_headers_reflect_configured_origin_only() -> None:
|
||||
app = create_app(
|
||||
config=CloudControlConfig(
|
||||
database_url="sqlite:///:memory:",
|
||||
cors_allowed_origins=("http://console.example",),
|
||||
)
|
||||
)
|
||||
with TestClient(app) as client:
|
||||
allowed = client.options(
|
||||
"/health/live",
|
||||
headers={
|
||||
"Origin": "http://console.example",
|
||||
"Access-Control-Request-Method": "GET",
|
||||
},
|
||||
)
|
||||
blocked = client.options(
|
||||
"/health/live",
|
||||
headers={
|
||||
"Origin": "http://attacker.example",
|
||||
"Access-Control-Request-Method": "GET",
|
||||
},
|
||||
)
|
||||
|
||||
assert allowed.status_code in {200, 204}
|
||||
assert allowed.headers["access-control-allow-origin"] == "http://console.example"
|
||||
# An origin that is not on the allow-list must not be echoed back.
|
||||
assert (
|
||||
blocked.headers.get("access-control-allow-origin") != "http://attacker.example"
|
||||
)
|
||||
|
||||
|
||||
def test_load_control_config_parses_cors_allow_list() -> None:
|
||||
from cloud.control_config import load_control_config
|
||||
|
||||
config = load_control_config(
|
||||
env={
|
||||
"CLOUD_ENVIRONMENT": "local",
|
||||
"CLOUD_DATABASE_URL": "sqlite:///:memory:",
|
||||
"CLOUD_CONSOLE_CORS_ORIGINS": (
|
||||
"http://console.example, https://console.example"
|
||||
),
|
||||
}
|
||||
)
|
||||
assert config.cors_allowed_origins == (
|
||||
"http://console.example",
|
||||
"https://console.example",
|
||||
)
|
||||
|
||||
|
||||
def test_load_control_config_defaults_to_empty_cors_allow_list() -> None:
|
||||
from cloud.control_config import load_control_config
|
||||
|
||||
config = load_control_config(
|
||||
env={
|
||||
"CLOUD_ENVIRONMENT": "local",
|
||||
"CLOUD_DATABASE_URL": "sqlite:///:memory:",
|
||||
}
|
||||
)
|
||||
assert config.cors_allowed_origins == ()
|
||||
|
||||
Reference in New Issue
Block a user