Files

127 lines
6.9 KiB
Python

"""Integration coverage for persisted, administrator-owned performance controls."""
from types import SimpleNamespace
from unittest.mock import AsyncMock
import httpx
import pytest
import pytest_asyncio
from fastapi import FastAPI, HTTPException
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
from test_api_read_cache import FakeRedis, _config
from app.gateway import api_read_cache as cache
from app.gateway.deps import get_config
from app.gateway.routers import browser_context, system_settings
from deerflow.config.browser_context_config import BrowserContextConfig
from deerflow.persistence.api_read_cache_settings.model import ApiReadCacheSettingRow
@pytest_asyncio.fixture
async def controls(monkeypatch, tmp_path):
engine = create_async_engine(f"sqlite+aiosqlite:///{tmp_path / 'controls.db'}")
async with engine.begin() as connection:
await connection.run_sync(ApiReadCacheSettingRow.__table__.create)
factory = async_sessionmaker(engine, expire_on_commit=False)
monkeypatch.setattr("deerflow.persistence.engine.get_session_factory", lambda: factory)
monkeypatch.setattr(cache, "_api_read_cache_settings_cache", None)
redis = FakeRedis()
monkeypatch.setattr(cache, "_client", lambda *_args, **_kwargs: (redis, "test:controls"))
config = _config()
config.browser_context = BrowserContextConfig(enabled=True)
auth = AsyncMock(return_value=SimpleNamespace(id="admin", system_role="admin"))
monkeypatch.setattr(system_settings, "get_current_user_from_request", auth)
app = FastAPI()
app.state.config = config
app.dependency_overrides[get_config] = lambda: config
app.include_router(system_settings.router)
app.include_router(browser_context.router)
async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app), base_url="http://test") as client:
yield client, config, redis, auth
await engine.dispose()
@pytest.mark.asyncio
async def test_partial_updates_persist_without_resetting_other_switches(controls, monkeypatch):
client, config, _, _ = controls
endpoint = "/api/system-settings/api-read-cache"
response = await client.put(endpoint, json={"enabled": True, "ttl_seconds": 777, "collection_overrides": {"agents": False}})
assert response.status_code == 200
assert {row["id"]: row["enabled"] for row in response.json()["collections"]}["agents"] is False
response = await client.put(endpoint, json={"browser_status_enabled": False, "collection_overrides": {"skills": False}})
assert response.status_code == 200
monkeypatch.setattr(cache, "_api_read_cache_settings_cache", None)
result = (await client.get(endpoint)).json()
assert result["enabled"] is True and result["ttl_seconds"] == 777
assert result["browser_status_enabled"] is False
rows = {row["id"]: row for row in result["collections"]}
assert not rows["agents"]["enabled"] and not rows["skills"]["enabled"]
assert rows["light_apps"]["enabled"]
assert all(row["paths"] and row["scope"] and row["ttl_seconds"] == 777 for row in rows.values())
assert await cache.is_browser_status_enabled(config) is False
@pytest.mark.asyncio
async def test_per_collection_disable_bypasses_reads_and_writes(controls):
client, config, redis, _ = controls
await cache.set_cached_json(config, "agents", "u:1", {"old": True})
await client.put("/api/system-settings/api-read-cache", json={"collection_overrides": {"agents": False}})
snapshot = dict(redis.values)
assert await cache.get_cached_json(config, "agents", "u:1", version="0") is None
assert await cache.get_cache_version(config, "agents") is None
assert await cache.wait_for_cached_json_or_claim_fill(config, "agents", "u:1", version="0") == (None, None)
await cache.set_cached_json(config, "agents", "u:1", {"new": True}, expected_version="0")
assert redis.values == snapshot
await cache.set_cached_json(config, "skills", "u:1", {"enabled": True})
assert await cache.get_cached_json(config, "skills", "u:1") == {"enabled": True}
await client.put("/api/system-settings/api-read-cache", json={"collection_overrides": {"agents": True}})
assert await cache.get_cached_json(config, "agents", "u:1") is None
@pytest.mark.asyncio
async def test_browser_access_gate_precedes_warm_cache_and_recovers(controls):
client, _, redis, _ = controls
assert (await client.get("/api/browser-context/status")).status_code == 200
assert any(":browser_status:v" in key for key in redis.values)
response = await client.put("/api/system-settings/api-read-cache", json={"browser_status_enabled": False})
assert response.status_code == 200
response = await client.get("/api/browser-context/status")
assert response.status_code == 403
assert response.json()["detail"]["code"] == "browser_status_disabled"
assert response.headers["Retry-After"] == "300"
await client.put("/api/system-settings/api-read-cache", json={"browser_status_enabled": True})
assert (await client.get("/api/browser-context/status")).json()["enabled"] is True
@pytest.mark.asyncio
@pytest.mark.parametrize("payload", [{"ttl_seconds": 0}, {"ttl_seconds": 3601}, {"collection_overrides": {"typo": True}}])
async def test_invalid_settings_are_rejected_without_persisting(controls, payload):
client, _, _, _ = controls
assert (await client.put("/api/system-settings/api-read-cache", json=payload)).status_code == 422
assert (await client.get("/api/system-settings/api-read-cache")).json()["ttl_seconds"] == 300
@pytest.mark.asyncio
@pytest.mark.parametrize("role,status", [("user", 403), ("anonymous", 401)])
async def test_only_admin_can_read_or_update_controls(controls, role, status):
client, _, _, auth = controls
if role == "anonymous":
auth.side_effect = HTTPException(status_code=401, detail="Login required")
else:
auth.return_value = SimpleNamespace(system_role=role)
assert (await client.get("/api/system-settings/api-read-cache")).status_code == status
assert (await client.put("/api/system-settings/api-read-cache", json={"browser_status_enabled": False})).status_code == status
@pytest.mark.asyncio
async def test_redis_failure_falls_back_and_global_off_preserves_individual_choices(controls, monkeypatch):
client, config, redis, _ = controls
await client.put("/api/system-settings/api-read-cache", json={"enabled": False, "collection_overrides": {"agents": False}})
assert await cache.get_cache_version(config, "skills") is None
response = await client.put("/api/system-settings/api-read-cache", json={"enabled": True})
assert next(row for row in response.json()["collections"] if row["id"] == "agents")["enabled"] is False
monkeypatch.setattr(redis, "get", AsyncMock(side_effect=ConnectionError("offline")))
assert await cache.get_cached_json(config, "skills", "u:1") is None
await cache.set_cached_json(config, "skills", "u:1", {"ok": True})
assert (await client.get("/api/browser-context/status")).status_code == 200