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 个测试通过
This commit is contained in:
gouki
2026-10-15 21:18:54 +00:00
parent d6a5ec8b71
commit 90695c82fd
3 changed files with 702 additions and 0 deletions
+225
View File
@@ -0,0 +1,225 @@
"""核心 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