from fastapi.testclient import TestClient from app.tests.conftest import TEST_USER_EMAIL, TEST_USER_PASSWORD def test_status_reports_setup_required_then_not(client: TestClient) -> None: res = client.get("/api/auth/status") assert res.status_code == 200 body = res.json() assert body["setup_required"] is True assert body["needs_setup"] is True res = client.post( "/api/auth/setup", json={ "email": "first@lifetrack.local", "password": "long-enough-pw", "display_name": "Premier", }, ) assert res.status_code == 201 body = res.json() assert body["token_type"] == "bearer" assert body["access_token"] assert body["user"]["email"] == "first@lifetrack.local" res = client.get("/api/auth/status") assert res.json()["setup_required"] is False def test_setup_forbidden_once_a_user_exists(client: TestClient, user) -> None: res = client.post( "/api/auth/setup", json={ "email": "other@lifetrack.local", "password": "long-enough-pw", "display_name": "Intrus", }, ) assert res.status_code == 403 body = res.json() assert body["error"]["code"] == "forbidden" assert body["error"]["message"] def test_login_ok_and_wrong_password(client: TestClient, user) -> None: res = client.post( "/api/auth/login", json={"email": TEST_USER_EMAIL, "password": TEST_USER_PASSWORD}, ) assert res.status_code == 200 assert res.json()["access_token"] res = client.post( "/api/auth/login", json={"email": TEST_USER_EMAIL, "password": "wrong-password"}, ) assert res.status_code == 401 assert res.json()["error"]["code"] == "unauthorized" def test_me_requires_auth(client: TestClient, user, auth_headers) -> None: res = client.get("/api/auth/me") assert res.status_code == 401 assert res.json()["error"]["code"] == "unauthorized" res = client.get("/api/auth/me", headers=auth_headers) assert res.status_code == 200 body = res.json() assert body["email"] == TEST_USER_EMAIL assert "password_hash" not in body def test_patch_me_password_change_requires_current( client: TestClient, user, auth_headers ) -> None: res = client.patch( "/api/auth/me", headers=auth_headers, json={"password": "new-password-123"}, ) assert res.status_code == 422 res = client.patch( "/api/auth/me", headers=auth_headers, json={ "password": "new-password-123", "current_password": TEST_USER_PASSWORD, "display_name": "Renommé", }, ) assert res.status_code == 200 assert res.json()["display_name"] == "Renommé" res = client.post( "/api/auth/login", json={"email": TEST_USER_EMAIL, "password": "new-password-123"}, ) assert res.status_code == 200 def test_device_key_lifecycle(client: TestClient, user, auth_headers) -> None: # Create: plaintext key returned exactly once, shaped ltk__. res = client.post( "/api/auth/device-keys", headers=auth_headers, json={"name": "Pixel 8 – pont Health Connect", "scopes": ["ingest:health"]}, ) assert res.status_code == 201 created = res.json() key = created["key"] assert key.startswith("ltk_") prefix = key.split("_", 2)[1] assert len(prefix) == 8 assert created["key_prefix"] == prefix assert created["scopes"] == ["ingest:health"] # List never exposes the plaintext key. res = client.get("/api/auth/device-keys", headers=auth_headers) assert res.status_code == 200 keys = res.json() assert len(keys) == 1 assert "key" not in keys[0] assert keys[0]["key_prefix"] == prefix assert keys[0]["revoked_at"] is None # Revoke (DELETE keeps the row, sets revoked_at). res = client.delete(f"/api/auth/device-keys/{created['id']}", headers=auth_headers) assert res.status_code == 204 res = client.get("/api/auth/device-keys", headers=auth_headers) assert res.json()[0]["revoked_at"] is not None def test_device_key_invalid_scope_rejected( client: TestClient, user, auth_headers ) -> None: res = client.post( "/api/auth/device-keys", headers=auth_headers, json={"name": "Mauvaise clé", "scopes": ["admin:*"]}, ) assert res.status_code == 422 def test_error_shape_on_unknown_route(client: TestClient) -> None: res = client.get("/api/definitely-not-a-route") assert res.status_code == 404 assert set(res.json()["error"].keys()) == {"code", "message", "details"}