Files
vps-manager/tests/test_router_smoke.py
T
gouki 90695c82fd test: 补充 renewal_service / ssl_service / 核心 router HTTP smoke test
- test_renewal_service.py: 14 个测试覆盖到期查询、文案生成、消息构建、续费检查流程
- test_ssl_service.py: 24 个测试覆盖子域名 CRUD、证书辅助函数、探测结果写入、全量刷新
- test_router_smoke.py: 16 个 HTTP smoke test 覆盖 assets/accounts/providers/stats 路由

全部 394 个测试通过
2026-10-15 21:18:54 +00:00

226 lines
8.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""核心 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