"""Tests for setup_service and settings_repo.""" from __future__ import annotations import asyncio import inspect from pathlib import Path import aiosqlite import pytest from app.db import init_db from app.repositories import settings_repo from app.services import setup_service @pytest.fixture async def db(tmp_path: Path) -> aiosqlite.Connection: # type: ignore[misc] """Provide an initialised aiosqlite connection for service-level tests.""" conn: aiosqlite.Connection = await aiosqlite.connect(str(tmp_path / "test.db")) conn.row_factory = aiosqlite.Row await init_db(conn) yield conn await conn.close() class TestIsSetupComplete: async def test_returns_false_on_fresh_db( self, db: aiosqlite.Connection ) -> None: """Setup is not complete on a fresh database.""" assert await setup_service.is_setup_complete(db) is False async def test_returns_true_after_run_setup( self, db: aiosqlite.Connection ) -> None: """Setup is marked complete after run_setup() succeeds.""" await setup_service.run_setup( db, master_password="mypassword1", database_path="bangui.db", fail2ban_socket="/var/run/fail2ban/fail2ban.sock", timezone="UTC", session_duration_minutes=60, ) assert await setup_service.is_setup_complete(db) is True class TestRunSetup: async def test_persists_all_settings(self, db: aiosqlite.Connection) -> None: """run_setup() stores every provided setting.""" await setup_service.run_setup( db, master_password="mypassword1", database_path="/data/bangui.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["fail2ban_socket"] == "/tmp/f2b.sock" assert all_settings["timezone"] == "Europe/Berlin" assert all_settings["session_duration_minutes"] == "120" async def test_password_stored_as_bcrypt_hash( self, db: aiosqlite.Connection ) -> None: """The master password is stored as a bcrypt hash, not plain text.""" import bcrypt await setup_service.run_setup( db, master_password="mypassword1", database_path="bangui.db", fail2ban_socket="/var/run/fail2ban/fail2ban.sock", timezone="UTC", session_duration_minutes=60, ) stored = await setup_service.get_password_hash(db) assert stored is not None assert stored != "mypassword1" # Verify it is a valid bcrypt hash. assert bcrypt.checkpw(b"mypassword1", stored.encode()) async def test_raises_if_setup_already_complete( self, db: aiosqlite.Connection ) -> None: """run_setup() raises RuntimeError if called a second time.""" kwargs = { "master_password": "mypassword1", "database_path": "bangui.db", "fail2ban_socket": "/var/run/fail2ban/fail2ban.sock", "timezone": "UTC", "session_duration_minutes": 60, } await setup_service.run_setup(db, **kwargs) # type: ignore[arg-type] with pytest.raises(RuntimeError, match="already been completed"): await setup_service.run_setup(db, **kwargs) # type: ignore[arg-type] class TestGetTimezone: async def test_returns_utc_on_fresh_db(self, db: aiosqlite.Connection) -> None: """get_timezone() returns 'UTC' before setup is run.""" assert await setup_service.get_timezone(db) == "UTC" async def test_returns_configured_timezone( self, db: aiosqlite.Connection ) -> None: """get_timezone() returns the value set during setup.""" await setup_service.run_setup( db, master_password="mypassword1", database_path="bangui.db", fail2ban_socket="/var/run/fail2ban/fail2ban.sock", timezone="America/New_York", session_duration_minutes=60, ) assert await setup_service.get_timezone(db) == "America/New_York" class TestRunSetupAsync: """Verify the async/non-blocking bcrypt behavior of run_setup.""" async def test_run_setup_is_coroutine_function(self) -> None: """run_setup must be declared as an async function.""" assert inspect.iscoroutinefunction(setup_service.run_setup) async def test_password_hash_does_not_block_event_loop( self, db: aiosqlite.Connection ) -> None: """run_setup completes without blocking; other coroutines can interleave.""" async def noop() -> str: """A trivial coroutine that should run concurrently with setup.""" await asyncio.sleep(0) return "ok" setup_coro = setup_service.run_setup( db, master_password="mypassword1", database_path="bangui.db", fail2ban_socket="/var/run/fail2ban/fail2ban.sock", timezone="UTC", session_duration_minutes=60, ) # Both tasks should finish without error. results = await asyncio.gather(setup_coro, noop()) assert results[1] == "ok" assert await setup_service.is_setup_complete(db) is True