- test_renewal_service.py: 14 个测试覆盖到期查询、文案生成、消息构建、续费检查流程 - test_ssl_service.py: 24 个测试覆盖子域名 CRUD、证书辅助函数、探测结果写入、全量刷新 - test_router_smoke.py: 16 个 HTTP smoke test 覆盖 assets/accounts/providers/stats 路由 全部 394 个测试通过
226 lines
8.3 KiB
Python
226 lines
8.3 KiB
Python
"""核心 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
|