from typing import Tuple import sqlite3 import pytest from fastapi.testclient import TestClient from exporter.api import create_app 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_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_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", 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