gabrielpamplonapg
Beta gates: rights attestation, demo top-up cap, 3 signup credits, hash-only reference DB
6596834 | """Tests for API key creation, listing, revocation, and authentication.""" | |
| import os | |
| import sys | |
| import uuid | |
| import math | |
| import wave | |
| import struct | |
| import io | |
| import pytest | |
| from fastapi.testclient import TestClient | |
| sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) | |
| from backend.main import app | |
| from tests.conftest import TestingSessionLocal | |
| # conftest.py already sets app.dependency_overrides[get_db] | |
| client = TestClient(app) | |
| def _register(): | |
| email = f"u_{uuid.uuid4().hex[:8]}@test.com" | |
| resp = client.post("/api/auth/register", json={"email": email, "password": "Pass12345X!"}) | |
| assert resp.status_code == 201 | |
| return resp.json()["access_token"], email | |
| def _headers(token): | |
| return {"Authorization": f"Bearer {token}"} | |
| def _wav(): | |
| n = int(0.5 * 22050) | |
| buf = io.BytesIO() | |
| with wave.open(buf, "wb") as wf: | |
| wf.setnchannels(1); wf.setsampwidth(2); wf.setframerate(22050) | |
| wf.writeframes(struct.pack(f"<{n}h", *[int(32767 * 0.3 * math.sin(2*math.pi*440*i/22050)) for i in range(n)])) | |
| return buf.getvalue() | |
| # ββ Create βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def test_create_api_key(): | |
| token, _ = _register() | |
| resp = client.post("/api/keys", json={"label": "Test Key"}, headers=_headers(token)) | |
| assert resp.status_code == 201 | |
| data = resp.json() | |
| assert data["key"].startswith("msk_") | |
| assert len(data["key"]) > 20 | |
| assert "warning" in data | |
| assert data["label"] == "Test Key" | |
| def test_create_api_key_default_label(): | |
| token, _ = _register() | |
| resp = client.post("/api/keys", json={}, headers=_headers(token)) | |
| assert resp.status_code == 201 | |
| assert resp.json()["label"] == "My API Key" | |
| def test_create_api_key_unauthenticated(): | |
| resp = client.post("/api/keys", json={"label": "x"}) | |
| assert resp.status_code == 401 | |
| # ββ List βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def test_list_api_keys_empty(): | |
| token, _ = _register() | |
| resp = client.get("/api/keys", headers=_headers(token)) | |
| assert resp.status_code == 200 | |
| assert resp.json()["keys"] == [] | |
| def test_list_api_keys_shows_prefix_only(): | |
| token, _ = _register() | |
| create_resp = client.post("/api/keys", json={"label": "K1"}, headers=_headers(token)) | |
| raw_key = create_resp.json()["key"] | |
| list_resp = client.get("/api/keys", headers=_headers(token)) | |
| keys = list_resp.json()["keys"] | |
| assert len(keys) == 1 | |
| # Should show prefix with ellipsis, NOT the full key | |
| assert keys[0]["prefix"].endswith("β¦") | |
| assert raw_key not in str(keys[0]) | |
| def test_list_api_keys_multiple(): | |
| token, _ = _register() | |
| for i in range(3): | |
| client.post("/api/keys", json={"label": f"Key {i}"}, headers=_headers(token)) | |
| resp = client.get("/api/keys", headers=_headers(token)) | |
| assert len(resp.json()["keys"]) == 3 | |
| # ββ Revoke βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def test_revoke_api_key(): | |
| token, _ = _register() | |
| create_resp = client.post("/api/keys", json={"label": "Temp"}, headers=_headers(token)) | |
| key_id = create_resp.json()["id"] | |
| del_resp = client.delete(f"/api/keys/{key_id}", headers=_headers(token)) | |
| assert del_resp.status_code == 204 | |
| # Should show as inactive | |
| list_resp = client.get("/api/keys", headers=_headers(token)) | |
| key = next(k for k in list_resp.json()["keys"] if k["id"] == key_id) | |
| assert key["is_active"] is False | |
| def test_revoke_other_users_key(): | |
| token_a, _ = _register() | |
| token_b, _ = _register() | |
| create_resp = client.post("/api/keys", json={"label": "A Key"}, headers=_headers(token_a)) | |
| key_id = create_resp.json()["id"] | |
| resp = client.delete(f"/api/keys/{key_id}", headers=_headers(token_b)) | |
| assert resp.status_code == 404 | |
| # ββ Auth via X-API-Key header ββββββββββββββββββββββββββββββββββββββββββββββ | |
| def test_auth_via_api_key(): | |
| token, _ = _register() | |
| create_resp = client.post("/api/keys", json={"label": "Auth Test"}, headers=_headers(token)) | |
| raw_key = create_resp.json()["key"] | |
| # Use X-API-Key instead of JWT | |
| resp = client.get("/api/user/me", headers={"X-API-Key": raw_key}) | |
| assert resp.status_code == 200 | |
| def test_auth_via_revoked_api_key(): | |
| token, _ = _register() | |
| create_resp = client.post("/api/keys", json={"label": "Revoked"}, headers=_headers(token)) | |
| raw_key = create_resp.json()["key"] | |
| key_id = create_resp.json()["id"] | |
| # Revoke it | |
| client.delete(f"/api/keys/{key_id}", headers=_headers(token)) | |
| # Should now fail | |
| resp = client.get("/api/user/me", headers={"X-API-Key": raw_key}) | |
| assert resp.status_code == 401 | |
| def test_api_key_can_analyze(): | |
| token, _ = _register() | |
| create_resp = client.post("/api/keys", json={"label": "Analyze Key"}, headers=_headers(token)) | |
| raw_key = create_resp.json()["key"] | |
| resp = client.post( | |
| "/api/analyze?mode=offline&rights_attested=true", | |
| files={"file": ("test.wav", _wav(), "audio/wav")}, | |
| headers={"X-API-Key": raw_key}, | |
| ) | |
| assert resp.status_code == 200 | |
| def test_invalid_api_key_rejected(): | |
| resp = client.get("/api/user/me", headers={"X-API-Key": "msk_invalidkey"}) | |
| assert resp.status_code == 401 | |
| # ββ Admin ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def test_admin_stats_requires_admin(): | |
| token, _ = _register() | |
| resp = client.get("/api/admin/stats", headers=_headers(token)) | |
| assert resp.status_code == 403 | |
| def test_admin_stats_accessible_to_admin(): | |
| token, email = _register() | |
| # Promote to admin via DB | |
| db = TestingSessionLocal() | |
| from backend.models import User as UserModel | |
| user = db.query(UserModel).filter(UserModel.email == email).first() | |
| user.is_admin = True | |
| db.commit() | |
| db.close() | |
| resp = client.get("/api/admin/stats", headers=_headers(token)) | |
| assert resp.status_code == 200 | |
| data = resp.json() | |
| assert "total_users" in data | |
| assert "total_analyses" in data | |
| assert "total_revenue_usd" in data | |
| assert "training_dataset" in data | |
| def test_admin_list_users(): | |
| token, email = _register() | |
| db = TestingSessionLocal() | |
| from backend.models import User as UserModel | |
| user = db.query(UserModel).filter(UserModel.email == email).first() | |
| user.is_admin = True | |
| db.commit() | |
| db.close() | |
| resp = client.get("/api/admin/users", headers=_headers(token)) | |
| assert resp.status_code == 200 | |
| data = resp.json() | |
| assert "users" in data | |
| assert "total" in data | |
| assert data["total"] >= 1 | |
| def test_admin_add_credits(): | |
| token, email = _register() | |
| db = TestingSessionLocal() | |
| from backend.models import User as UserModel | |
| user = db.query(UserModel).filter(UserModel.email == email).first() | |
| user.is_admin = True | |
| target_id = user.id | |
| db.commit() | |
| db.close() | |
| resp = client.post(f"/api/admin/users/{target_id}/add-credits?amount=50", | |
| headers=_headers(token)) | |
| assert resp.status_code == 200 | |
| assert resp.json()["credits"] >= 50 | |
| # ββ /api/user/me returns is_admin ββββββββββββββββββββββββββββββββββββββββββ | |
| def test_me_returns_is_admin_false(): | |
| token, _ = _register() | |
| resp = client.get("/api/user/me", headers=_headers(token)) | |
| assert resp.status_code == 200 | |
| assert resp.json()["is_admin"] is False | |
| def test_me_returns_is_admin_true(): | |
| token, email = _register() | |
| db = TestingSessionLocal() | |
| from backend.models import User as UserModel | |
| user = db.query(UserModel).filter(UserModel.email == email).first() | |
| user.is_admin = True | |
| db.commit() | |
| db.close() | |
| resp = client.get("/api/user/me", headers=_headers(token)) | |
| assert resp.status_code == 200 | |
| assert resp.json()["is_admin"] is True | |
| # ββ Change password ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def test_change_password_success(): | |
| token, _ = _register() | |
| resp = client.patch( | |
| "/api/user/me/password", | |
| json={"current_password": "Pass12345X!", "new_password": "NewPass999!"}, | |
| headers=_headers(token), | |
| ) | |
| assert resp.status_code == 204 | |
| def test_change_password_wrong_current(): | |
| token, _ = _register() | |
| resp = client.patch( | |
| "/api/user/me/password", | |
| json={"current_password": "wrongpassword", "new_password": "newpass999"}, | |
| headers=_headers(token), | |
| ) | |
| assert resp.status_code == 400 | |
| assert "incorrect" in resp.json()["detail"].lower() | |
| def test_change_password_too_short(): | |
| token, _ = _register() | |
| resp = client.patch( | |
| "/api/user/me/password", | |
| json={"current_password": "Pass12345X!", "new_password": "short"}, | |
| headers=_headers(token), | |
| ) | |
| assert resp.status_code == 422 | |
| def test_change_password_unauthenticated(): | |
| resp = client.patch( | |
| "/api/user/me/password", | |
| json={"current_password": "pass12345", "new_password": "newpass999"}, | |
| ) | |
| assert resp.status_code == 401 | |
| def test_change_password_then_login_with_new(): | |
| token, email = _register() | |
| # Change password | |
| client.patch( | |
| "/api/user/me/password", | |
| json={"current_password": "Pass12345X!", "new_password": "NewPass999!"}, | |
| headers=_headers(token), | |
| ) | |
| # Login with new password | |
| resp = client.post("/api/auth/login", json={"email": email, "password": "NewPass999!"}) | |
| assert resp.status_code == 200 | |
| assert "access_token" in resp.json() | |
| # ββ Password reset βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def test_forgot_password_always_202(): | |
| """Should return 202 even for unknown emails (no enumeration).""" | |
| resp = client.post("/api/auth/forgot-password", | |
| json={"email": "nobody@nowhere.com"}) | |
| assert resp.status_code == 202 | |
| def test_forgot_password_known_email(): | |
| _, email = _register() | |
| resp = client.post("/api/auth/forgot-password", json={"email": email}) | |
| assert resp.status_code == 202 | |
| assert "reset link" in resp.json()["message"].lower() | |
| def test_reset_password_invalid_token(): | |
| resp = client.post("/api/auth/reset-password", | |
| json={"token": "badtoken", "new_password": "newpass999"}) | |
| assert resp.status_code == 400 | |
| assert "invalid" in resp.json()["detail"].lower() | |
| def test_reset_password_full_flow(): | |
| """Forgot β get token from DB β reset β login with new password.""" | |
| _, email = _register() | |
| # Trigger forgot-password | |
| client.post("/api/auth/forgot-password", json={"email": email}) | |
| # Get the raw token from DB | |
| from backend.models import PasswordResetToken, User as UserModel | |
| db = TestingSessionLocal() | |
| user = db.query(UserModel).filter(UserModel.email == email).first() | |
| record = db.query(PasswordResetToken).filter( | |
| PasswordResetToken.user_id == user.id, | |
| PasswordResetToken.used == False, # noqa: E712 | |
| ).first() | |
| token = record.token | |
| db.close() | |
| # Reset password | |
| resp = client.post("/api/auth/reset-password", | |
| json={"token": token, "new_password": "brandnew999"}) | |
| assert resp.status_code == 204 | |
| # Login with new password | |
| resp = client.post("/api/auth/login", | |
| json={"email": email, "password": "brandnew999"}) | |
| assert resp.status_code == 200 | |
| def test_reset_token_cannot_be_reused(): | |
| _, email = _register() | |
| client.post("/api/auth/forgot-password", json={"email": email}) | |
| from backend.models import PasswordResetToken, User as UserModel | |
| db = TestingSessionLocal() | |
| user = db.query(UserModel).filter(UserModel.email == email).first() | |
| record = db.query(PasswordResetToken).filter( | |
| PasswordResetToken.user_id == user.id, | |
| PasswordResetToken.used == False, # noqa: E712 | |
| ).first() | |
| token = record.token | |
| db.close() | |
| client.post("/api/auth/reset-password", | |
| json={"token": token, "new_password": "first999pw"}) | |
| # Second use should fail | |
| resp = client.post("/api/auth/reset-password", | |
| json={"token": token, "new_password": "second999pw"}) | |
| assert resp.status_code == 400 | |
| def test_reset_password_too_short(): | |
| _, email = _register() | |
| client.post("/api/auth/forgot-password", json={"email": email}) | |
| from backend.models import PasswordResetToken, User as UserModel | |
| db = TestingSessionLocal() | |
| user = db.query(UserModel).filter(UserModel.email == email).first() | |
| record = db.query(PasswordResetToken).filter( | |
| PasswordResetToken.user_id == user.id, | |
| PasswordResetToken.used == False, # noqa: E712 | |
| ).first() | |
| token = record.token | |
| db.close() | |
| resp = client.post("/api/auth/reset-password", | |
| json={"token": token, "new_password": "short"}) | |
| assert resp.status_code == 422 | |
| # ββ Delete account (GDPR) ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def test_delete_account(): | |
| token, email = _register() | |
| resp = client.delete("/api/user/me", headers=_headers(token)) | |
| assert resp.status_code == 204 | |
| def test_deleted_account_cannot_login(): | |
| token, email = _register() | |
| client.delete("/api/user/me", headers=_headers(token)) | |
| resp = client.post("/api/auth/login", json={"email": email, "password": "Pass12345X!"}) | |
| assert resp.status_code == 401 | |
| def test_delete_account_unauthenticated(): | |
| resp = client.delete("/api/user/me") | |
| assert resp.status_code == 401 | |