feat: Stage 1 — backend and frontend scaffolding
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
This commit is contained in:
1
backend/tests/test_repositories/__init__.py
Normal file
1
backend/tests/test_repositories/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Repository test package."""
|
||||
69
backend/tests/test_repositories/test_db_init.py
Normal file
69
backend/tests/test_repositories/test_db_init.py
Normal file
@@ -0,0 +1,69 @@
|
||||
"""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.
|
||||
Reference in New Issue
Block a user