From 68005c92e6094ee7d3b7538874ac6d147f8a275f Mon Sep 17 00:00:00 2001 From: kevin9327 <5299031+kevin9327@users.noreply.github.com> Date: Wed, 9 Sep 2026 07:37:17 +0900 Subject: [PATCH] =?UTF-8?q?fix(alerts):=20=E8=A7=A6=E5=8F=91=E8=AE=B0?= =?UTF-8?q?=E5=BD=95=E6=9F=A5=E8=AF=A2=E7=9A=84=20days/limit=20=E8=A1=A5?= =?UTF-8?q?=E4=B8=8A=E8=8C=83=E5=9B=B4=E7=BA=A6=E6=9D=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GET /api/alerts 的 days、limit 是裸 int,没有任何约束,越界值不报错而是 静默返回错误结果: - days=-1 → cutoff 被推到未来,200 但 alerts 为空(total 仍是 3) - limit=-1 → out[:limit] 变成负数切片,3 条记录只返回 2 条, 静默丢掉最旧一条,调用方无从察觉 - limit=0 → 200 但 alerts 为空 同类列表端点已有正确写法:abnormal.py 用 `limit: int = Query(500, ge=1, le=2000)`,rps.py 用 `days: int = Query(12, ge=7, le=30)`,越界直接 422。 本次把 /api/alerts 补齐到同一口径,上下限取存储侧保留策略本身 (alert_store.MAX_DAYS / MAX_RECORDS),不新增常量,超出保留窗口的请求 本来也没有可返回的数据。 前端 alertsList 实际只传 days=7、limit=1/10/500,全部落在新区间内; 新增的 tests/test_alerts_query_bounds.py 同时锁住这些合法取值仍返回 200 且记录条数不变。 Co-Authored-By: Claude Opus 5 --- backend/app/api/alerts.py | 8 ++- backend/tests/test_alerts_query_bounds.py | 69 +++++++++++++++++++++++ 2 files changed, 74 insertions(+), 3 deletions(-) create mode 100644 backend/tests/test_alerts_query_bounds.py diff --git a/backend/app/api/alerts.py b/backend/app/api/alerts.py index af8a5a9..b79d0b7 100644 --- a/backend/app/api/alerts.py +++ b/backend/app/api/alerts.py @@ -5,7 +5,7 @@ import random import time from pathlib import Path -from fastapi import APIRouter, HTTPException, Request +from fastapi import APIRouter, HTTPException, Query, Request from app.services import alert_store @@ -19,8 +19,10 @@ def _data_dir(request: Request) -> Path: @router.get("") def list_alerts( request: Request, - days: int = 7, - limit: int = 5000, + # 上限取存储侧保留策略 (alert_store.MAX_DAYS / MAX_RECORDS): 超出也没有可返回的记录。 + # 无下限时 days=-1 会把 cutoff 推到未来直接返回空, limit=-1 会走负数切片静默丢掉最旧一条。 + days: int = Query(alert_store.MAX_DAYS, ge=1, le=alert_store.MAX_DAYS), + limit: int = Query(alert_store.MAX_RECORDS, ge=1, le=alert_store.MAX_RECORDS), source: str | None = None, type: str | None = None, ext_columns: str | None = None, diff --git a/backend/tests/test_alerts_query_bounds.py b/backend/tests/test_alerts_query_bounds.py new file mode 100644 index 0000000..ca8df70 --- /dev/null +++ b/backend/tests/test_alerts_query_bounds.py @@ -0,0 +1,69 @@ +"""告警记录查询入参边界 — days/limit 越界应被拦下, 合法值照常返回。 + +同类列表端点 (abnormal.py 的 limit、rps.py 的 days) 已用 Query(ge=..., le=...) +约束同名参数; 本文件锁住 /api/alerts 的同一口径。 +""" +from __future__ import annotations + +import time +from types import SimpleNamespace + +import pytest +from fastapi import FastAPI +from fastapi.testclient import TestClient + +from app.api.alerts import router +from app.services import alert_store + + +def _client(tmp_path) -> TestClient: + app = FastAPI() + app.include_router(router) + app.state.repo = SimpleNamespace(store=SimpleNamespace(data_dir=tmp_path)) + return TestClient(app) + + +def _seed(tmp_path, count: int = 3) -> None: + now_ms = int(time.time() * 1000) + alert_store.append_many( + tmp_path, + [ + {"ts": now_ms - i * 1000, "rule_id": f"r{i}", "source": "monitor", "type": "price"} + for i in range(count) + ], + ) + + +@pytest.mark.parametrize("params", [{"days": -1}, {"days": 0}, {"limit": -1}, {"limit": 0}]) +def test_out_of_range_params_are_rejected(tmp_path, params): + _seed(tmp_path) + resp = _client(tmp_path).get("/api/alerts", params=params) + assert resp.status_code == 422, resp.text + + +def test_days_and_limit_above_store_retention_are_rejected(tmp_path): + _seed(tmp_path) + client = _client(tmp_path) + # 存储侧只保留 MAX_DAYS 天 / MAX_RECORDS 条, 超出上限的请求没有可返回的数据 + assert client.get("/api/alerts", params={"days": alert_store.MAX_DAYS + 1}).status_code == 422 + assert client.get( + "/api/alerts", params={"limit": alert_store.MAX_RECORDS + 1} + ).status_code == 422 + + +def test_valid_params_still_return_all_records(tmp_path): + _seed(tmp_path, 3) + client = _client(tmp_path) + # 默认值 + body = client.get("/api/alerts").json() + assert len(body["alerts"]) == 3 + assert body["total"] == 3 + # 显式边界值 (前端实际用的 days=7 / limit=1、10、500 均在内) + for params in ( + {"days": 1, "limit": 1}, + {"days": 7, "limit": 500}, + {"days": alert_store.MAX_DAYS, "limit": alert_store.MAX_RECORDS}, + ): + resp = client.get("/api/alerts", params=params) + assert resp.status_code == 200, resp.text + assert len(resp.json()["alerts"]) == min(3, params["limit"])