Strengthen setup password validation

- Add backend Pydantic password complexity validation for setup
- Update frontend setup page with password rule feedback and strength indicator
- Add/adjust setup API tests for password validation
- Document setup password requirements
- Fix frontend test type annotation issue
This commit is contained in:
2026-04-20 19:23:12 +02:00
parent cc8c71906f
commit e593498de5
7 changed files with 241 additions and 22 deletions

View File

@@ -19,7 +19,7 @@ from app.services import setup_service
# ---------------------------------------------------------------------------
_SETUP_PAYLOAD: dict[str, object] = {
"master_password": "supersecret123",
"master_password": "Supersecret1!",
"database_path": "bangui.db",
"fail2ban_socket": "/var/run/fail2ban/fail2ban.sock",
"timezone": "UTC",
@@ -78,7 +78,7 @@ class TestGetSetupStatus:
await client.post(
"/api/setup",
json={
"master_password": "supersecret123",
"master_password": "Supersecret1!",
"database_path": "bangui.db",
"fail2ban_socket": "/var/run/fail2ban/fail2ban.sock",
"timezone": "UTC",
@@ -98,7 +98,7 @@ class TestPostSetup:
response = await client.post(
"/api/setup",
json={
"master_password": "supersecret123",
"master_password": "Supersecret1!",
"database_path": "bangui.db",
"fail2ban_socket": "/var/run/fail2ban/fail2ban.sock",
"timezone": "UTC",
@@ -117,10 +117,48 @@ class TestPostSetup:
)
assert response.status_code == 422
async def test_rejects_missing_uppercase_password(self, client: AsyncClient) -> None:
"""Setup endpoint rejects passwords missing an uppercase character."""
response = await client.post(
"/api/setup",
json={"master_password": "lowercase1!"},
)
assert response.status_code == 422
assert any(
"uppercase" in error["msg"].lower()
for error in response.json()["detail"]
)
async def test_rejects_missing_number_password(self, client: AsyncClient) -> None:
"""Setup endpoint rejects passwords missing a numeric character."""
response = await client.post(
"/api/setup",
json={"master_password": "NoNumbers!"},
)
assert response.status_code == 422
assert any(
"number" in error["msg"].lower()
for error in response.json()["detail"]
)
async def test_rejects_missing_special_character_password(
self, client: AsyncClient
) -> None:
"""Setup endpoint rejects passwords missing a required special character."""
response = await client.post(
"/api/setup",
json={"master_password": "NoSpecial1"},
)
assert response.status_code == 422
assert any(
"special character" in error["msg"].lower()
for error in response.json()["detail"]
)
async def test_rejects_second_call(self, client: AsyncClient) -> None:
"""Setup endpoint returns 409 if setup has already been completed."""
payload = {
"master_password": "supersecret123",
"master_password": "Supersecret1!",
"database_path": "bangui.db",
"fail2ban_socket": "/var/run/fail2ban/fail2ban.sock",
"timezone": "UTC",
@@ -138,7 +176,7 @@ class TestPostSetup:
"""Setup endpoint uses defaults when optional fields are omitted."""
response = await client.post(
"/api/setup",
json={"master_password": "supersecret123"},
json={"master_password": "Supersecret1!"},
)
assert response.status_code == 201
@@ -150,7 +188,7 @@ class TestPostSetupRuntimeState:
"""App state should reflect setup settings immediately after setup."""
app, client = app_and_client
payload = {
"master_password": "supersecret123",
"master_password": "Supersecret1!",
"database_path": "bangui.db",
"fail2ban_socket": "/tmp/persisted.sock",
"timezone": "Europe/Berlin",
@@ -193,7 +231,7 @@ class TestSetupRedirectMiddleware:
"""Protected endpoints are reachable (no redirect) after setup."""
await client.post(
"/api/setup",
json={"master_password": "supersecret123"},
json={"master_password": "Supersecret1!"},
)
# /api/auth/login should now be reachable (returns 405 GET not allowed,
# not a setup redirect)
@@ -220,7 +258,7 @@ class TestGetTimezone:
await client.post(
"/api/setup",
json={
"master_password": "supersecret123",
"master_password": "Supersecret1!",
"timezone": "Europe/Berlin",
},
)
@@ -413,7 +451,7 @@ class TestLifespanSetupCache:
await init_db(db)
await setup_service.run_setup(
db,
master_password="supersecret123",
master_password="Supersecret1!",
database_path=settings.database_path,
fail2ban_socket=settings.fail2ban_socket,
timezone=settings.timezone,