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,26 @@
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
def _super_headers(client) -> dict[str, str]:
|
||||
login = client.post(
|
||||
"/api/v1/auth/login",
|
||||
json={"email": "admin@compton.example", "password": "Admin1234"},
|
||||
)
|
||||
assert login.status_code == 200
|
||||
return {"Authorization": f"Bearer {login.json()['access_token']}"}
|
||||
|
||||
|
||||
def test_admin_create_user_rejects_weak_password(client):
|
||||
headers = _super_headers(client)
|
||||
response = client.post(
|
||||
"/api/v1/admin/users",
|
||||
headers=headers,
|
||||
json={
|
||||
"email": "weak-pass@example.com",
|
||||
"password": "password",
|
||||
"role": "user",
|
||||
"is_superuser": False,
|
||||
"status": "active",
|
||||
},
|
||||
)
|
||||
assert response.status_code == 422
|
||||
@@ -0,0 +1,73 @@
|
||||
from app.core.security import hash_password
|
||||
from app.modules.users import repository
|
||||
|
||||
|
||||
def _admin_headers(client):
|
||||
login = client.post(
|
||||
"/api/v1/auth/login",
|
||||
json={"email": "admin@compton.example", "password": "Admin1234"},
|
||||
)
|
||||
token = login.json()["access_token"]
|
||||
return {"Authorization": f"Bearer {token}"}
|
||||
|
||||
|
||||
def _plain_admin_headers(client):
|
||||
email = "ops-admin@compton.example"
|
||||
if not repository.get_user_by_email(email):
|
||||
repository.create_user(
|
||||
email=email,
|
||||
password_hash=hash_password("Admin1234"),
|
||||
role="admin",
|
||||
is_superuser=False,
|
||||
status="active",
|
||||
)
|
||||
login = client.post("/api/v1/auth/login", json={"email": email, "password": "Admin1234"})
|
||||
token = login.json()["access_token"]
|
||||
return {"Authorization": f"Bearer {token}"}
|
||||
|
||||
|
||||
def test_admin_users_list(client):
|
||||
response = client.get("/api/v1/admin/users", headers=_admin_headers(client))
|
||||
assert response.status_code == 200
|
||||
assert "data" in response.json()
|
||||
|
||||
|
||||
def test_non_superuser_cannot_patch_settings(client):
|
||||
response = client.patch(
|
||||
"/api/v1/admin/settings",
|
||||
json={"values": {"enable_docs": False}},
|
||||
headers=_plain_admin_headers(client),
|
||||
)
|
||||
assert response.status_code == 403
|
||||
assert response.json()["detail"] == "SUPERUSER_ONLY"
|
||||
|
||||
|
||||
def test_superuser_can_patch_settings(client):
|
||||
response = client.patch(
|
||||
"/api/v1/admin/settings",
|
||||
json={"values": {"enable_docs": False}},
|
||||
headers=_admin_headers(client),
|
||||
)
|
||||
assert response.status_code == 200
|
||||
payload = response.json()
|
||||
assert "values" in payload
|
||||
|
||||
|
||||
def test_superuser_can_create_and_delete_user(client):
|
||||
created = client.post(
|
||||
"/api/v1/admin/users",
|
||||
json={
|
||||
"email": "created-by-admin@compton.example",
|
||||
"password": "StrongPass123A",
|
||||
"role": "user",
|
||||
"is_superuser": False,
|
||||
"status": "active",
|
||||
},
|
||||
headers=_admin_headers(client),
|
||||
)
|
||||
assert created.status_code == 200
|
||||
user_id = created.json()["id"]
|
||||
|
||||
deleted = client.delete(f"/api/v1/admin/users/{user_id}", headers=_admin_headers(client))
|
||||
assert deleted.status_code == 200
|
||||
assert deleted.json()["status"] == "deleted"
|
||||
@@ -0,0 +1,35 @@
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
def _login(client, email: str, password: str) -> dict[str, str]:
|
||||
response = client.post("/api/v1/auth/login", json={"email": email, "password": password})
|
||||
assert response.status_code == 200
|
||||
return {"Authorization": f"Bearer {response.json()['access_token']}"}
|
||||
|
||||
|
||||
def test_superuser_can_read_install_secrets(client):
|
||||
headers = _login(client, "admin@compton.example", "Admin1234")
|
||||
response = client.get("/api/v1/admin/secrets", headers=headers)
|
||||
assert response.status_code == 200
|
||||
payload = response.json()
|
||||
assert "secrets_status" in payload
|
||||
assert payload["secrets_status"]["postgres_password"] in {"configured", "missing"}
|
||||
assert "POSTGRES_PASSWORD" not in str(payload)
|
||||
|
||||
|
||||
def test_non_superuser_cannot_read_install_secrets(client):
|
||||
headers = _login(client, "ops@compton.example", "OpsAdmin1234")
|
||||
response = client.get("/api/v1/admin/secrets", headers=headers)
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
def test_superuser_can_reveal_db_password(client):
|
||||
headers = _login(client, "admin@compton.example", "Admin1234")
|
||||
response = client.post(
|
||||
"/api/v1/admin/secrets/reveal",
|
||||
headers=headers,
|
||||
json={"key": "database_password"},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["key"] == "database_password"
|
||||
assert "value" in response.json()
|
||||
@@ -0,0 +1,33 @@
|
||||
from unittest.mock import patch
|
||||
|
||||
from app.modules.admin.service import patch_user
|
||||
from app.modules.users import repository
|
||||
from app.modules.users.repository import create_user, get_user_by_email
|
||||
from app.core.security import hash_password
|
||||
|
||||
|
||||
def test_admin_cannot_self_demote():
|
||||
admin = get_user_by_email("admin@compton.example")
|
||||
try:
|
||||
patch_user(admin, admin.id, "user", None)
|
||||
assert False, "Expected self-demotion error"
|
||||
except ValueError as exc:
|
||||
assert str(exc) == "SELF_DEMOTION_FORBIDDEN"
|
||||
|
||||
|
||||
def test_admin_can_promote_user():
|
||||
admin = get_user_by_email("admin@compton.example")
|
||||
regular = create_user("sample@example.com", hash_password("Valid123"), role="user", status="active")
|
||||
result = patch_user(admin, regular.id, "admin", None)
|
||||
assert result["role"] == "admin"
|
||||
|
||||
|
||||
def test_last_admin_protected():
|
||||
admin = get_user_by_email("admin@compton.example")
|
||||
target = create_user("target@example.com", hash_password("Valid123"), role="admin", status="active")
|
||||
with patch.object(repository, "count_admins", return_value=1):
|
||||
try:
|
||||
patch_user(admin, target.id, "user", None)
|
||||
assert False, "Expected last-admin protection"
|
||||
except ValueError as exc:
|
||||
assert str(exc) == "LAST_ADMIN_PROTECTED"
|
||||
@@ -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)
|
||||
@@ -0,0 +1,45 @@
|
||||
def _admin_headers(client):
|
||||
login = client.post(
|
||||
"/api/v1/auth/login",
|
||||
json={"email": "admin@compton.example", "password": "Admin1234"},
|
||||
)
|
||||
token = login.json()["access_token"]
|
||||
return {"Authorization": f"Bearer {token}"}
|
||||
|
||||
|
||||
def test_create_and_read_published_page(client):
|
||||
headers = _admin_headers(client)
|
||||
created = client.post(
|
||||
"/api/v1/content/pages",
|
||||
headers=headers,
|
||||
json={"slug": "about", "title": "About", "body": "<p>Hello</p>", "status": "published"},
|
||||
)
|
||||
assert created.status_code == 200
|
||||
|
||||
fetched = client.get("/api/v1/content/pages/about")
|
||||
assert fetched.status_code == 200
|
||||
assert fetched.json()["slug"] == "about"
|
||||
|
||||
|
||||
def test_list_all_pages_requires_admin(client):
|
||||
response = client.get("/api/v1/content/pages/manage/all")
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
def test_list_all_pages_includes_drafts(client):
|
||||
headers = _admin_headers(client)
|
||||
created = client.post(
|
||||
"/api/v1/content/pages",
|
||||
headers=headers,
|
||||
json={"slug": "draft-page", "title": "Draft", "body": "<p>Draft</p>", "status": "draft"},
|
||||
)
|
||||
assert created.status_code == 200
|
||||
|
||||
listed = client.get("/api/v1/content/pages/manage/all", headers=headers)
|
||||
assert listed.status_code == 200
|
||||
slugs = [page["slug"] for page in listed.json()["data"]]
|
||||
assert "draft-page" in slugs
|
||||
|
||||
public = client.get("/api/v1/content/pages")
|
||||
public_slugs = [page["slug"] for page in public.json()["data"]]
|
||||
assert "draft-page" not in public_slugs
|
||||
@@ -0,0 +1,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from app.modules.content.service import sanitize_html
|
||||
|
||||
|
||||
def test_sanitize_html_strips_javascript_protocol():
|
||||
raw = '<a href="javascript:alert(1)">x</a><img src="javascript:alert(1)" alt="x" />'
|
||||
clean = sanitize_html(raw)
|
||||
assert "javascript:" not in clean
|
||||
@@ -0,0 +1,21 @@
|
||||
def test_media_requires_signature(client):
|
||||
response = client.get("/api/v1/media/files/avatars/missing.png")
|
||||
assert response.status_code == 422
|
||||
|
||||
|
||||
def test_media_file_not_found(client):
|
||||
from app.core.media_signing import build_signed_media_url
|
||||
|
||||
signed_url = build_signed_media_url("/api/v1/media/files/avatars/missing.png")
|
||||
response = client.get(signed_url)
|
||||
assert response.status_code == 404
|
||||
assert response.json()["detail"] == "FILE_NOT_FOUND"
|
||||
|
||||
|
||||
def test_media_rejects_non_avatar_path(client):
|
||||
from app.core.media_signing import build_signed_media_url
|
||||
|
||||
signed_url = build_signed_media_url("/api/v1/media/files/other/file.png")
|
||||
response = client.get(signed_url)
|
||||
assert response.status_code == 404
|
||||
assert response.json()["detail"] == "FILE_NOT_FOUND"
|
||||
@@ -0,0 +1,14 @@
|
||||
import pytest
|
||||
|
||||
from app.modules.media.service import AvatarValidationError, validate_and_process_avatar
|
||||
from tests.fixtures import make_test_png
|
||||
|
||||
|
||||
def test_validate_rejects_empty():
|
||||
with pytest.raises(AvatarValidationError, match="INVALID_IMAGE"):
|
||||
validate_and_process_avatar(b"")
|
||||
|
||||
|
||||
def test_validate_rejects_corrupt_bytes():
|
||||
with pytest.raises(AvatarValidationError, match="INVALID_IMAGE"):
|
||||
validate_and_process_avatar(b"not-an-image")
|
||||
@@ -0,0 +1,7 @@
|
||||
from app.modules.notifications.service import enqueue_email
|
||||
|
||||
|
||||
def test_enqueue_email_returns_payload():
|
||||
data = enqueue_email("verify", "u@example.com", {"token": "123"})
|
||||
assert data["queued"] is True
|
||||
assert data["template"] == "verify"
|
||||
@@ -0,0 +1,170 @@
|
||||
from tests.fixtures import make_test_png
|
||||
from tests.helpers import latest_token, register_and_verify, register_verify_login
|
||||
|
||||
|
||||
def _admin_headers(client) -> dict[str, str]:
|
||||
login = client.post(
|
||||
"/api/v1/auth/login",
|
||||
json={"email": "admin@compton.example", "password": "Admin1234"},
|
||||
)
|
||||
token = login.json()["access_token"]
|
||||
return {"Authorization": f"Bearer {token}"}
|
||||
|
||||
|
||||
def test_auth_refresh_and_logout(client):
|
||||
register_and_verify(client, "refresh@example.com")
|
||||
login = client.post(
|
||||
"/api/v1/auth/login",
|
||||
json={"email": "refresh@example.com", "password": "Valid123"},
|
||||
)
|
||||
assert login.status_code == 200
|
||||
assert "refresh_token" in login.cookies
|
||||
|
||||
refresh = client.post("/api/v1/auth/refresh", headers={"Origin": "http://localhost:5173"})
|
||||
assert refresh.status_code == 200
|
||||
assert "access_token" in refresh.json()
|
||||
|
||||
logout = client.post("/api/v1/auth/logout", headers={"Origin": "http://localhost:5173"})
|
||||
assert logout.status_code == 200
|
||||
|
||||
|
||||
def test_auth_invalid_login(client):
|
||||
response = client.post(
|
||||
"/api/v1/auth/login",
|
||||
json={"email": "missing@example.com", "password": "Valid123"},
|
||||
)
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
def test_users_patch_and_password_and_avatar(client):
|
||||
headers = register_verify_login(client, "patch@example.com")
|
||||
patch = client.patch("/api/v1/users/me", headers=headers, json={"display_name": "Patched"})
|
||||
assert patch.status_code == 200
|
||||
assert patch.json()["profile"]["display_name"] == "Patched"
|
||||
|
||||
bad_password = client.post(
|
||||
"/api/v1/users/me/password",
|
||||
headers=headers,
|
||||
json={"current_password": "wrong", "new_password": "NewValid1"},
|
||||
)
|
||||
assert bad_password.status_code == 400
|
||||
|
||||
avatar = client.post(
|
||||
"/api/v1/users/me/avatar",
|
||||
headers=headers,
|
||||
files={"file": ("avatar.png", make_test_png(), "image/png")},
|
||||
)
|
||||
assert avatar.status_code == 200
|
||||
assert avatar.json()["profile"]["avatar_url"]
|
||||
|
||||
|
||||
def test_users_unauthorized(client):
|
||||
response = client.get("/api/v1/users/me")
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
def test_content_crud_flow(client):
|
||||
headers = _admin_headers(client)
|
||||
created = client.post(
|
||||
"/api/v1/content/pages",
|
||||
headers=headers,
|
||||
json={"slug": "terms", "title": "Terms", "body": "<p>Terms</p>", "status": "draft"},
|
||||
)
|
||||
assert created.status_code == 200
|
||||
page_id = created.json()["id"]
|
||||
|
||||
listed = client.get("/api/v1/content/pages")
|
||||
assert listed.status_code == 200
|
||||
|
||||
updated = client.patch(
|
||||
f"/api/v1/content/pages/{page_id}",
|
||||
headers=headers,
|
||||
json={"status": "published"},
|
||||
)
|
||||
assert updated.status_code == 200
|
||||
|
||||
fetched = client.get("/api/v1/content/pages/terms")
|
||||
assert fetched.status_code == 200
|
||||
|
||||
deleted = client.delete(f"/api/v1/content/pages/{page_id}", headers=headers)
|
||||
assert deleted.status_code == 200
|
||||
|
||||
|
||||
def test_admin_stats_and_patch_user(client):
|
||||
headers = _admin_headers(client)
|
||||
stats = client.get("/api/v1/admin/stats", headers=headers)
|
||||
assert stats.status_code == 200
|
||||
|
||||
user_headers = register_verify_login(client, "blockme@example.com")
|
||||
me = client.get("/api/v1/users/me", headers=user_headers)
|
||||
user_id = me.json()["user"]["id"]
|
||||
|
||||
blocked = client.patch(
|
||||
f"/api/v1/admin/users/{user_id}",
|
||||
headers=headers,
|
||||
json={"status": "blocked"},
|
||||
)
|
||||
assert blocked.status_code == 200
|
||||
assert blocked.json()["status"] == "blocked"
|
||||
|
||||
|
||||
def test_auth_pending_user_cannot_login(client):
|
||||
client.post("/api/v1/auth/register", json={"email": "pending@example.com", "password": "Valid123"})
|
||||
response = client.post(
|
||||
"/api/v1/auth/login",
|
||||
json={"email": "pending@example.com", "password": "Valid123"},
|
||||
)
|
||||
assert response.status_code == 403
|
||||
assert response.json()["detail"] == "EMAIL_NOT_VERIFIED"
|
||||
|
||||
|
||||
def test_pending_user_cannot_access_profile(client):
|
||||
from app.core.security import create_access_token
|
||||
from app.modules.users.repository import get_user_by_email
|
||||
|
||||
client.post("/api/v1/auth/register", json={"email": "pendingme@example.com", "password": "Valid123"})
|
||||
user = get_user_by_email("pendingme@example.com")
|
||||
token = create_access_token(user.id, user.role)
|
||||
response = client.get("/api/v1/users/me", headers={"Authorization": f"Bearer {token}"})
|
||||
assert response.status_code == 403
|
||||
assert response.json()["detail"] == "EMAIL_NOT_VERIFIED"
|
||||
|
||||
|
||||
def test_auth_invalid_refresh_token(client):
|
||||
response = client.post("/api/v1/auth/refresh")
|
||||
assert response.status_code == 401
|
||||
|
||||
client.cookies.set("refresh_token", "invalid-token", path="/api/v1/auth")
|
||||
invalid = client.post("/api/v1/auth/refresh", headers={"Origin": "http://localhost:5173"})
|
||||
assert invalid.status_code == 401
|
||||
|
||||
|
||||
def test_auth_resend_and_reset_password(client):
|
||||
client.post("/api/v1/auth/register", json={"email": "resetme@example.com", "password": "Valid123"})
|
||||
resend = client.post("/api/v1/auth/resend-verification", json={"email": "resetme@example.com"})
|
||||
assert resend.status_code == 200
|
||||
verify_token = latest_token("resetme@example.com", "verify_email")
|
||||
verify = client.post("/api/v1/auth/verify-email", json={"token": verify_token})
|
||||
assert verify.status_code == 200
|
||||
|
||||
forgot = client.post("/api/v1/auth/forgot-password", json={"email": "resetme@example.com"})
|
||||
assert forgot.status_code == 200
|
||||
reset_token = latest_token("resetme@example.com", "reset_password")
|
||||
|
||||
reset = client.post(
|
||||
"/api/v1/auth/reset-password",
|
||||
json={"token": reset_token, "new_password": "NewValid1"},
|
||||
)
|
||||
assert reset.status_code == 200
|
||||
|
||||
login = client.post(
|
||||
"/api/v1/auth/login",
|
||||
json={"email": "resetme@example.com", "password": "NewValid1"},
|
||||
)
|
||||
assert login.status_code == 200
|
||||
|
||||
invalid = client.post(
|
||||
"/api/v1/auth/reset-password",
|
||||
json={"token": "invalid-token", "new_password": "NewValid1"},
|
||||
)
|
||||
assert invalid.status_code == 400
|
||||
@@ -0,0 +1,42 @@
|
||||
from tests.fixtures import make_test_png
|
||||
from tests.helpers import register_verify_login
|
||||
|
||||
|
||||
def test_upload_avatar_success(client):
|
||||
headers = register_verify_login(client, "avatar@example.com")
|
||||
response = client.post(
|
||||
"/api/v1/users/me/avatar",
|
||||
headers=headers,
|
||||
files={"file": ("avatar.png", make_test_png(), "image/png")},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
avatar_url = response.json()["profile"]["avatar_url"]
|
||||
assert avatar_url.startswith("/api/v1/media/files/avatars/")
|
||||
|
||||
media = client.get(avatar_url)
|
||||
assert media.status_code == 200
|
||||
assert media.headers["content-type"].startswith("image/")
|
||||
|
||||
|
||||
def test_upload_avatar_rejects_svg(client):
|
||||
headers = register_verify_login(client, "svg@example.com")
|
||||
svg = b"<svg xmlns='http://www.w3.org/2000/svg'><rect width='10' height='10'/></svg>"
|
||||
response = client.post(
|
||||
"/api/v1/users/me/avatar",
|
||||
headers=headers,
|
||||
files={"file": ("avatar.svg", svg, "image/svg+xml")},
|
||||
)
|
||||
assert response.status_code == 400
|
||||
assert response.json()["detail"] in {"INVALID_MIME", "INVALID_IMAGE"}
|
||||
|
||||
|
||||
def test_upload_avatar_rejects_oversize(client):
|
||||
headers = register_verify_login(client, "big@example.com")
|
||||
oversized = make_test_png() + b"0" * (2 * 1024 * 1024)
|
||||
response = client.post(
|
||||
"/api/v1/users/me/avatar",
|
||||
headers=headers,
|
||||
files={"file": ("big.png", oversized, "image/png")},
|
||||
)
|
||||
assert response.status_code == 400
|
||||
assert response.json()["detail"] == "FILE_TOO_LARGE"
|
||||
@@ -0,0 +1,30 @@
|
||||
from tests.helpers import register_verify_login
|
||||
|
||||
|
||||
def test_me_endpoint(client):
|
||||
headers = register_verify_login(client, "me@example.com", "Valid123")
|
||||
response = client.get("/api/v1/users/me", headers=headers)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["user"]["email"] == "me@example.com"
|
||||
|
||||
|
||||
def test_patch_me_updates_display_name(client):
|
||||
headers = register_verify_login(client, "display@example.com", "Valid123")
|
||||
response = client.patch(
|
||||
"/api/v1/users/me",
|
||||
headers=headers,
|
||||
json={"display_name": "Updated Name"},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["profile"]["display_name"] == "Updated Name"
|
||||
|
||||
|
||||
def test_change_password_rejects_invalid_current(client):
|
||||
headers = register_verify_login(client, "pwd@example.com", "Valid123")
|
||||
response = client.post(
|
||||
"/api/v1/users/me/password",
|
||||
headers=headers,
|
||||
json={"current_password": "Wrong123", "new_password": "NewValid1"},
|
||||
)
|
||||
assert response.status_code == 400
|
||||
assert response.json()["detail"] == "INVALID_CURRENT_PASSWORD"
|
||||
Reference in New Issue
Block a user