|
12 | 12 | # pyright: reportPrivateUsage=false |
13 | 13 |
|
14 | 14 | import asyncio |
| 15 | +import json |
15 | 16 | from collections.abc import AsyncGenerator |
16 | 17 | from datetime import UTC, datetime |
17 | 18 | from typing import Any |
|
24 | 25 | from drukbox_sdk import ( |
25 | 26 | HTTPProxy, |
26 | 27 | HTTPProxyAttachment, |
| 28 | + Issuer, |
27 | 29 | SandboxAPI, |
28 | 30 | SandboxAuthError, |
29 | 31 | SandboxConflictError, |
|
32 | 34 | SandboxResponseError, |
33 | 35 | SandboxUnavailableError, |
34 | 36 | SandboxValidationError, |
| 37 | + Secret, |
35 | 38 | ) |
36 | 39 |
|
37 | 40 | BASE_URL = "https://sandbox.test" |
@@ -268,6 +271,115 @@ async def test_create_host_409_when_template_is_building(api: SandboxAPI): |
268 | 271 | await api.create_host(template=template_id) |
269 | 272 |
|
270 | 273 |
|
| 274 | +@respx.mock |
| 275 | +async def test_create_host_sends_secrets_with_only_the_fields_set(api: SandboxAPI): |
| 276 | + route = respx.post(f"{BASE_URL}/hosts").mock( |
| 277 | + return_value=httpx.Response(201, json=_host_payload()), |
| 278 | + ) |
| 279 | + |
| 280 | + await api.create_host( |
| 281 | + secrets={ |
| 282 | + "anthropic": Issuer( |
| 283 | + "https://mint.test/box-1/anthropic", |
| 284 | + headers={"Authorization": "Bearer d2d"}, |
| 285 | + refresh="1h", |
| 286 | + ), |
| 287 | + "openai": Secret("sk-real"), |
| 288 | + "acme": Secret("ak_real", host="api.acme.test", auth_variable="ACME_TOKEN"), |
| 289 | + }, |
| 290 | + ) |
| 291 | + |
| 292 | + body = json.loads(route.calls.last.request.content) |
| 293 | + assert body["secrets"] == { |
| 294 | + "anthropic": { |
| 295 | + "issuer": { |
| 296 | + "url": "https://mint.test/box-1/anthropic", |
| 297 | + "headers": {"Authorization": "Bearer d2d"}, |
| 298 | + "refresh": "1h", |
| 299 | + } |
| 300 | + }, |
| 301 | + "openai": {"value": "sk-real"}, |
| 302 | + "acme": {"value": "ak_real", "host": "api.acme.test", "auth_variable": "ACME_TOKEN"}, |
| 303 | + } |
| 304 | + |
| 305 | + |
| 306 | +@respx.mock |
| 307 | +async def test_create_host_omits_secrets_when_unset(api: SandboxAPI): |
| 308 | + route = respx.post(f"{BASE_URL}/hosts").mock( |
| 309 | + return_value=httpx.Response(201, json=_host_payload()), |
| 310 | + ) |
| 311 | + |
| 312 | + await api.create_host() |
| 313 | + |
| 314 | + assert "secrets" not in json.loads(route.calls.last.request.content) |
| 315 | + |
| 316 | + |
| 317 | +@respx.mock |
| 318 | +async def test_create_host_keeps_an_empty_auth_prefix(api: SandboxAPI): |
| 319 | + route = respx.post(f"{BASE_URL}/hosts").mock( |
| 320 | + return_value=httpx.Response(201, json=_host_payload()), |
| 321 | + ) |
| 322 | + custom = {"host": "api.acme.test", "auth_variable": "ACME_TOKEN", "auth_prefix": ""} |
| 323 | + |
| 324 | + await api.create_host( |
| 325 | + secrets={ |
| 326 | + "acme": Secret("ak_real", **custom), |
| 327 | + "acme-minted": Issuer("https://mint.test/acme", {"X-Key": "k"}, "1h", **custom), |
| 328 | + }, |
| 329 | + ) |
| 330 | + |
| 331 | + body = json.loads(route.calls.last.request.content) |
| 332 | + assert body["secrets"]["acme"]["auth_prefix"] == "" |
| 333 | + assert body["secrets"]["acme-minted"]["auth_prefix"] == "" |
| 334 | + |
| 335 | + |
| 336 | +def test_a_record_never_shows_its_credential(): |
| 337 | + secret = Secret("VALUE_MARKER", host="api.acme.test", auth_variable="ACME_TOKEN") |
| 338 | + issuer = Issuer("https://mint.test/acme", {"Authorization": "ISSUER_MARKER"}, "1h") |
| 339 | + |
| 340 | + shown = repr({"acme": secret, "minted": issuer}) |
| 341 | + |
| 342 | + assert "VALUE_MARKER" not in shown |
| 343 | + assert "ISSUER_MARKER" not in shown |
| 344 | + assert "api.acme.test" in shown and "https://mint.test/acme" in shown |
| 345 | + |
| 346 | + |
| 347 | +def test_the_custom_service_fields_are_keywords(): |
| 348 | + with pytest.raises(TypeError): |
| 349 | + Secret("ak_real", "api.acme.test") # type: ignore[misc] |
| 350 | + with pytest.raises(TypeError): |
| 351 | + Issuer("https://mint.test", {"X-Key": "k"}, "1h", "api.acme.test") # type: ignore[misc] |
| 352 | + |
| 353 | + |
| 354 | +@respx.mock |
| 355 | +async def test_create_host_409_without_the_secrets_proxy_raises_conflict(api: SandboxAPI): |
| 356 | + respx.post(f"{BASE_URL}/hosts").mock( |
| 357 | + return_value=httpx.Response( |
| 358 | + 409, |
| 359 | + json={ |
| 360 | + "detail": "secrets need the secrets proxy, which this deployment lacks", |
| 361 | + "error_code": "SECRETS_PROXY_NOT_CONFIGURED", |
| 362 | + }, |
| 363 | + ), |
| 364 | + ) |
| 365 | + |
| 366 | + with pytest.raises(SandboxConflictError, match="secrets proxy"): |
| 367 | + await api.create_host(secrets={"openai": Secret("sk-real")}) |
| 368 | + |
| 369 | + |
| 370 | +@respx.mock |
| 371 | +async def test_create_host_422_for_an_unknown_secret_service_raises_validation(api: SandboxAPI): |
| 372 | + respx.post(f"{BASE_URL}/hosts").mock( |
| 373 | + return_value=httpx.Response( |
| 374 | + 422, |
| 375 | + json={"detail": [{"loc": ["body", "secrets"], "msg": "unknown secret service 'acme'"}]}, |
| 376 | + ), |
| 377 | + ) |
| 378 | + |
| 379 | + with pytest.raises(SandboxValidationError, match="unknown secret service"): |
| 380 | + await api.create_host(secrets={"acme": Secret("ak_real")}) |
| 381 | + |
| 382 | + |
271 | 383 | @respx.mock |
272 | 384 | async def test_get_host_returns_parsed_host(api: SandboxAPI): |
273 | 385 | payload = _host_payload(status="active") |
|
0 commit comments