argus-netconf-exporter/tests/test_api_devices.py

246 lines
6.8 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.

from typing import Tuple
import sqlite3
import pytest
from fastapi.testclient import TestClient
from exporter.api import create_app, DeviceIn
from exporter.config import DeviceConfig, GlobalConfig
from exporter.metrics import TransceiverCollector
from exporter.models import DeviceHealthState, DeviceMetricsSnapshot
from exporter.registry import DeviceRegistry
from exporter.sqlite_store import PasswordEncryptor, SQLiteDeviceStore
VALID_FERNET_KEY = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
@pytest.fixture
def global_cfg(tmp_path) -> GlobalConfig:
cfg = GlobalConfig()
cfg.api_token = "changeme"
cfg.runtime_db_path = str(tmp_path / "devices.db")
cfg.password_secret = VALID_FERNET_KEY
return cfg
def _make_app_and_registry(global_cfg: GlobalConfig) -> Tuple[TestClient, DeviceRegistry]:
encryptor = PasswordEncryptor(global_cfg.password_secret)
store = SQLiteDeviceStore(global_cfg.runtime_db_path, encryptor)
store.init_db()
registry = DeviceRegistry(global_scrape_interval=global_cfg.scrape_interval_seconds)
# cache/health 可共享空 dict
cache: dict[str, DeviceMetricsSnapshot] = {}
health: dict[str, DeviceHealthState] = {}
collector = TransceiverCollector(cache, health)
app = create_app(registry, store, collector, global_cfg)
return TestClient(app), registry
@pytest.fixture
def app_with_registry(global_cfg) -> Tuple[TestClient, DeviceRegistry]:
return _make_app_and_registry(global_cfg)
def test_devicein_vendor_validator_accepts_none():
# vendor 省略时应保持为 None走 validator 的 None 分支
d = DeviceIn(
name="dev1",
host="192.0.2.1",
username="u",
password="p",
)
assert d.vendor is None
def test_get_devices_requires_auth(app_with_registry):
client, _ = app_with_registry
resp = client.get("/api/v1/devices")
assert resp.status_code == 401
def test_get_devices_returns_list(app_with_registry):
client, _ = app_with_registry
resp = client.get("/api/v1/devices", headers={"X-API-Token": "changeme"})
assert resp.status_code == 200
assert isinstance(resp.json(), list)
def test_post_device_creates_runtime_device(app_with_registry):
client, registry = app_with_registry
device_data = {
"name": "new-device",
"host": "192.168.1.100",
"port": 830,
"username": "admin",
"password": "secret",
"enabled": True,
}
resp = client.post(
"/api/v1/devices",
headers={"X-API-Token": "changeme"},
json=device_data,
)
assert resp.status_code == 201
body = resp.json()
assert body["name"] == "new-device"
assert body["source"] == "runtime"
# registry 中也应该有
devices = registry.list_devices()
assert any(d.name == "new-device" and d.source == "runtime" for d in devices)
def test_post_device_accepts_vendor_and_normalizes(app_with_registry):
client, registry = app_with_registry
device_data = {
"name": "rj-dev",
"host": "192.168.1.200",
"port": 830,
"username": "admin",
"password": "secret",
"enabled": True,
"vendor": " Ruijie ",
}
resp = client.post(
"/api/v1/devices",
headers={"X-API-Token": "changeme"},
json=device_data,
)
assert resp.status_code == 201
body = resp.json()
# API 返回的 vendor 应已被 strip + lower
assert body["vendor"] == "ruijie"
# registry 中也应保存规范化后的 vendor
devices = registry.list_devices()
dev = next(d for d in devices if d.name == "rj-dev")
assert dev.vendor == "ruijie"
def test_post_duplicate_device_returns_409(app_with_registry):
client, _ = app_with_registry
device_data = {
"name": "dup-dev",
"host": "192.168.1.100",
"port": 830,
"username": "admin",
"password": "secret",
"enabled": True,
}
resp1 = client.post(
"/api/v1/devices",
headers={"X-API-Token": "changeme"},
json=device_data,
)
assert resp1.status_code == 201
resp2 = client.post(
"/api/v1/devices",
headers={"X-API-Token": "changeme"},
json=device_data,
)
assert resp2.status_code == 409
def test_delete_runtime_device(app_with_registry):
client, _ = app_with_registry
device_data = {
"name": "to-delete",
"host": "192.168.1.100",
"port": 830,
"username": "admin",
"password": "secret",
"enabled": True,
}
client.post(
"/api/v1/devices",
headers={"X-API-Token": "changeme"},
json=device_data,
)
resp = client.delete(
"/api/v1/devices/to-delete",
headers={"X-API-Token": "changeme"},
)
assert resp.status_code == 204
devices = client.get(
"/api/v1/devices",
headers={"X-API-Token": "changeme"},
).json()
assert "to-delete" not in [d["name"] for d in devices]
def test_delete_static_device_fails(app_with_registry):
client, registry = app_with_registry
static_dev = DeviceConfig(
name="static-1",
host="10.0.0.2",
port=830,
username="u",
password="p",
vendor="h3c",
source="static",
)
registry.register_static_device(static_dev)
resp = client.delete(
"/api/v1/devices/static-1",
headers={"X-API-Token": "changeme"},
)
assert resp.status_code == 400
assert "static device" in resp.json()["detail"].lower()
def test_healthz_endpoint(app_with_registry):
client, _ = app_with_registry
resp = client.get("/healthz")
assert resp.status_code == 200
data = resp.json()
assert "status" in data
assert "devices_total" in data
def test_metrics_endpoint_returns_prometheus_format(app_with_registry):
client, _ = app_with_registry
resp = client.get("/metrics")
assert resp.status_code == 200
assert "text/plain" in resp.headers["content-type"]
assert "# HELP" in resp.text
assert "netconf_scrape_success" in resp.text
def test_get_devices_when_api_token_disabled(tmp_path):
# 当 global.api_token 为空时,/api/v1/devices 不应要求鉴权
gc = GlobalConfig()
gc.api_token = ""
gc.runtime_db_path = str(tmp_path / "devices.db")
gc.password_secret = VALID_FERNET_KEY
encryptor = PasswordEncryptor(gc.password_secret)
store = SQLiteDeviceStore(gc.runtime_db_path, encryptor)
store.init_db()
registry = DeviceRegistry(global_scrape_interval=gc.scrape_interval_seconds)
cache: dict[str, DeviceMetricsSnapshot] = {}
health: dict[str, DeviceHealthState] = {}
collector = TransceiverCollector(cache, health)
app = create_app(registry, store, collector, gc)
client = TestClient(app)
resp = client.get("/api/v1/devices")
assert resp.status_code == 200