c0310ed573
- Add pytest test suite: test_auth.py, test_admin.py, test_pass.py, test_proxy.py, test_rate_limit.py, test_client_contract.py - Fix SQLite 'database is locked' errors: moved log_audit() calls outside with get_db() blocks in register, login, logout, refresh, activate_pass - Enable WAL mode and busy_timeout in get_db() for concurrent access - Fix /admin/me: removed non-existent 'email' column from query - Fix /admin/users list: disambiguated activated_at column in JOIN query - Fix /auth/refresh: now returns refresh_token + expires_in + username/uuid/role to match AuthManager.AuthSession expectations; revokes old refresh token - Fix conftest.py: unique usernames per test to avoid conflicts - All 47 tests passing
143 lines
5.5 KiB
Python
143 lines
5.5 KiB
Python
"""Tests verifying server responses match client (AuthManager.java) expectations."""
|
|
import pytest
|
|
from tests.conftest import auth_headers
|
|
|
|
|
|
class TestAuthResponseContract:
|
|
"""Verify /auth/register and /auth/login response fields match AuthSession.java."""
|
|
|
|
def test_register_has_all_session_fields(self, client):
|
|
"""Client expects: access_token, refresh_token, expires_in, uuid, username, role."""
|
|
resp = client.post("/auth/register", json={
|
|
"username": "contracttest",
|
|
"password": "ContractPass123"
|
|
})
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
|
|
# AuthManager.AuthSession fields
|
|
assert "access_token" in data, "Client needs access_token"
|
|
assert "refresh_token" in data, "Client needs refresh_token"
|
|
assert "expires_in" in data, "Client needs expires_in (int)"
|
|
assert "uuid" in data, "Client needs uuid"
|
|
assert "username" in data, "Client needs username"
|
|
assert "role" in data, "Client needs role (int)"
|
|
|
|
# Type checks
|
|
assert isinstance(data["access_token"], str)
|
|
assert isinstance(data["refresh_token"], str)
|
|
assert isinstance(data["expires_in"], int)
|
|
assert isinstance(data["uuid"], str)
|
|
assert isinstance(data["role"], int)
|
|
assert data["expires_in"] > 0 # Must be positive seconds
|
|
|
|
def test_login_has_all_session_fields(self, client, registered_user):
|
|
resp = client.post("/auth/login", json=registered_user)
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
|
|
assert "access_token" in data
|
|
assert "refresh_token" in data
|
|
assert "expires_in" in data
|
|
assert "uuid" in data
|
|
assert "username" in data
|
|
assert "role" in data
|
|
|
|
assert isinstance(data["expires_in"], int)
|
|
assert isinstance(data["role"], int)
|
|
|
|
|
|
class TestValidateResponseContract:
|
|
"""Verify /auth/validate response matches client expectations."""
|
|
|
|
def test_validate_valid_response_fields(self, client, logged_in_user):
|
|
"""Client checks: valid (bool), username, uuid, role."""
|
|
resp = client.post("/auth/validate", json={
|
|
"username": logged_in_user["username"],
|
|
"uuid": logged_in_user["uuid"]
|
|
}, headers=auth_headers(logged_in_user["access_token"]))
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
|
|
assert "valid" in data
|
|
assert isinstance(data["valid"], bool)
|
|
assert data["valid"] is True
|
|
assert "username" in data
|
|
assert "uuid" in data
|
|
|
|
def test_validate_invalid_response_fields(self, client):
|
|
resp = client.post("/auth/validate", json={
|
|
"username": "test",
|
|
"uuid": "test"
|
|
}, headers=auth_headers("bad.token"))
|
|
assert resp.status_code == 401 # Invalid token returns 401
|
|
|
|
|
|
class TestAdminMeResponseContract:
|
|
"""Verify /admin/me response matches UserInfo.java expectations."""
|
|
|
|
def test_admin_me_has_all_userinfo_fields(self, client, logged_in_user):
|
|
"""
|
|
Client UserInfo.java expects:
|
|
id (int), username, uuid, role (int), role_name, has_pass (bool), permissions (list)
|
|
"""
|
|
resp = client.get("/admin/me", headers=auth_headers(logged_in_user["access_token"]))
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
|
|
assert "id" in data, "UserInfo needs id"
|
|
assert "username" in data
|
|
assert "uuid" in data
|
|
assert "role" in data, "UserInfo needs role"
|
|
assert "role_name" in data, "UserInfo needs role_name"
|
|
assert "has_pass" in data, "UserInfo needs has_pass"
|
|
assert "permissions" in data, "UserInfo needs permissions"
|
|
|
|
# Type checks
|
|
assert isinstance(data["id"], int)
|
|
assert isinstance(data["role"], int)
|
|
assert isinstance(data["has_pass"], bool)
|
|
assert isinstance(data["permissions"], list)
|
|
assert isinstance(data["role_name"], str)
|
|
|
|
|
|
class TestErrorResponseContract:
|
|
"""Verify error responses match client extractError() parsing."""
|
|
|
|
def test_error_has_detail_field(self, client):
|
|
"""Client parses json.detail (string or array with msg)."""
|
|
resp = client.post("/auth/login", json={
|
|
"username": "nonexistent",
|
|
"password": "wrong"
|
|
})
|
|
# FastAPI returns 422 for validation errors, auth errors return 401
|
|
assert resp.status_code in (401, 422)
|
|
data = resp.json()
|
|
assert "detail" in data, "Client expects 'detail' field in errors"
|
|
assert isinstance(data["detail"], (str, list))
|
|
|
|
def test_validation_error_has_detail_array(self, client):
|
|
"""FastAPI 422 returns detail as array of {loc, msg, type}."""
|
|
resp = client.post("/auth/register", json={
|
|
"username": "ab",
|
|
"password": "x"
|
|
})
|
|
assert resp.status_code == 422
|
|
data = resp.json()
|
|
assert "detail" in data
|
|
assert isinstance(data["detail"], list)
|
|
assert "msg" in data["detail"][0]
|
|
|
|
|
|
class TestPackResponseContract:
|
|
"""Verify /packs response matches client expectations."""
|
|
|
|
def test_packs_response_structure(self, client, logged_in_user):
|
|
resp = client.get("/packs", headers=auth_headers(logged_in_user["access_token"]))
|
|
# May return 200 or 401/403 depending on auth setup
|
|
assert resp.status_code in (200, 401, 403)
|
|
if resp.status_code == 200:
|
|
data = resp.json()
|
|
assert "packs" in data
|
|
assert isinstance(data["packs"], list)
|