diff --git a/backend/src/alphapulse/api/risk.py b/backend/src/alphapulse/api/risk.py index 8cae5b3..d8ccfb3 100644 --- a/backend/src/alphapulse/api/risk.py +++ b/backend/src/alphapulse/api/risk.py @@ -1,6 +1,7 @@ """Authenticated news risk research endpoints.""" from datetime import UTC, datetime, timedelta -from typing import Annotated, Literal +from enum import IntEnum +from typing import Annotated from fastapi import APIRouter, Depends, HTTPException, Query from pydantic import BaseModel, Field from sqlalchemy import select, func @@ -17,6 +18,12 @@ router = APIRouter(prefix='/api/risk', tags=['risk'], dependencies=[Depends(requ DB = Annotated[AsyncSession, Depends(get_session)] +class RiskWindow(IntEnum): + DAY = 24 + TWO_DAYS = 48 + WEEK = 168 + + class SettingsIn(BaseModel): enabled: bool = False provider_id: int = Field(gt=0) @@ -72,7 +79,7 @@ async def coverage(session): @router.get('/overview') -async def overview(session: DB, hours: Literal[24, 48, 168] = 24): +async def overview(session: DB, hours: RiskWindow = RiskWindow.DAY): now = datetime.now(UTC); cutoff = now - timedelta(hours=hours) counts = dict((await session.execute(select(RiskEvent.stars, func.count()).where( RiskEvent.updated_at >= cutoff, RiskEvent.result["verification"].astext != "insufficient").group_by(RiskEvent.stars))).all()) @@ -86,7 +93,7 @@ async def overview(session: DB, hours: Literal[24, 48, 168] = 24): @router.get('/events') -async def events(session: DB, hours: Literal[24, 48, 168] | None = None, stars: int = Query(1, ge=0, le=5), +async def events(session: DB, hours: RiskWindow | None = None, stars: int = Query(1, ge=0, le=5), q: str = '', start: datetime | None = None, end: datetime | None = None, page: int = Query(1, ge=1), limit: int = Query(20, ge=1, le=100)): conditions = [RiskEvent.stars >= stars] diff --git a/backend/tests/test_news_risk.py b/backend/tests/test_news_risk.py index 7838c8d..934cc13 100644 --- a/backend/tests/test_news_risk.py +++ b/backend/tests/test_news_risk.py @@ -141,3 +141,30 @@ def test_context_budget_includes_unicode_and_tool_schema(risk): from types import SimpleNamespace provider = SimpleNamespace(max_tokens=256, context_window=4096) assert not risk.fits_context(provider, risk.SYSTEM, {'content': '风'*1000}, risk.Labels) + + +@pytest.mark.parametrize('endpoint', ['overview', 'events']) +@pytest.mark.parametrize('hours,status', [(None, 200), ('24', 200), ('48', 200), ('168', 200), ('25', 422), ('0', 422), ('abc', 422)]) +def test_window_query_parameters_over_http(risk, monkeypatch, endpoint, hours, status): + from types import SimpleNamespace + from unittest.mock import AsyncMock + from fastapi import FastAPI + import httpx + from alphapulse.api import risk as api + + session = SimpleNamespace(execute=AsyncMock(return_value=SimpleNamespace(all=lambda: [])), + scalar=AsyncMock(return_value=None), scalars=AsyncMock(return_value=SimpleNamespace(all=lambda: []))) + app = FastAPI() + app.include_router(api.router) + app.dependency_overrides[api.require_admin] = lambda: None + app.dependency_overrides[api.get_session] = lambda: session + monkeypatch.setattr(api, 'coverage', AsyncMock(return_value={})) + monkeypatch.setattr(api.redis, 'get', AsyncMock(return_value=None)) + + async def check(): + async with httpx.AsyncClient(transport=httpx.ASGITransport(app), base_url='http://test') as client: + response = await client.get('/api/risk/'+endpoint, params={} if hours is None else {'hours': hours}) + assert response.status_code == status, response.text + if status == 200: + assert response.json()['code'] == 0 + asyncio.run(check()) diff --git a/scripts/smoke_app.py b/scripts/smoke_app.py index 1f67b6f..72b22ee 100644 --- a/scripts/smoke_app.py +++ b/scripts/smoke_app.py @@ -46,6 +46,12 @@ async def main(): response.raise_for_status() assert response.json()['code'] == 0, path assert 'CI synthetic news' in (await client.get('/api/news?q=CI')).text + for endpoint in ('overview', 'events'): + for hours in (24, 48, 168): + response = await client.get('/api/risk/'+endpoint, params={'hours': hours}) + response.raise_for_status() + assert response.json()['code'] == 0 + assert (await client.get('/api/risk/'+endpoint, params={'hours': 25})).status_code == 422 item = (await client.get('/api/news?q=CI')).json()['data']['items'][0] selected = await client.get('/api/news', params=[('ids', item['id']), ('ids', 2147483647)]) assert selected.status_code == 200 and [n['id'] for n in selected.json()['data']['items']] == [item['id']]