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:
@@ -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}"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user