from fastapi.testclient import TestClient from app.db.repositories import 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_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