Update admin theme/layout and refresh README details.
Align the project baseline with the latest admin interface styling and layout structure while documenting setup and usage updates in README.
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
from tests.helpers import latest_token, register_and_verify
|
||||
|
||||
|
||||
def test_health(client):
|
||||
response = client.get("/api/v1/health")
|
||||
assert response.status_code == 200
|
||||
assert response.json()["status"] == "ok"
|
||||
|
||||
|
||||
def test_register_and_verify_and_login(client):
|
||||
register_response = client.post(
|
||||
"/api/v1/auth/register",
|
||||
json={"email": "user@example.com", "password": "Valid123"},
|
||||
)
|
||||
assert register_response.status_code == 200
|
||||
assert "user_id" not in register_response.json()
|
||||
|
||||
duplicate = client.post(
|
||||
"/api/v1/auth/register",
|
||||
json={"email": "user@example.com", "password": "Valid123"},
|
||||
)
|
||||
assert duplicate.status_code == 200
|
||||
assert "user_id" not in duplicate.json()
|
||||
|
||||
verify_response = client.post(
|
||||
"/api/v1/auth/verify-email",
|
||||
json={"token": latest_token("user@example.com", "verify_email")},
|
||||
)
|
||||
assert verify_response.status_code == 200
|
||||
|
||||
login_response = client.post(
|
||||
"/api/v1/auth/login",
|
||||
json={"email": "user@example.com", "password": "Valid123"},
|
||||
)
|
||||
assert login_response.status_code == 200
|
||||
assert "access_token" in login_response.json()
|
||||
|
||||
|
||||
def test_forgot_password_anti_enumeration(client):
|
||||
response = client.post("/api/v1/auth/forgot-password", json={"email": "missing@example.com"})
|
||||
assert response.status_code == 200
|
||||
assert "If email is registered" in response.json()["message"]
|
||||
@@ -0,0 +1,20 @@
|
||||
from tests.helpers import latest_token
|
||||
|
||||
|
||||
def test_verify_email_rejects_invalid_token(client):
|
||||
client.post("/api/v1/auth/register", json={"email": "bad@example.com", "password": "Valid123"})
|
||||
response = client.post("/api/v1/auth/verify-email", json={"token": "invalid-token"})
|
||||
assert response.status_code == 400
|
||||
assert response.json()["detail"] == "INVALID_TOKEN"
|
||||
|
||||
|
||||
def test_resend_verification_sends_new_token(client):
|
||||
client.post("/api/v1/auth/register", json={"email": "resend@example.com", "password": "Valid123"})
|
||||
first_token = latest_token("resend@example.com", "verify_email")
|
||||
|
||||
client.post("/api/v1/auth/resend-verification", json={"email": "resend@example.com"})
|
||||
second_token = latest_token("resend@example.com", "verify_email")
|
||||
assert first_token != second_token
|
||||
|
||||
verify = client.post("/api/v1/auth/verify-email", json={"token": second_token})
|
||||
assert verify.status_code == 200
|
||||
@@ -0,0 +1,19 @@
|
||||
from tests.helpers import latest_token, register_and_verify
|
||||
|
||||
|
||||
def test_auth_brute_force_lockout(client):
|
||||
register_and_verify(client, "locked@example.com")
|
||||
|
||||
for _ in range(5):
|
||||
response = client.post(
|
||||
"/api/v1/auth/login",
|
||||
json={"email": "locked@example.com", "password": "WrongPass1"},
|
||||
)
|
||||
assert response.status_code == 401
|
||||
|
||||
locked = client.post(
|
||||
"/api/v1/auth/login",
|
||||
json={"email": "locked@example.com", "password": "Valid123"},
|
||||
)
|
||||
assert locked.status_code == 429
|
||||
assert locked.json()["detail"] == "ACCOUNT_TEMPORARILY_LOCKED"
|
||||
@@ -0,0 +1,7 @@
|
||||
from app.core.password_policy import validate_password_strength
|
||||
import pytest
|
||||
|
||||
|
||||
def test_password_policy_rejects_denied_password():
|
||||
with pytest.raises(ValueError, match="too common"):
|
||||
validate_password_strength("Password123")
|
||||
@@ -0,0 +1,12 @@
|
||||
import pytest
|
||||
|
||||
from app.core.password_policy import validate_password_strength
|
||||
|
||||
|
||||
def test_password_policy_accepts_valid_password():
|
||||
assert validate_password_strength("Valid123") == "Valid123"
|
||||
|
||||
|
||||
def test_password_policy_rejects_weak_password():
|
||||
with pytest.raises(ValueError, match="uppercase letter"):
|
||||
validate_password_strength("valid123")
|
||||
@@ -0,0 +1,22 @@
|
||||
from uuid import uuid4
|
||||
|
||||
|
||||
def test_login_rate_limit(client, monkeypatch):
|
||||
from app.core.config import settings
|
||||
|
||||
monkeypatch.setattr(settings, "enable_rate_limit", True)
|
||||
|
||||
email = f"missing-{uuid4().hex}@example.com"
|
||||
for _ in range(5):
|
||||
response = client.post(
|
||||
"/api/v1/auth/login",
|
||||
json={"email": email, "password": "Valid123"},
|
||||
)
|
||||
assert response.status_code == 401
|
||||
|
||||
blocked = client.post(
|
||||
"/api/v1/auth/login",
|
||||
json={"email": email, "password": "Valid123"},
|
||||
)
|
||||
assert blocked.status_code == 429
|
||||
assert blocked.json()["detail"] == "RATE_LIMIT_EXCEEDED"
|
||||
@@ -0,0 +1,47 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from tests.helpers import register_and_verify
|
||||
|
||||
|
||||
def _admin_headers(client) -> dict[str, str]:
|
||||
from app.core.security import create_access_token
|
||||
from app.modules.users.repository import get_user_by_email
|
||||
|
||||
admin = get_user_by_email("admin@compton.example")
|
||||
token = create_access_token(admin.id, admin.role, admin.is_superuser)
|
||||
return {"Authorization": f"Bearer {token}"}
|
||||
|
||||
|
||||
def test_refresh_fails_for_blocked_user(client):
|
||||
register_and_verify(client, "blocked-refresh@example.com")
|
||||
login = client.post(
|
||||
"/api/v1/auth/login",
|
||||
json={"email": "blocked-refresh@example.com", "password": "Valid123"},
|
||||
)
|
||||
assert login.status_code == 200
|
||||
admin_headers = _admin_headers(client)
|
||||
me = client.get(
|
||||
"/api/v1/users/me",
|
||||
headers={"Authorization": f"Bearer {login.json()['access_token']}"},
|
||||
)
|
||||
user_id = me.json()["user"]["id"]
|
||||
blocked = client.patch(
|
||||
f"/api/v1/admin/users/{user_id}",
|
||||
headers=admin_headers,
|
||||
json={"status": "blocked"},
|
||||
)
|
||||
assert blocked.status_code == 200
|
||||
refresh = client.post("/api/v1/auth/refresh", headers={"Origin": "http://localhost:5173"})
|
||||
assert refresh.status_code == 401
|
||||
|
||||
|
||||
def test_refresh_requires_origin_header_when_cookie_present(client):
|
||||
register_and_verify(client, "origin-required@example.com")
|
||||
login = client.post(
|
||||
"/api/v1/auth/login",
|
||||
json={"email": "origin-required@example.com", "password": "Valid123"},
|
||||
)
|
||||
assert login.status_code == 200
|
||||
response = client.post("/api/v1/auth/refresh")
|
||||
assert response.status_code == 403
|
||||
assert response.json()["detail"] == "INVALID_ORIGIN"
|
||||
@@ -0,0 +1,26 @@
|
||||
from app.core.security import (
|
||||
create_access_token,
|
||||
decode_access_token,
|
||||
generate_refresh_token,
|
||||
hash_password,
|
||||
hash_refresh_token,
|
||||
verify_password,
|
||||
)
|
||||
|
||||
|
||||
def test_password_hashing_roundtrip():
|
||||
hashed = hash_password("Strong123")
|
||||
assert verify_password("Strong123", hashed) is True
|
||||
|
||||
|
||||
def test_access_token_encode_decode():
|
||||
token = create_access_token("u1", "user", False)
|
||||
payload = decode_access_token(token)
|
||||
assert payload["sub"] == "u1"
|
||||
assert payload["role"] == "user"
|
||||
assert payload["is_superuser"] is False
|
||||
|
||||
|
||||
def test_refresh_token_hashing():
|
||||
token = generate_refresh_token()
|
||||
assert hash_refresh_token(token) == hash_refresh_token(token)
|
||||
Reference in New Issue
Block a user