"""核心 router HTTP smoke test:验证 assets / accounts / providers / stats 路由装配与基本 CRUD 使用 TestClient 挂载真实 app,依赖覆盖到 tmp_path 临时库,覆盖: - GET 列表接口返回 200 + 正确结构(空库 / 有数据) - POST 创建 → GET 详情 → PUT 更新 → DELETE 删除的完整链路 - /api/stats/overview 和 /api/stats/expiring 返回正确结构 - /api/providers/adapters 返回适配器元信息 安全前提:TestClient 使用本机回环来源(可信内网),读操作无需 API Key; 写操作同样来自可信内网,不走 fail-closed 分支。 全程免凭证、免网络:不触发 lifespan,get_session / get_metrics_session 覆盖到临时库。 运行:.venv/bin/pytest tests/test_router_smoke.py -v """ import pytest from fastapi.testclient import TestClient from sqlmodel import Session, SQLModel, create_engine import app.models.asset # noqa: F401 import app.models.credential # noqa: F401 import app.models.monitor # noqa: F401 import app.models.provider # noqa: F401 import app.models.ssl # noqa: F401 from app.core.config import settings from app.database import get_metrics_session, get_session from app.main import app @pytest.fixture() def client(tmp_path, monkeypatch): """挂载真实 app 的 TestClient:依赖覆盖到临时库""" assets_eng = create_engine( f"sqlite:///{tmp_path / 'assets.db'}", connect_args={"check_same_thread": False} ) metrics_eng = create_engine( f"sqlite:///{tmp_path / 'metrics.db'}", connect_args={"check_same_thread": False} ) SQLModel.metadata.create_all(assets_eng) SQLModel.metadata.create_all(metrics_eng) def _assets_session(): with Session(assets_eng, expire_on_commit=False) as s: yield s def _metrics_session(): with Session(metrics_eng, expire_on_commit=False) as s: yield s app.dependency_overrides[get_session] = _assets_session app.dependency_overrides[get_metrics_session] = _metrics_session monkeypatch.setattr(settings, "API_KEY", "") monkeypatch.setattr(settings, "AGENT_KEY", "") yield TestClient(app, client=("127.0.0.1", 0)) app.dependency_overrides.clear() assets_eng.dispose() metrics_eng.dispose() # ============================= /api/assets ============================= # class TestAssetsRouter: def test_list_empty(self, client): resp = client.get("/api/assets") assert resp.status_code == 200 assert resp.json() == [] def test_create_and_get(self, client): body = { "name": "smoke-vps", "asset_type": "vps", "provider": "vultr", "vps_detail": {"ip_address": "10.0.0.1"}, } resp = client.post("/api/assets", json=body) assert resp.status_code == 201 created = resp.json() assert created["name"] == "smoke-vps" assert created["vps_detail"]["ip_address"] == "10.0.0.1" # GET 详情 resp = client.get(f"/api/assets/{created['id']}") assert resp.status_code == 200 assert resp.json()["name"] == "smoke-vps" def test_create_list_then_delete(self, client): body = { "name": "del-vps", "asset_type": "vps", "provider": "vultr", "vps_detail": {"ip_address": "10.0.0.2"}, } created = client.post("/api/assets", json=body).json() assert len(client.get("/api/assets").json()) == 1 resp = client.delete(f"/api/assets/{created['id']}") assert resp.status_code == 204 assert client.get("/api/assets").json() == [] def test_get_nonexistent_returns_404(self, client): resp = client.get("/api/assets/999999") assert resp.status_code == 404 # ============================= /api/accounts ============================= # class TestAccountsRouter: def test_list_empty(self, client): resp = client.get("/api/accounts") assert resp.status_code == 200 assert resp.json() == [] def test_create_and_list(self, client): body = {"name": "test@example.com", "platform": "vultr"} resp = client.post("/api/accounts", json=body) assert resp.status_code == 201 created = resp.json() assert created["name"] == "test@example.com" assert created["platform"] == "vultr" listing = client.get("/api/accounts").json() assert len(listing) == 1 assert listing[0]["name"] == "test@example.com" def test_create_duplicate_returns_400(self, client): body = {"name": "dup@example.com", "platform": "vultr"} client.post("/api/accounts", json=body) resp = client.post("/api/accounts", json=body) assert resp.status_code == 400 def test_delete_account(self, client): body = {"name": "del@example.com", "platform": "vultr"} created = client.post("/api/accounts", json=body).json() resp = client.delete(f"/api/accounts/{created['id']}") assert resp.status_code == 200 assert "affected_assets" in resp.json() assert client.get("/api/accounts").json() == [] # ============================= /api/providers ============================= # class TestProvidersRouter: def test_list_empty(self, client): resp = client.get("/api/providers") assert resp.status_code == 200 assert resp.json() == [] def test_create_and_get(self, client): body = { "slug": "test-provider", "name": "Test Provider", "category": "vps", } resp = client.post("/api/providers", json=body) assert resp.status_code == 201 created = resp.json() assert created["slug"] == "test-provider" resp = client.get(f"/api/providers/{created['id']}") assert resp.status_code == 200 assert resp.json()["name"] == "Test Provider" def test_create_duplicate_slug_returns_400(self, client): body = {"slug": "dup-slug", "name": "First", "category": "vps"} client.post("/api/providers", json=body) body["name"] = "Second" resp = client.post("/api/providers", json=body) assert resp.status_code == 400 def test_adapters_endpoint(self, client): resp = client.get("/api/providers/adapters") assert resp.status_code == 200 data = resp.json() assert "adapters" in data assert "supported_types" in data assert "capabilities" in data def test_delete_provider(self, client): body = {"slug": "del-provider", "name": "Del", "category": "vps"} created = client.post("/api/providers", json=body).json() resp = client.delete(f"/api/providers/{created['id']}") assert resp.status_code == 204 assert client.get("/api/providers").json() == [] # ============================= /api/stats ============================= # class TestStatsRouter: def test_overview_empty(self, client): resp = client.get("/api/stats/overview") assert resp.status_code == 200 data = resp.json() assert data["total"] == 0 assert "year_cost" in data assert "expiring_30" in data assert "overdue_count" in data def test_expiring_empty(self, client): resp = client.get("/api/stats/expiring") assert resp.status_code == 200 assert resp.json() == [] def test_overview_with_data(self, client): from datetime import date # 先创建一个带到期日的资产(year_cost 只统计本年到期资产) body = { "name": "stats-vps", "asset_type": "vps", "provider": "vultr", "vps_detail": {"ip_address": "10.0.0.3"}, "cost": 12.5, "expiry_date": str(date.today()), } client.post("/api/assets", json=body) data = client.get("/api/stats/overview").json() assert data["total"] == 1 assert data["year_cost"] == 12.5 def test_expiring_param_validation(self, client): """days 参数有范围限制(1-365)""" resp = client.get("/api/stats/expiring?days=0") assert resp.status_code == 422 resp = client.get("/api/stats/expiring?days=366") assert resp.status_code == 422 resp = client.get("/api/stats/expiring?days=30") assert resp.status_code == 200