161 lines
4.8 KiB
Python
161 lines
4.8 KiB
Python
import sqlite3
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from exporter.config import DeviceConfig
|
|
from exporter.sqlite_store import PasswordEncryptor, SQLiteDeviceStore
|
|
|
|
|
|
@pytest.fixture
|
|
def encryptor() -> PasswordEncryptor:
|
|
# 生成一个有效 Fernet key
|
|
from cryptography.fernet import Fernet
|
|
|
|
key = Fernet.generate_key().decode()
|
|
return PasswordEncryptor(key)
|
|
|
|
|
|
def test_init_db_creates_vendor_column_on_fresh_db(tmp_path: Path, encryptor: PasswordEncryptor):
|
|
db_path = tmp_path / "test_vendor.db"
|
|
store = SQLiteDeviceStore(str(db_path), encryptor)
|
|
store.init_db()
|
|
|
|
conn = sqlite3.connect(str(db_path))
|
|
cols = [row[1] for row in conn.execute("PRAGMA table_info(devices)").fetchall()]
|
|
conn.close()
|
|
|
|
assert "vendor" in cols
|
|
|
|
|
|
def test_init_db_alter_table_vendor_preserves_existing_rows(tmp_path: Path, encryptor: PasswordEncryptor):
|
|
db_path = tmp_path / "legacy.db"
|
|
conn = sqlite3.connect(str(db_path))
|
|
# 创建旧版本 devices 表(无 vendor 列)
|
|
conn.execute(
|
|
"""
|
|
CREATE TABLE devices (
|
|
name TEXT PRIMARY KEY,
|
|
host TEXT NOT NULL,
|
|
port INTEGER NOT NULL,
|
|
username TEXT NOT NULL,
|
|
password_cipher BLOB NOT NULL,
|
|
enabled INTEGER NOT NULL,
|
|
scrape_interval_seconds INTEGER,
|
|
supports_xpath INTEGER NOT NULL DEFAULT 0,
|
|
created_at INTEGER NOT NULL,
|
|
updated_at INTEGER NOT NULL
|
|
);
|
|
"""
|
|
)
|
|
conn.execute(
|
|
"INSERT INTO devices (name, host, port, username, password_cipher, enabled, "
|
|
"scrape_interval_seconds, supports_xpath, created_at, updated_at) "
|
|
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
("old-dev", "h", 830, "u", b"cipher", 1, None, 0, 1, 1),
|
|
)
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
store = SQLiteDeviceStore(str(db_path), encryptor)
|
|
store.init_db()
|
|
|
|
conn2 = sqlite3.connect(str(db_path))
|
|
row = conn2.execute("SELECT name, vendor FROM devices WHERE name = 'old-dev'").fetchone()
|
|
conn2.close()
|
|
|
|
assert row is not None
|
|
assert row[0] == "old-dev"
|
|
# 旧数据 vendor 应为空
|
|
assert row[1] is None
|
|
|
|
|
|
def test_save_and_load_device_persists_vendor(tmp_path: Path, encryptor: PasswordEncryptor):
|
|
db_path = tmp_path / "vendor_persist.db"
|
|
store = SQLiteDeviceStore(str(db_path), encryptor)
|
|
store.init_db()
|
|
|
|
dev = DeviceConfig(
|
|
name="dev-vendor",
|
|
host="h",
|
|
port=830,
|
|
username="u",
|
|
password="p",
|
|
enabled=True,
|
|
vendor="ruijie",
|
|
source="runtime",
|
|
)
|
|
store.save_device(dev)
|
|
loaded = store.load_runtime_devices()
|
|
|
|
assert len(loaded) == 1
|
|
assert loaded[0].name == "dev-vendor"
|
|
assert loaded[0].vendor == "ruijie"
|
|
|
|
|
|
def test_save_and_load_device_vendor_none_roundtrip(tmp_path: Path, encryptor: PasswordEncryptor):
|
|
db_path = tmp_path / "vendor_none.db"
|
|
store = SQLiteDeviceStore(str(db_path), encryptor)
|
|
store.init_db()
|
|
|
|
dev = DeviceConfig(
|
|
name="dev-no-vendor",
|
|
host="h",
|
|
port=830,
|
|
username="u",
|
|
password="p",
|
|
enabled=True,
|
|
vendor=None,
|
|
source="runtime",
|
|
)
|
|
store.save_device(dev)
|
|
loaded = store.load_runtime_devices()
|
|
|
|
assert len(loaded) == 1
|
|
assert loaded[0].name == "dev-no-vendor"
|
|
assert loaded[0].vendor is None
|
|
|
|
|
|
def test_init_db_alter_table_silently_ignores_duplicate_column_error(tmp_path: Path, encryptor: PasswordEncryptor):
|
|
db_path = tmp_path / "dup_col.db"
|
|
store = SQLiteDeviceStore(str(db_path), encryptor)
|
|
# 第一次初始化,创建带 vendor 列的表
|
|
store.init_db()
|
|
# 第二次调用,不应抛异常
|
|
store.init_db()
|
|
|
|
|
|
def test_init_db_alter_table_raises_on_non_duplicate_errors(tmp_path: Path, encryptor: PasswordEncryptor, monkeypatch):
|
|
db_path = tmp_path / "locked.db"
|
|
|
|
real_connect = sqlite3.connect
|
|
|
|
class ConnWrapper:
|
|
def __init__(self, inner: sqlite3.Connection) -> None:
|
|
self._inner = inner
|
|
self._alter_attempted = False
|
|
|
|
def execute(self, sql: str, *args, **kwargs):
|
|
# 在第一次尝试 ALTER TABLE 时注入错误
|
|
if "ALTER TABLE devices ADD COLUMN vendor" in sql and not self._alter_attempted:
|
|
self._alter_attempted = True
|
|
raise sqlite3.OperationalError("database is locked")
|
|
return self._inner.execute(sql, *args, **kwargs)
|
|
|
|
def commit(self) -> None:
|
|
return self._inner.commit()
|
|
|
|
def close(self) -> None:
|
|
return self._inner.close()
|
|
|
|
def wrapped_connect(*args, **kwargs):
|
|
conn = real_connect(*args, **kwargs)
|
|
return ConnWrapper(conn)
|
|
|
|
monkeypatch.setattr(sqlite3, "connect", wrapped_connect)
|
|
|
|
store = SQLiteDeviceStore(str(db_path), encryptor)
|
|
with pytest.raises(sqlite3.OperationalError):
|
|
store.init_db()
|
|
|