fix: stabilize test suite by ensuring proper test DB isolation and initialization
This commit is contained in:
@@ -210,7 +210,7 @@ def status() -> None:
|
||||
table.add_column("Hostname")
|
||||
table.add_column("Status")
|
||||
|
||||
running = {c.name: c.status for c in client.containers.list(all=True)}
|
||||
running = {c.name: c.status for c in client.containers.list(all=True, ignore_removed=True)}
|
||||
|
||||
for decky in config.deckies:
|
||||
statuses = []
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import uuid
|
||||
from contextlib import asynccontextmanager
|
||||
from datetime import timedelta
|
||||
from typing import Any, AsyncGenerator, Optional
|
||||
@@ -20,7 +19,7 @@ from decnet.web.auth import (
|
||||
)
|
||||
from decnet.web.sqlite_repository import SQLiteRepository
|
||||
from decnet.web.ingester import log_ingestion_worker
|
||||
from decnet.env import DECNET_ADMIN_USER, DECNET_ADMIN_PASSWORD, DECNET_DEVELOPER
|
||||
from decnet.env import DECNET_DEVELOPER
|
||||
import asyncio
|
||||
|
||||
repo: SQLiteRepository = SQLiteRepository()
|
||||
@@ -39,22 +38,6 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
||||
except Exception:
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
# Create default admin if no users exist
|
||||
try:
|
||||
_admin_user: Optional[dict[str, Any]] = await repo.get_user_by_username(DECNET_ADMIN_USER)
|
||||
if not _admin_user:
|
||||
await repo.create_user(
|
||||
{
|
||||
"uuid": str(uuid.uuid4()),
|
||||
"username": DECNET_ADMIN_USER,
|
||||
"password_hash": get_password_hash(DECNET_ADMIN_PASSWORD),
|
||||
"role": "admin",
|
||||
"must_change_password": True # nosec B105
|
||||
}
|
||||
)
|
||||
except Exception: # nosec B110
|
||||
pass
|
||||
|
||||
# Start background ingestion task
|
||||
if ingestion_task is None or ingestion_task.done():
|
||||
ingestion_task = asyncio.create_task(log_ingestion_worker(repo))
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import aiosqlite
|
||||
import asyncio
|
||||
from typing import Any, Optional
|
||||
from decnet.web.repository import BaseRepository
|
||||
from decnet.config import load_state, _ROOT
|
||||
@@ -9,46 +10,85 @@ class SQLiteRepository(BaseRepository):
|
||||
|
||||
def __init__(self, db_path: str = str(_ROOT / "decnet.db")) -> None:
|
||||
self.db_path: str = db_path
|
||||
self._initialize_sync()
|
||||
|
||||
async def initialize(self) -> None:
|
||||
def _initialize_sync(self) -> None:
|
||||
"""Initialize the database schema synchronously to ensure reliability."""
|
||||
import sqlite3
|
||||
with sqlite3.connect(self.db_path) as _conn:
|
||||
import uuid
|
||||
import os
|
||||
from decnet.env import DECNET_ADMIN_USER, DECNET_ADMIN_PASSWORD
|
||||
from decnet.web.auth import get_password_hash
|
||||
|
||||
# Ensure directory exists
|
||||
os.makedirs(os.path.dirname(os.path.abspath(self.db_path)), exist_ok=True)
|
||||
|
||||
with sqlite3.connect(self.db_path, isolation_level=None) as _conn:
|
||||
_conn.execute("PRAGMA journal_mode=WAL")
|
||||
_conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS logs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
decky TEXT,
|
||||
service TEXT,
|
||||
event_type TEXT,
|
||||
attacker_ip TEXT,
|
||||
raw_line TEXT,
|
||||
fields TEXT,
|
||||
msg TEXT
|
||||
)
|
||||
""")
|
||||
_conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
uuid TEXT PRIMARY KEY,
|
||||
username TEXT UNIQUE,
|
||||
password_hash TEXT,
|
||||
role TEXT DEFAULT 'viewer',
|
||||
must_change_password BOOLEAN DEFAULT 0
|
||||
)
|
||||
""")
|
||||
_conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS bounty (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
decky TEXT,
|
||||
service TEXT,
|
||||
attacker_ip TEXT,
|
||||
bounty_type TEXT,
|
||||
payload TEXT
|
||||
)
|
||||
""")
|
||||
_conn.commit()
|
||||
_conn.execute("PRAGMA synchronous=NORMAL")
|
||||
|
||||
_conn.execute("BEGIN IMMEDIATE")
|
||||
try:
|
||||
_conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS logs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
decky TEXT,
|
||||
service TEXT,
|
||||
event_type TEXT,
|
||||
attacker_ip TEXT,
|
||||
raw_line TEXT,
|
||||
fields TEXT,
|
||||
msg TEXT
|
||||
)
|
||||
""")
|
||||
_conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
uuid TEXT PRIMARY KEY,
|
||||
username TEXT UNIQUE,
|
||||
password_hash TEXT,
|
||||
role TEXT DEFAULT 'viewer',
|
||||
must_change_password BOOLEAN DEFAULT 0
|
||||
)
|
||||
""")
|
||||
_conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS bounty (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
decky TEXT,
|
||||
service TEXT,
|
||||
attacker_ip TEXT,
|
||||
bounty_type TEXT,
|
||||
payload TEXT
|
||||
)
|
||||
""")
|
||||
|
||||
# Ensure admin exists
|
||||
_cursor = _conn.execute("SELECT uuid FROM users WHERE username = ?", (DECNET_ADMIN_USER,))
|
||||
if not _cursor.fetchone():
|
||||
_conn.execute(
|
||||
"INSERT INTO users (uuid, username, password_hash, role, must_change_password) VALUES (?, ?, ?, ?, ?)",
|
||||
(str(uuid.uuid4()), DECNET_ADMIN_USER, get_password_hash(DECNET_ADMIN_PASSWORD), "admin", 1)
|
||||
)
|
||||
_conn.execute("COMMIT")
|
||||
except Exception:
|
||||
_conn.execute("ROLLBACK")
|
||||
raise
|
||||
|
||||
async def initialize(self) -> None:
|
||||
"""Initialize the database schema and verify it exists."""
|
||||
# Schema already initialized in __init__ via _initialize_sync
|
||||
# But we do a synchronous 'warm up' query here to ensure the file is ready for async threads
|
||||
import sqlite3
|
||||
with sqlite3.connect(self.db_path) as _conn:
|
||||
_conn.execute("SELECT count(*) FROM users")
|
||||
_conn.execute("SELECT count(*) FROM logs")
|
||||
_conn.execute("SELECT count(*) FROM bounty")
|
||||
pass
|
||||
|
||||
def reinitialize(self) -> None:
|
||||
"""Force a re-initialization of the schema (useful for tests)."""
|
||||
self._initialize_sync()
|
||||
|
||||
async def add_log(self, log_data: dict[str, Any]) -> None:
|
||||
async with aiosqlite.connect(self.db_path) as _db:
|
||||
@@ -273,11 +313,16 @@ class SQLiteRepository(BaseRepository):
|
||||
return _deckies
|
||||
|
||||
async def get_user_by_username(self, username: str) -> Optional[dict[str, Any]]:
|
||||
async with aiosqlite.connect(self.db_path) as _db:
|
||||
_db.row_factory = aiosqlite.Row
|
||||
async with _db.execute("SELECT * FROM users WHERE username = ?", (username,)) as _cursor:
|
||||
_row: Optional[aiosqlite.Row] = await _cursor.fetchone()
|
||||
return dict(_row) if _row else None
|
||||
for _ in range(3):
|
||||
try:
|
||||
async with aiosqlite.connect(self.db_path) as _db:
|
||||
_db.row_factory = aiosqlite.Row
|
||||
async with _db.execute("SELECT * FROM users WHERE username = ?", (username,)) as _cursor:
|
||||
_row = await _cursor.fetchone()
|
||||
return dict(_row) if _row else None
|
||||
except aiosqlite.OperationalError:
|
||||
await asyncio.sleep(0.1)
|
||||
return None
|
||||
|
||||
async def get_user_by_uuid(self, uuid: str) -> Optional[dict[str, Any]]:
|
||||
async with aiosqlite.connect(self.db_path) as _db:
|
||||
|
||||
Reference in New Issue
Block a user