Backend (tasks 1.1, 1.5–1.8): - pyproject.toml with FastAPI, Pydantic v2, aiosqlite, APScheduler 3.x, structlog, bcrypt; ruff + mypy strict configured - Pydantic Settings (BANGUI_ prefix env vars, fail-fast validation) - SQLite schema: settings, sessions, blocklist_sources, import_log; WAL mode + foreign keys; idempotent init_db() - FastAPI app factory with lifespan (DB, aiohttp session, scheduler), CORS, unhandled-exception handler, GET /api/health - Fail2BanClient: async Unix-socket wrapper using run_in_executor, custom error types, async context manager - Utility modules: ip_utils, time_utils, constants - 47 tests; ruff 0 errors; mypy --strict 0 errors Frontend (tasks 1.2–1.4): - Vite + React 18 + TypeScript strict; Fluent UI v9; ESLint + Prettier - Custom brand theme (#0F6CBD, WCAG AA contrast) with light/dark variants - Typed fetch API client (ApiError, get/post/put/del) + endpoints constants - tsc --noEmit 0 errors
70 lines
2.3 KiB
Python
70 lines
2.3 KiB
Python
"""Tests for app.db — database schema initialisation."""
|
|
|
|
from pathlib import Path
|
|
|
|
import aiosqlite
|
|
import pytest
|
|
|
|
from app.db import init_db
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_init_db_creates_settings_table(tmp_path: Path) -> None:
|
|
"""``init_db`` must create the ``settings`` table."""
|
|
db_path = str(tmp_path / "test.db")
|
|
async with aiosqlite.connect(db_path) as db:
|
|
await init_db(db)
|
|
async with db.execute(
|
|
"SELECT name FROM sqlite_master WHERE type='table' AND name='settings';"
|
|
) as cursor:
|
|
row = await cursor.fetchone()
|
|
assert row is not None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_init_db_creates_sessions_table(tmp_path: Path) -> None:
|
|
"""``init_db`` must create the ``sessions`` table."""
|
|
db_path = str(tmp_path / "test.db")
|
|
async with aiosqlite.connect(db_path) as db:
|
|
await init_db(db)
|
|
async with db.execute(
|
|
"SELECT name FROM sqlite_master WHERE type='table' AND name='sessions';"
|
|
) as cursor:
|
|
row = await cursor.fetchone()
|
|
assert row is not None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_init_db_creates_blocklist_sources_table(tmp_path: Path) -> None:
|
|
"""``init_db`` must create the ``blocklist_sources`` table."""
|
|
db_path = str(tmp_path / "test.db")
|
|
async with aiosqlite.connect(db_path) as db:
|
|
await init_db(db)
|
|
async with db.execute(
|
|
"SELECT name FROM sqlite_master WHERE type='table' AND name='blocklist_sources';"
|
|
) as cursor:
|
|
row = await cursor.fetchone()
|
|
assert row is not None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_init_db_creates_import_log_table(tmp_path: Path) -> None:
|
|
"""``init_db`` must create the ``import_log`` table."""
|
|
db_path = str(tmp_path / "test.db")
|
|
async with aiosqlite.connect(db_path) as db:
|
|
await init_db(db)
|
|
async with db.execute(
|
|
"SELECT name FROM sqlite_master WHERE type='table' AND name='import_log';"
|
|
) as cursor:
|
|
row = await cursor.fetchone()
|
|
assert row is not None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_init_db_is_idempotent(tmp_path: Path) -> None:
|
|
"""Calling ``init_db`` twice on the same database must not raise."""
|
|
db_path = str(tmp_path / "test.db")
|
|
async with aiosqlite.connect(db_path) as db:
|
|
await init_db(db)
|
|
await init_db(db) # Second call must be a no-op.
|