feat(14-03): cache settings/status API and security tests

- api/cloud/cache.py: GET /analysis/cache, PATCH /analysis/cache/settings
- api/cloud/analysis.py: analysis router aggregator (Phase 14 parent)
- schemas.py: CacheStatusOut, CacheSettingsUpdateRequest (explicit allowlists)
- __init__.py: register analysis_router under /api/cloud
- 7 new security tests: admin block, unauthenticated block, object_key exclusion,
  response structure, enum validation, tier cap enforcement (T-14-01..02, T-14-08)
- All 47 plan target tests pass (test_cloud_cache.py + test_cloud_security.py)
This commit is contained in:
curo1305
2026-06-23 15:32:12 +02:00
parent 5bdd23f3bc
commit 299d4b523d
5 changed files with 346 additions and 0 deletions
+125
View File
@@ -579,3 +579,128 @@ async def test_no_byte_download_during_browse(async_client, db_session):
assert resp.status_code == 200
byte_call_tracker.assert_not_called()
# ── Phase 14: Cache API security (T-14-01, T-14-02, T-14-06, T-14-08) ──────────
async def test_cache_status_admin_blocked(async_client, db_session):
"""T-14-01/CACHE-05: Admin account cannot access the cache status endpoint.
get_regular_user blocks all admin accounts from analysis/cache routes.
"""
admin_ctx = await _create_user_and_token(db_session, role="admin")
resp = await async_client.get(
"/api/cloud/analysis/cache",
headers=admin_ctx["headers"],
)
assert resp.status_code == 403, (
f"Admin must be blocked from cache status — expected 403, got {resp.status_code}"
)
async def test_cache_settings_admin_blocked(async_client, db_session):
"""T-14-01: Admin account cannot update cache settings."""
admin_ctx = await _create_user_and_token(db_session, role="admin")
resp = await async_client.patch(
"/api/cloud/analysis/cache/settings",
json={"analysis_progress_detail": "detailed"},
headers=admin_ctx["headers"],
)
assert resp.status_code == 403, (
f"Admin must be blocked from cache settings update — got {resp.status_code}"
)
async def test_cache_status_unauthenticated_blocked(async_client, db_session):
"""Cache status requires authentication — unauthenticated request returns 401/403."""
resp = await async_client.get("/api/cloud/analysis/cache")
assert resp.status_code in (401, 403), (
f"Unauthenticated request must be rejected — got {resp.status_code}"
)
async def test_cache_status_response_excludes_object_key(async_client, db_session):
"""T-14-02/T-14-08: Cache status response must not contain object_key field.
The MinIO object key is an internal detail — never exposed in API responses.
"""
user_ctx = await _create_user_and_token(db_session, role="user")
resp = await async_client.get(
"/api/cloud/analysis/cache",
headers=user_ctx["headers"],
)
assert resp.status_code == 200, f"Expected 200 from cache status, got {resp.status_code}"
body_text = resp.text
# T-14-02 / T-14-08: object key and credential fields must be absent
assert "object_key" not in body_text, (
"T-14-02: MinIO object_key must not appear in cache API responses"
)
assert "credentials_enc" not in body_text, (
"T-14-08: credentials_enc must not appear in cache API responses"
)
async def test_cache_status_response_structure(async_client, db_session):
"""CACHE-05: Cache status returns aggregate totals, not per-entry detail."""
user_ctx = await _create_user_and_token(db_session, role="user")
resp = await async_client.get(
"/api/cloud/analysis/cache",
headers=user_ctx["headers"],
)
assert resp.status_code == 200
body = resp.json()
# Aggregate fields must be present
assert "entry_count" in body, "Cache status must include entry_count"
assert "total_bytes" in body, "Cache status must include total_bytes"
assert "cache_limit_bytes" in body, "Cache status must include cache_limit_bytes"
assert "analysis_progress_detail" in body, "Cache status must include analysis_progress_detail"
assert "analysis_failure_behavior" in body, "Cache status must include analysis_failure_behavior"
# No per-entry detail that could leak internal state
assert "object_key" not in body
assert "provider_item_id" not in body or isinstance(body.get("provider_item_id"), type(None))
async def test_cache_settings_invalid_enum_returns_422(async_client, db_session):
"""Cache settings update rejects invalid enum values with 422."""
user_ctx = await _create_user_and_token(db_session, role="user")
resp = await async_client.patch(
"/api/cloud/analysis/cache/settings",
json={"analysis_progress_detail": "invalid_value"},
headers=user_ctx["headers"],
)
assert resp.status_code == 422, (
f"Invalid enum value must return 422, got {resp.status_code}"
)
async def test_cache_settings_limit_above_tier_cap_returns_422(async_client, db_session):
"""Cache settings update rejects cache limit above tier cap with 422."""
from services.cloud_analysis_settings import DEFAULT_MAX_CACHE_LIMIT_BYTES
user_ctx = await _create_user_and_token(db_session, role="user")
too_large = DEFAULT_MAX_CACHE_LIMIT_BYTES + 1
resp = await async_client.patch(
"/api/cloud/analysis/cache/settings",
json={"cloud_cache_limit_bytes": too_large},
headers=user_ctx["headers"],
)
assert resp.status_code == 422, (
f"Cache limit above tier cap must return 422, got {resp.status_code}"
)