maestroai / tests /test_api_keys.py
gabrielpamplonapg
Beta gates: rights attestation, demo top-up cap, 3 signup credits, hash-only reference DB
6596834
Raw
History Blame Contribute Delete
14.5 kB
"""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