173 lines
5.1 KiB
Python
173 lines
5.1 KiB
Python
|
|
"""API tests for GET/PUT /api/settings and GET /api/news/models."""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import asyncio
|
||
|
|
import os
|
||
|
|
|
||
|
|
import asyncpg
|
||
|
|
import httpx
|
||
|
|
import pytest
|
||
|
|
|
||
|
|
from conftest import requires_db
|
||
|
|
|
||
|
|
from main import app
|
||
|
|
|
||
|
|
BASE = "http://test"
|
||
|
|
|
||
|
|
|
||
|
|
def _conn_kwargs() -> dict:
|
||
|
|
return {
|
||
|
|
"host": os.environ["DB_HOST"],
|
||
|
|
"port": int(os.environ["DB_PORT"]),
|
||
|
|
"user": os.environ["DB_USER"],
|
||
|
|
"password": os.environ["DB_PASSWORD"],
|
||
|
|
"database": os.environ["DB_NAME"],
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
def _truncate_settings() -> None:
|
||
|
|
async def run():
|
||
|
|
conn = await asyncpg.connect(**_conn_kwargs())
|
||
|
|
try:
|
||
|
|
await conn.execute("DROP TABLE IF EXISTS app_settings")
|
||
|
|
finally:
|
||
|
|
await conn.close()
|
||
|
|
|
||
|
|
asyncio.run(run())
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.fixture()
|
||
|
|
def clean_settings():
|
||
|
|
import settings_store
|
||
|
|
settings_store._ensured = False
|
||
|
|
_truncate_settings()
|
||
|
|
yield
|
||
|
|
settings_store._ensured = False
|
||
|
|
_truncate_settings()
|
||
|
|
|
||
|
|
|
||
|
|
def _request(method: str, path: str, json: dict | None = None) -> httpx.Response:
|
||
|
|
async def _run() -> httpx.Response:
|
||
|
|
transport = httpx.ASGITransport(app=app)
|
||
|
|
async with httpx.AsyncClient(transport=transport, base_url=BASE) as client:
|
||
|
|
return await client.request(method, path, json=json)
|
||
|
|
|
||
|
|
return asyncio.run(_run())
|
||
|
|
|
||
|
|
|
||
|
|
def _get(path: str) -> httpx.Response:
|
||
|
|
return _request("GET", path)
|
||
|
|
|
||
|
|
|
||
|
|
def _put(path: str, json: dict) -> httpx.Response:
|
||
|
|
return _request("PUT", path, json=json)
|
||
|
|
|
||
|
|
|
||
|
|
def test_get_news_models_without_key_returns_fallback(monkeypatch):
|
||
|
|
monkeypatch.delenv("NOUS_API_KEY", raising=False)
|
||
|
|
monkeypatch.setattr("keystore.get_api_key", _missing_key)
|
||
|
|
|
||
|
|
resp = _get("/api/news/models")
|
||
|
|
assert resp.status_code == 200
|
||
|
|
body = resp.json()
|
||
|
|
assert body["source"] == "fallback"
|
||
|
|
ids = [m["id"] for m in body["models"]]
|
||
|
|
assert "Hermes-4.3-36B" in ids
|
||
|
|
from settings_store import FALLBACK_MODELS
|
||
|
|
assert ids == FALLBACK_MODELS
|
||
|
|
|
||
|
|
|
||
|
|
async def _missing_key(name: str):
|
||
|
|
return None
|
||
|
|
|
||
|
|
|
||
|
|
async def _present_key(name: str):
|
||
|
|
return "test-nous-api-key-1234"
|
||
|
|
|
||
|
|
|
||
|
|
def test_get_news_models_live_from_upstream(monkeypatch):
|
||
|
|
monkeypatch.setattr("keystore.get_api_key", _present_key)
|
||
|
|
monkeypatch.setenv("NOUS_BASE_URL", "https://inference-api.nousresearch.com/v1")
|
||
|
|
monkeypatch.delenv("OSINT_USER_AGENT", raising=False)
|
||
|
|
|
||
|
|
captured: dict = {}
|
||
|
|
|
||
|
|
class FakeResponse:
|
||
|
|
status_code = 200
|
||
|
|
|
||
|
|
def raise_for_status(self):
|
||
|
|
return None
|
||
|
|
|
||
|
|
def json(self):
|
||
|
|
return {"data": [{"id": "live-model-a"}, {"id": "Hermes-4.3-36B"}]}
|
||
|
|
|
||
|
|
async def fake_http_get(url, *, headers, timeout):
|
||
|
|
captured["url"] = url
|
||
|
|
captured["headers"] = headers
|
||
|
|
captured["timeout"] = timeout
|
||
|
|
return FakeResponse()
|
||
|
|
|
||
|
|
import settings_store
|
||
|
|
monkeypatch.setattr(settings_store, "_http_get", fake_http_get)
|
||
|
|
settings_store._models_cache = None
|
||
|
|
|
||
|
|
resp = _get("/api/news/models")
|
||
|
|
assert resp.status_code == 200
|
||
|
|
body = resp.json()
|
||
|
|
assert body["source"] == "live"
|
||
|
|
ids = [m["id"] for m in body["models"]]
|
||
|
|
assert ids == ["live-model-a", "Hermes-4.3-36B"]
|
||
|
|
assert captured["url"] == "https://inference-api.nousresearch.com/v1/models"
|
||
|
|
assert captured["headers"]["User-Agent"] == "osint-dashboard-news-summarizer"
|
||
|
|
assert captured["headers"]["Authorization"] == "Bearer test-nous-api-key-1234"
|
||
|
|
timeout = captured["timeout"]
|
||
|
|
assert timeout == 8 or getattr(timeout, "read", timeout) == 8 or float(timeout) == 8.0
|
||
|
|
|
||
|
|
|
||
|
|
def test_get_news_models_upstream_failure_returns_fallback(monkeypatch):
|
||
|
|
monkeypatch.setattr("keystore.get_api_key", _present_key)
|
||
|
|
|
||
|
|
async def boom_http_get(url, *, headers, timeout):
|
||
|
|
raise httpx.ConnectError("upstream down")
|
||
|
|
|
||
|
|
import settings_store
|
||
|
|
monkeypatch.setattr(settings_store, "_http_get", boom_http_get)
|
||
|
|
settings_store._models_cache = None
|
||
|
|
|
||
|
|
resp = _get("/api/news/models")
|
||
|
|
assert resp.status_code == 200
|
||
|
|
body = resp.json()
|
||
|
|
assert body["source"] == "fallback"
|
||
|
|
ids = [m["id"] for m in body["models"]]
|
||
|
|
assert "Hermes-4.3-36B" in ids
|
||
|
|
|
||
|
|
|
||
|
|
def test_put_settings_empty_returns_422():
|
||
|
|
resp = _put("/api/settings", {"summary_model": ""})
|
||
|
|
assert resp.status_code == 422
|
||
|
|
|
||
|
|
|
||
|
|
def test_put_settings_whitespace_only_returns_422():
|
||
|
|
resp = _put("/api/settings", {"summary_model": " "})
|
||
|
|
assert resp.status_code == 422
|
||
|
|
|
||
|
|
|
||
|
|
@requires_db
|
||
|
|
def test_put_settings_round_trip(clean_settings, monkeypatch):
|
||
|
|
monkeypatch.delenv("SUMMARY_MODEL", raising=False)
|
||
|
|
monkeypatch.delenv("NOUS_BASE_URL", raising=False)
|
||
|
|
|
||
|
|
put_resp = _put("/api/settings", {"summary_model": "google/gemini-2.5-flash"})
|
||
|
|
assert put_resp.status_code == 200
|
||
|
|
put_body = put_resp.json()
|
||
|
|
assert put_body["summary_model"] == "google/gemini-2.5-flash"
|
||
|
|
assert put_body["nous_base_url"] == "https://inference-api.nousresearch.com/v1"
|
||
|
|
|
||
|
|
get_resp = _get("/api/settings")
|
||
|
|
assert get_resp.status_code == 200
|
||
|
|
get_body = get_resp.json()
|
||
|
|
assert get_body["summary_model"] == "google/gemini-2.5-flash"
|
||
|
|
assert get_body["nous_base_url"] == "https://inference-api.nousresearch.com/v1"
|
||
|
|
assert set(get_body.keys()) == {"summary_model", "nous_base_url"}
|