Files
mywhoosh2garmin/tests/web/test_users.py
Bastian Wagner 85b0d861b4 feat: restyle admin UI and add per-user sync run history
Adds a self-hosted stylesheet (no CDN dependencies) with a card-based
dashboard and color-coded status badges, and shows the last 10 sync
runs per user on the detail page.
2026-08-15 20:19:31 +02:00

387 lines
12 KiB
Python

from fastapi.testclient import TestClient
from app.db.models import SyncRunStatus
from app.db.repositories import SyncRunRepository, UserRepository
from app.security.credentials import CredentialCipher
def login(client: TestClient) -> None:
page = client.get("/login")
csrf = extract_csrf(page.text)
response = client.post(
"/login",
data={"password": "admin-secret", "csrf_token": csrf},
follow_redirects=False,
)
assert response.status_code == 303
def extract_csrf(html: str) -> str:
marker = 'name="csrf_token" value="'
start = html.index(marker) + len(marker)
end = html.index('"', start)
return html[start:end]
def create_user_via_http(client: TestClient) -> int:
page = client.get("/users/new")
csrf = extract_csrf(page.text)
response = client.post(
"/users",
data={
"csrf_token": csrf,
"name": "Max",
"mywhoosh_email": "max@example.com",
"mywhoosh_password": "mw-secret",
"garmin_email": "max-garmin@example.com",
"garmin_password": "garmin-secret",
"enabled": "on",
},
follow_redirects=False,
)
assert response.status_code == 303
location = response.headers["location"]
return int(location.rsplit("/", 1)[-1])
def test_create_user_encrypts_credentials_and_never_renders_them(client: TestClient) -> None:
login(client)
page = client.get("/users/new")
assert page.status_code == 200
csrf = extract_csrf(page.text)
response = client.post(
"/users",
data={
"csrf_token": csrf,
"name": "Max",
"mywhoosh_email": "max@example.com",
"mywhoosh_password": "mw-secret",
"garmin_email": "max-garmin@example.com",
"garmin_password": "garmin-secret",
"enabled": "on",
},
follow_redirects=True,
)
assert response.status_code == 200
assert "mw-secret" not in response.text
assert "garmin-secret" not in response.text
def test_create_user_persists_encrypted_credentials(client: TestClient) -> None:
login(client)
user_id = create_user_via_http(client)
with client.app.state.session_factory() as session:
user = UserRepository(session).get(user_id)
assert user is not None
assert user.mywhoosh_email_enc != "max@example.com"
assert user.mywhoosh_password_enc != "mw-secret"
cipher = CredentialCipher(client.app.state.settings.credential_encryption_key)
assert cipher.decrypt(user.mywhoosh_email_enc) == "max@example.com"
assert cipher.decrypt(user.mywhoosh_password_enc) == "mw-secret"
assert cipher.decrypt(user.garmin_email_enc) == "max-garmin@example.com"
assert cipher.decrypt(user.garmin_password_enc) == "garmin-secret"
def test_edit_page_never_renders_passwords(client: TestClient) -> None:
login(client)
user_id = create_user_via_http(client)
page = client.get(f"/users/{user_id}/edit")
assert page.status_code == 200
assert "mw-secret" not in page.text
assert "garmin-secret" not in page.text
# emails may be shown for display
assert "max@example.com" in page.text
assert "max-garmin@example.com" in page.text
def test_edit_user_blank_password_preserves_existing_password(client: TestClient) -> None:
login(client)
user_id = create_user_via_http(client)
edit_page = client.get(f"/users/{user_id}/edit")
csrf = extract_csrf(edit_page.text)
response = client.post(
f"/users/{user_id}",
data={
"csrf_token": csrf,
"name": "Max Updated",
"mywhoosh_email": "max@example.com",
"mywhoosh_password": "",
"garmin_email": "max-garmin@example.com",
"garmin_password": "",
"enabled": "on",
},
follow_redirects=True,
)
assert response.status_code == 200
assert "mw-secret" not in response.text
assert "garmin-secret" not in response.text
with client.app.state.session_factory() as session:
user = UserRepository(session).get(user_id)
assert user is not None
assert user.name == "Max Updated"
cipher = CredentialCipher(client.app.state.settings.credential_encryption_key)
assert cipher.decrypt(user.mywhoosh_password_enc) == "mw-secret"
assert cipher.decrypt(user.garmin_password_enc) == "garmin-secret"
def test_edit_user_replaces_password_when_submitted(client: TestClient) -> None:
login(client)
user_id = create_user_via_http(client)
edit_page = client.get(f"/users/{user_id}/edit")
csrf = extract_csrf(edit_page.text)
response = client.post(
f"/users/{user_id}",
data={
"csrf_token": csrf,
"name": "Max",
"mywhoosh_email": "max@example.com",
"mywhoosh_password": "new-mw-secret",
"garmin_email": "max-garmin@example.com",
"garmin_password": "",
"enabled": "on",
},
follow_redirects=True,
)
assert response.status_code == 200
assert "new-mw-secret" not in response.text
with client.app.state.session_factory() as session:
user = UserRepository(session).get(user_id)
assert user is not None
cipher = CredentialCipher(client.app.state.settings.credential_encryption_key)
assert cipher.decrypt(user.mywhoosh_password_enc) == "new-mw-secret"
assert cipher.decrypt(user.garmin_password_enc) == "garmin-secret"
def test_user_detail_page_shows_no_secrets(client: TestClient) -> None:
login(client)
user_id = create_user_via_http(client)
response = client.get(f"/users/{user_id}")
assert response.status_code == 200
assert "mw-secret" not in response.text
assert "garmin-secret" not in response.text
assert "max@example.com" not in response.text
assert "max-garmin@example.com" not in response.text
assert "Max" in response.text
def test_user_detail_page_shows_recent_sync_runs(client: TestClient) -> None:
login(client)
user_id = create_user_via_http(client)
with client.app.state.session_factory() as session:
repo = SyncRunRepository(session)
run = repo.start(user_id)
repo.finish(
run.id,
status=SyncRunStatus.SUCCESS,
discovered=3,
imported=2,
skipped=1,
failed=0,
)
response = client.get(f"/users/{user_id}")
assert response.status_code == 200
assert "Recent sync runs" in response.text
assert "success" in response.text
assert ">3<" in response.text
assert ">2<" in response.text
def test_unknown_user_returns_404_for_detail(client: TestClient) -> None:
login(client)
response = client.get("/users/999999")
assert response.status_code == 404
def test_unknown_user_returns_404_for_edit(client: TestClient) -> None:
login(client)
response = client.get("/users/999999/edit")
assert response.status_code == 404
def test_unknown_user_returns_404_for_update(client: TestClient) -> None:
login(client)
page = client.get("/users/new")
csrf = extract_csrf(page.text)
response = client.post(
"/users/999999",
data={
"csrf_token": csrf,
"name": "Nobody",
"mywhoosh_email": "a@example.com",
"mywhoosh_password": "",
"garmin_email": "b@example.com",
"garmin_password": "",
"enabled": "on",
},
)
assert response.status_code == 404
def test_create_user_requires_admin(client: TestClient) -> None:
response = client.get("/users/new", follow_redirects=False)
assert response.status_code == 303
assert response.headers["location"] == "/login"
def test_create_user_rejects_invalid_csrf(client: TestClient) -> None:
login(client)
response = client.post(
"/users",
data={
"csrf_token": "invalid-token",
"name": "Max",
"mywhoosh_email": "max@example.com",
"mywhoosh_password": "mw-secret",
"garmin_email": "max-garmin@example.com",
"garmin_password": "garmin-secret",
"enabled": "on",
},
)
assert response.status_code == 403
def test_create_user_rejects_non_ascii_csrf_token(client: TestClient) -> None:
login(client)
response = client.post(
"/users",
data={
"csrf_token": "invalid-tokeü",
"name": "Max",
"mywhoosh_email": "max@example.com",
"mywhoosh_password": "mw-secret",
"garmin_email": "max-garmin@example.com",
"garmin_password": "garmin-secret",
"enabled": "on",
},
)
assert response.status_code == 403
def test_dashboard_lists_created_user_without_secrets(client: TestClient) -> None:
login(client)
create_user_via_http(client)
response = client.get("/")
assert response.status_code == 200
assert "Max" in response.text
assert "mw-secret" not in response.text
assert "garmin-secret" not in response.text
def _create_payload(**overrides: str) -> dict:
payload = {
"name": "Max",
"mywhoosh_email": "max@example.com",
"mywhoosh_password": "mw-secret",
"garmin_email": "max-garmin@example.com",
"garmin_password": "garmin-secret",
"enabled": "on",
}
payload.update(overrides)
return payload
def test_create_user_rejects_empty_mywhoosh_email(client: TestClient) -> None:
login(client)
page = client.get("/users/new")
csrf = extract_csrf(page.text)
response = client.post(
"/users",
data={"csrf_token": csrf, **_create_payload(mywhoosh_email="")},
)
assert response.status_code == 400
def test_create_user_rejects_empty_mywhoosh_password(client: TestClient) -> None:
login(client)
page = client.get("/users/new")
csrf = extract_csrf(page.text)
response = client.post(
"/users",
data={"csrf_token": csrf, **_create_payload(mywhoosh_password="")},
)
assert response.status_code == 400
def test_create_user_rejects_empty_garmin_email(client: TestClient) -> None:
login(client)
page = client.get("/users/new")
csrf = extract_csrf(page.text)
response = client.post(
"/users",
data={"csrf_token": csrf, **_create_payload(garmin_email="")},
)
assert response.status_code == 400
def test_create_user_rejects_empty_garmin_password(client: TestClient) -> None:
login(client)
page = client.get("/users/new")
csrf = extract_csrf(page.text)
response = client.post(
"/users",
data={"csrf_token": csrf, **_create_payload(garmin_password="")},
)
assert response.status_code == 400
def test_create_user_rejects_whitespace_only_email(client: TestClient) -> None:
login(client)
page = client.get("/users/new")
csrf = extract_csrf(page.text)
response = client.post(
"/users",
data={"csrf_token": csrf, **_create_payload(mywhoosh_email=" ")},
)
assert response.status_code == 400
def test_update_user_rejects_empty_mywhoosh_email(client: TestClient) -> None:
login(client)
user_id = create_user_via_http(client)
edit_page = client.get(f"/users/{user_id}/edit")
csrf = extract_csrf(edit_page.text)
response = client.post(
f"/users/{user_id}",
data={
"csrf_token": csrf,
"name": "Max",
"mywhoosh_email": "",
"mywhoosh_password": "",
"garmin_email": "max-garmin@example.com",
"garmin_password": "",
"enabled": "on",
},
)
assert response.status_code == 400
def test_update_user_rejects_empty_garmin_email(client: TestClient) -> None:
login(client)
user_id = create_user_via_http(client)
edit_page = client.get(f"/users/{user_id}/edit")
csrf = extract_csrf(edit_page.text)
response = client.post(
f"/users/{user_id}",
data={
"csrf_token": csrf,
"name": "Max",
"mywhoosh_email": "max@example.com",
"mywhoosh_password": "",
"garmin_email": "",
"garmin_password": "",
"enabled": "on",
},
)
assert response.status_code == 400