Consolidate setup persistence into bootstrap metadata and runtime DB

This commit is contained in:
2026-04-11 20:57:55 +02:00
parent cd69550053
commit ffe7ada469
6 changed files with 82 additions and 43 deletions

View File

@@ -334,6 +334,7 @@ async def test_startup_loads_geo_cache_from_persisted_runtime_database(tmp_path:
patch("app.services.geo_service.load_cache_from_db", new=load_cache),
patch("app.services.geo_service.count_unresolved", new=AsyncMock(return_value=0)),
patch("app.services.setup_service.is_setup_complete", new=AsyncMock(return_value=True)),
patch("app.services.setup_service.get_runtime_database_path", new=AsyncMock(return_value=runtime_db_path)),
patch(
"app.services.setup_service.get_persisted_runtime_settings",
new=AsyncMock(

View File

@@ -23,7 +23,7 @@ async def db(tmp_path: Path) -> aiosqlite.Connection: # type: ignore[misc]
await setup_service.run_setup(
conn,
master_password="correctpassword1",
database_path="bangui.db",
database_path=str(tmp_path / "auth.db"),
fail2ban_socket="/var/run/fail2ban/fail2ban.sock",
timezone="UTC",
session_duration_minutes=60,

View File

@@ -32,13 +32,13 @@ class TestIsSetupComplete:
assert await setup_service.is_setup_complete(db) is False
async def test_returns_true_after_run_setup(
self, db: aiosqlite.Connection
self, db: aiosqlite.Connection, tmp_path: Path
) -> None:
"""Setup is marked complete after run_setup() succeeds."""
await setup_service.run_setup(
db,
master_password="mypassword1",
database_path="bangui.db",
database_path=str(tmp_path / "test.db"),
fail2ban_socket="/var/run/fail2ban/fail2ban.sock",
timezone="UTC",
session_duration_minutes=60,
@@ -47,24 +47,59 @@ class TestIsSetupComplete:
class TestRunSetup:
async def test_persists_all_settings(self, db: aiosqlite.Connection) -> None:
"""run_setup() stores every provided setting."""
async def test_persists_all_settings(
self, db: aiosqlite.Connection, tmp_path: Path
) -> None:
"""run_setup() stores every provided setting when runtime DB equals the bootstrap DB."""
await setup_service.run_setup(
db,
master_password="mypassword1",
database_path="/data/bangui.db",
database_path=str(tmp_path / "test.db"),
fail2ban_socket="/tmp/f2b.sock",
timezone="Europe/Berlin",
session_duration_minutes=120,
)
all_settings = await settings_repo.get_all_settings(db)
assert all_settings["database_path"] == "/data/bangui.db"
assert all_settings["database_path"] == str(tmp_path / "test.db")
assert all_settings["fail2ban_socket"] == "/tmp/f2b.sock"
assert all_settings["timezone"] == "Europe/Berlin"
assert all_settings["session_duration_minutes"] == "120"
async def test_runs_setup_into_separate_runtime_database(
self, db: aiosqlite.Connection, tmp_path: Path
) -> None:
"""run_setup() stores runtime configuration in the runtime DB only."""
runtime_db_path = str(tmp_path / "runtime.db")
await setup_service.run_setup(
db,
master_password="mypassword1",
database_path=runtime_db_path,
fail2ban_socket="/tmp/f2b.sock",
timezone="Europe/Berlin",
session_duration_minutes=120,
)
bootstrap_settings = await settings_repo.get_all_settings(db)
assert bootstrap_settings["database_path"] == runtime_db_path
assert bootstrap_settings["setup_completed"] == "1"
assert "fail2ban_socket" not in bootstrap_settings
assert "timezone" not in bootstrap_settings
assert "session_duration_minutes" not in bootstrap_settings
runtime_db = await aiosqlite.connect(runtime_db_path)
runtime_db.row_factory = aiosqlite.Row
try:
runtime_settings = await settings_repo.get_all_settings(runtime_db)
finally:
await runtime_db.close()
assert runtime_settings["fail2ban_socket"] == "/tmp/f2b.sock"
assert runtime_settings["timezone"] == "Europe/Berlin"
assert runtime_settings["session_duration_minutes"] == "120"
assert runtime_settings["master_password_hash"] != "mypassword1"
async def test_password_stored_as_bcrypt_hash(
self, db: aiosqlite.Connection
self, db: aiosqlite.Connection, tmp_path: Path
) -> None:
"""The master password is stored as a bcrypt hash, not plain text."""
import bcrypt
@@ -72,7 +107,7 @@ class TestRunSetup:
await setup_service.run_setup(
db,
master_password="mypassword1",
database_path="bangui.db",
database_path=str(tmp_path / "test.db"),
fail2ban_socket="/var/run/fail2ban/fail2ban.sock",
timezone="UTC",
session_duration_minutes=60,
@@ -84,12 +119,12 @@ class TestRunSetup:
assert bcrypt.checkpw(b"mypassword1", stored.encode())
async def test_raises_if_setup_already_complete(
self, db: aiosqlite.Connection
self, db: aiosqlite.Connection, tmp_path: Path
) -> None:
"""run_setup() raises RuntimeError if called a second time."""
kwargs = {
"master_password": "mypassword1",
"database_path": "bangui.db",
"database_path": str(tmp_path / "test.db"),
"fail2ban_socket": "/var/run/fail2ban/fail2ban.sock",
"timezone": "UTC",
"session_duration_minutes": 60,
@@ -99,13 +134,13 @@ class TestRunSetup:
await setup_service.run_setup(db, **kwargs) # type: ignore[arg-type]
async def test_initializes_map_color_thresholds_with_defaults(
self, db: aiosqlite.Connection
self, db: aiosqlite.Connection, tmp_path: Path
) -> None:
"""run_setup() initializes map color thresholds with default values."""
await setup_service.run_setup(
db,
master_password="mypassword1",
database_path="bangui.db",
database_path=str(tmp_path / "test.db"),
fail2ban_socket="/var/run/fail2ban/fail2ban.sock",
timezone="UTC",
session_duration_minutes=60,
@@ -122,13 +157,13 @@ class TestGetTimezone:
assert await setup_service.get_timezone(db) == "UTC"
async def test_returns_configured_timezone(
self, db: aiosqlite.Connection
self, db: aiosqlite.Connection, tmp_path: Path
) -> None:
"""get_timezone() returns the value set during setup."""
await setup_service.run_setup(
db,
master_password="mypassword1",
database_path="bangui.db",
database_path=str(tmp_path / "test.db"),
fail2ban_socket="/var/run/fail2ban/fail2ban.sock",
timezone="America/New_York",
session_duration_minutes=60,
@@ -187,13 +222,13 @@ class TestMapColorThresholds:
)
async def test_run_setup_initializes_default_thresholds(
self, db: aiosqlite.Connection
self, db: aiosqlite.Connection, tmp_path: Path
) -> None:
"""run_setup() initializes map color thresholds with defaults."""
await setup_service.run_setup(
db,
master_password="mypassword1",
database_path="bangui.db",
database_path=str(tmp_path / "test.db"),
fail2ban_socket="/var/run/fail2ban/fail2ban.sock",
timezone="UTC",
session_duration_minutes=60,
@@ -212,7 +247,7 @@ class TestRunSetupAsync:
assert inspect.iscoroutinefunction(setup_service.run_setup)
async def test_password_hash_does_not_block_event_loop(
self, db: aiosqlite.Connection
self, db: aiosqlite.Connection, tmp_path: Path
) -> None:
"""run_setup completes without blocking; other coroutines can interleave."""
@@ -224,7 +259,7 @@ class TestRunSetupAsync:
setup_coro = setup_service.run_setup(
db,
master_password="mypassword1",
database_path="bangui.db",
database_path=str(tmp_path / "test.db"),
fail2ban_socket="/var/run/fail2ban/fail2ban.sock",
timezone="UTC",
session_duration_minutes=60,