Files
lifetrack/apps/api/app/tests/test_auth.py
T
MeeJayandClaude Opus 5 93f0689c1e Initial import: LifeTrack v1 (santé, vape, finances)
Tracker de vie auto-hébergé : suivi poids/calories/sport avec planning de
pesées, sevrage tabac (vape) avec modèle de coût DIY et économies, et
finances personnelles avec import de relevés bancaires.

Architecture : FastAPI + SQLAlchemy 2.0 + PostgreSQL 16, React 18 + TS +
Vite + Tailwind + ECharts, déploiement Docker Compose. Modules
auto-découverts des deux côtés (pkgutil / import.meta.glob) et framework
de connecteurs à deux voies (importeurs de fichiers + ingestion JSON)
pour brancher de nouvelles sources sans toucher au noyau.

Validé : 292 tests pytest, tsc + vite build, contrat API/web vérifié
contre le schéma OpenAPI, et déploiement Docker réel sur PostgreSQL 16
(28 tables, SPA servie par nginx, wizard de premier démarrage).

Documentation : README.md, docs/GUIDE.md, CONVENTIONS.md, et les
documents de conception et de recherche dans docs/.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 10:48:57 +02:00

150 lines
4.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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_<prefix>_<secret>.
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"}