Initial commit: site monorepo with API, web, and infra.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
влад
2026-07-16 10:11:54 +03:00
co-authored by Cursor
commit 016910ffb7
447 changed files with 73972 additions and 0 deletions
@@ -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,125 @@
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"
def test_admin_summary_and_stats(client):
headers = _admin_headers(client)
summary = client.get("/api/v1/admin/summary", headers=headers)
assert summary.status_code == 200
assert "users_count" in summary.json()
stats = client.get("/api/v1/admin/stats", headers=headers)
assert stats.status_code == 200
def test_superuser_diagnostics_and_server_log(client):
headers = _admin_headers(client)
diagnostics = client.get("/api/v1/admin/diagnostics/report", headers=headers)
assert diagnostics.status_code == 200
assert "checks" in diagnostics.json()
activity = client.get("/api/v1/admin/activity-feed", headers=headers)
assert activity.status_code == 200
assert "events" in activity.json()
server_log = client.get("/api/v1/admin/server-log", headers=headers)
assert server_log.status_code == 200
assert "lines" in server_log.json()
def test_admin_ui_activity(client):
response = client.post(
"/api/v1/admin/ui-activity",
json={"event": "tab_open", "meta": {"tab": "users"}},
headers=_admin_headers(client),
)
assert response.status_code == 200
assert response.json()["status"] == "ok"
def test_admin_sync_metrics_with_enterprise(client):
from app.modules.sync import repository as sync_repo
ent = sync_repo.create_enterprise("Metrics Test Farm", "metrics-test-farm")
headers = _admin_headers(client)
response = client.get("/api/v1/admin/sync-metrics", headers=headers)
assert response.status_code == 200
payload = response.json()
assert isinstance(payload, list)
match = next((row for row in payload if row["enterprise_id"] == ent.id), None)
assert match is not None
assert match["name"] == "Metrics Test Farm"
assert "hubs" in match
assert "outbox_pending" in match
@@ -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,10 @@
from app.modules.admin.security_diagnostics import build_security_diagnostics_report
def test_build_security_diagnostics_report_returns_checks():
report = build_security_diagnostics_report()
assert "checks" in report
assert len(report["checks"]) >= 10
ids = {check["id"] for check in report["checks"]}
assert "jwt_access_secret" in ids
assert "install_secrets_locked" in ids
@@ -0,0 +1,88 @@
from unittest.mock import patch
from app.modules.admin.service import (
create_admin_user,
delete_admin_user,
get_server_log_tail,
patch_user,
reset_user_password,
)
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"
def test_admin_cannot_self_block():
admin = get_user_by_email("admin@compton.example")
try:
patch_user(admin, admin.id, None, "blocked")
assert False, "Expected self-block error"
except ValueError as exc:
assert str(exc) == "SELF_BLOCK_FORBIDDEN"
def test_reset_user_password_revokes_sessions():
admin = get_user_by_email("admin@compton.example")
target = create_user("reset-pw@example.com", hash_password("Valid123"), status="active")
result = reset_user_password(admin, target.id, "NewValid123")
assert result["status"] == "ok"
def test_delete_admin_user_forbidden_for_self():
admin = get_user_by_email("admin@compton.example")
try:
delete_admin_user(admin, admin.id)
assert False, "Expected self-delete error"
except ValueError as exc:
assert str(exc) == "SELF_DELETE_FORBIDDEN"
def test_create_admin_user_rejects_duplicate_email():
admin = get_user_by_email("admin@compton.example")
try:
create_admin_user(
admin,
email="user@compton.example",
password="Valid123",
role="user",
is_superuser=False,
status="active",
)
assert False, "Expected duplicate user error"
except ValueError as exc:
assert str(exc) == "USER_EXISTS"
def test_get_server_log_tail_empty_when_missing():
with patch("app.modules.admin.service.settings") as mock_settings:
mock_settings.server_log_path = "/tmp/compton-missing-log.txt"
payload = get_server_log_tail()
assert payload["lines"] == []
@@ -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,35 @@
from app.core.security import create_access_token, hash_password
from tests.helpers import register_and_verify
def _admin_headers(client) -> dict[str, str]:
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_blocked_user_access_token_revoked_after_admin_block(client):
register_and_verify(client, "blocked-jwt@example.com")
login = client.post(
"/api/v1/auth/login",
json={"email": "blocked-jwt@example.com", "password": "Valid123"},
)
assert login.status_code == 200
access_token = login.json()["access_token"]
me = client.get("/api/v1/users/me", headers={"Authorization": f"Bearer {access_token}"})
assert me.status_code == 200
user_id = me.json()["user"]["id"]
blocked = client.patch(
f"/api/v1/admin/users/{user_id}",
headers=_admin_headers(client),
json={"status": "blocked"},
)
assert blocked.status_code == 200
revoked = client.get("/api/v1/users/me", headers={"Authorization": f"Bearer {access_token}"})
assert revoked.status_code == 401
assert revoked.json()["detail"] == "TOKEN_REVOKED"
@@ -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,77 @@
from unittest.mock import MagicMock, patch
import pytest
from app.core import jwt_denylist as denylist_module
from app.core.jwt_denylist import (
bump_auth_epoch,
deny_jti,
ensure_jwt_revocation_backend,
get_auth_epoch,
is_jti_denied,
)
def test_ensure_jwt_revocation_backend_requires_redis_in_production(monkeypatch):
monkeypatch.setattr(denylist_module.settings, "app_env", "production")
with patch("app.core.jwt_denylist.get_redis_client", return_value=None):
with pytest.raises(RuntimeError, match="Redis is required"):
ensure_jwt_revocation_backend()
def test_redis_auth_epoch_roundtrip():
mock_client = MagicMock()
mock_client.get.return_value = "3"
mock_client.incr.return_value = 4
with patch("app.core.jwt_denylist.get_redis_client", return_value=mock_client):
assert get_auth_epoch("user-1") == 3
assert bump_auth_epoch("user-1") == 4
mock_client.incr.assert_called_once()
def test_redis_deny_jti_and_check():
mock_client = MagicMock()
with patch("app.core.jwt_denylist.get_redis_client", return_value=mock_client):
with patch("app.core.jwt_denylist.time.time", return_value=1000):
deny_jti("abc-jti", 1060)
mock_client.setex.assert_called_once_with("jwt:deny:abc-jti", 60, "1")
mock_client.exists.return_value = 1
assert is_jti_denied("abc-jti") is True
def test_forgot_password_skips_blocked_user(client):
from app.modules.users.repository import create_user, get_user_by_email, update_user
from app.core.security import hash_password
create_user("blocked-forgot@example.com", hash_password("Valid123"), status="active")
user = get_user_by_email("blocked-forgot@example.com")
user.status = "blocked"
update_user(user)
response = client.post(
"/api/v1/auth/forgot-password",
json={"email": "blocked-forgot@example.com"},
)
assert response.status_code == 200
from app.core.email import memory_mailer
assert not any(msg.to == "blocked-forgot@example.com" for msg in memory_mailer.sent)
def test_logout_revokes_access_token(client):
from tests.helpers import register_and_verify
register_and_verify(client, "logout-jti@example.com")
login = client.post(
"/api/v1/auth/login",
json={"email": "logout-jti@example.com", "password": "Valid123"},
)
token = login.json()["access_token"]
logout = client.post(
"/api/v1/auth/logout",
headers={"Authorization": f"Bearer {token}", "Origin": "http://localhost:5173"},
)
assert logout.status_code == 200
me = client.get("/api/v1/users/me", headers={"Authorization": f"Bearer {token}"})
assert me.status_code == 401
assert me.json()["detail"] == "TOKEN_REVOKED"
@@ -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,73 @@
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_pending_user(client):
client.post(
"/api/v1/auth/register",
json={"email": "pending-refresh@example.com", "password": "Valid123"},
)
login = client.post(
"/api/v1/auth/login",
json={"email": "pending-refresh@example.com", "password": "Valid123"},
)
assert login.status_code == 403
assert login.json()["detail"] == "EMAIL_NOT_VERIFIED"
# Simulate stale refresh cookie from an earlier active session edge case via direct token issue.
from app.modules.auth.service import issue_refresh_token
from app.modules.users.repository import get_user_by_email
user = get_user_by_email("pending-refresh@example.com")
refresh_token = issue_refresh_token(user.id)
client.cookies.set("refresh_token", refresh_token, path="/api/v1/auth")
refresh = client.post("/api/v1/auth/refresh", headers={"Origin": "http://localhost:5173"})
assert refresh.status_code == 403
assert refresh.json()["detail"] == "EMAIL_NOT_VERIFIED"
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 == 403
assert refresh.json()["detail"] == "ACCOUNT_BLOCKED"
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,40 @@
from __future__ import annotations
from unittest.mock import patch
import pytest
from app.core.security import hash_password
from app.modules.auth import repository as auth_repository
from app.modules.auth.service import issue_refresh_token, refresh
from app.modules.users.repository import create_user, get_user_by_id, update_user
def test_refresh_returns_account_blocked_for_blocked_user():
user = create_user("blocked-svc@example.com", hash_password("Valid123"), status="active")
token = issue_refresh_token(user.id)
user.status = "blocked"
update_user(user)
auth_repository.revoke_user_families(user.id)
with pytest.raises(PermissionError, match="ACCOUNT_BLOCKED"):
refresh(token)
def test_refresh_returns_email_not_verified_for_pending_user():
user = create_user("pending-svc@example.com", hash_password("Valid123"), status="pending")
token = issue_refresh_token(user.id)
with pytest.raises(PermissionError, match="EMAIL_NOT_VERIFIED"):
refresh(token)
def test_refresh_rejects_revoked_token_for_active_user():
user = create_user("active-svc@example.com", hash_password("Valid123"), status="active")
token = issue_refresh_token(user.id)
auth_repository.revoke_user_families(user.id)
with patch.object(auth_repository, "revoke_family_tokens") as revoke_family:
with pytest.raises(PermissionError, match="INVALID_REFRESH"):
refresh(token)
revoke_family.assert_called_once()
@@ -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,120 @@
from __future__ import annotations
from uuid import uuid4
import pytest
from fastapi.testclient import TestClient
from app.modules.sync import repository as repo
from tests.orchestrator_dual_harness import OrchestratorDualHubHarness, make_enterprise
def test_hub_conflicts_via_hub_auth(client: TestClient):
enterprise_id = make_enterprise(client)
harness = OrchestratorDualHubHarness(client, enterprise_id)
hub_a, hub_b = harness.pair_hubs()
record_id = "hub-auth-conflict-1"
harness.push_from(
hub_a,
[
{
"event_id": str(uuid4()),
"seq": 1,
"domain": "global",
"table": "component",
"record_id": record_id,
"action": "upsert",
"version": 1,
"content_hash": "hub-h1",
"payload": {"name": "A", "type": "g", "dry_matter": 1, "id": record_id, "version": 1, "content_hash": "hub-h1"},
"emitted_at": "2026-07-15T12:00:00Z",
"origin_site_id": hub_a.hub_site_id,
}
],
)
harness.push_from(
hub_b,
[
{
"event_id": str(uuid4()),
"seq": 1,
"domain": "global",
"table": "component",
"record_id": record_id,
"action": "upsert",
"version": 1,
"content_hash": "hub-h2",
"payload": {"name": "B", "type": "g", "dry_matter": 2, "id": record_id, "version": 1, "content_hash": "hub-h2"},
"emitted_at": "2026-07-15T12:01:00Z",
"origin_site_id": hub_b.hub_site_id,
}
],
)
listed = client.get("/api/v1/sync/hub/conflicts", headers=hub_a.auth_header())
assert listed.status_code == 200
conflicts = listed.json()
assert len(conflicts) >= 1
def test_multiserver_list_and_resolve(client: TestClient):
enterprise_id = make_enterprise(client)
harness = OrchestratorDualHubHarness(client, enterprise_id)
hub_a, hub_b = harness.pair_hubs()
record_id = "ms-conflict-1"
harness.push_from(
hub_a,
[
{
"event_id": str(uuid4()),
"seq": 1,
"domain": "global",
"table": "component",
"record_id": record_id,
"action": "upsert",
"version": 1,
"content_hash": "ms-h1",
"payload": {"name": "A", "type": "g", "dry_matter": 1, "id": record_id, "version": 1, "content_hash": "ms-h1"},
"emitted_at": "2026-07-15T12:00:00Z",
"origin_site_id": hub_a.hub_site_id,
}
],
)
harness.push_from(
hub_b,
[
{
"event_id": str(uuid4()),
"seq": 1,
"domain": "global",
"table": "component",
"record_id": record_id,
"action": "upsert",
"version": 1,
"content_hash": "ms-h2",
"payload": {"name": "B", "type": "g", "dry_matter": 2, "id": record_id, "version": 1, "content_hash": "ms-h2"},
"emitted_at": "2026-07-15T12:01:00Z",
"origin_site_id": hub_b.hub_site_id,
}
],
)
login = client.post("/api/v1/auth/login", json={"email": "admin@compton.example", "password": "Admin1234"})
token = login.json()["access_token"]
listed = client.get(
f"/api/v1/sync/conflicts?enterprise_id={enterprise_id}",
headers={"Authorization": f"Bearer {token}"},
)
assert listed.status_code == 200
conflicts = listed.json()
assert len(conflicts) >= 1
cid = conflicts[0]["id"]
detail = client.get(
f"/api/v1/sync/conflicts/{cid}?enterprise_id={enterprise_id}",
headers={"Authorization": f"Bearer {token}"},
)
assert detail.status_code == 200
resolved = client.post(
f"/api/v1/sync/conflicts/{cid}/resolve?enterprise_id={enterprise_id}",
headers={"Authorization": f"Bearer {token}"},
json={"resolution": "keep_orchestrator"},
)
assert resolved.status_code == 200
@@ -0,0 +1,181 @@
from __future__ import annotations
import json
from datetime import UTC, datetime
from uuid import uuid4
import pytest
from fastapi.testclient import TestClient
from app.modules.sync import repository as repo
from app.modules.sync.engine import SyncEngine
from tests.orchestrator_dual_harness import OrchestratorDualHubHarness, make_enterprise
@pytest.fixture()
def chaos_harness(client: TestClient):
enterprise_id = make_enterprise(client)
harness = OrchestratorDualHubHarness(client, enterprise_id)
hub_a, hub_b = harness.pair_hubs()
return harness, hub_a, hub_b
def test_duplicate_ack_safe(client: TestClient, chaos_harness):
harness, hub_a, _hub_b = chaos_harness
record_id = "chaos-ack-1"
event = harness.make_component_event(
hub_a, record_id, name="DupAck", version=1, content_hash="da1", event_id="evt-dup-ack"
)
harness.push_from(hub_a, [event])
events = harness.pull_for(hub_a) # hub should not see own events
assert events == []
resp = client.post(
"/api/v1/sync/changes/ack",
headers=hub_a.auth_header(),
json={"event_ids": ["evt-dup-ack"], "direction": "inbound"},
)
assert resp.status_code == 200
def test_out_of_order_seq_replay_idempotent(client: TestClient, chaos_harness):
harness, hub_a, hub_b = chaos_harness
record_id = "chaos-seq-1"
e2 = harness.make_component_event(
hub_a, record_id, name="Second", version=2, content_hash="s2", event_id="evt-seq-2"
)
e1 = harness.make_component_event(
hub_a, record_id, name="First", version=1, content_hash="s1", event_id="evt-seq-1"
)
harness.push_from(hub_a, [e2])
harness.push_from(hub_a, [e1])
harness.drain()
row = harness.get_catalog(hub_b, "component", record_id)
assert row is not None
assert row["content_hash"] in {"s1", "s2"}
def test_resolve_keep_orchestrator_fanout(client: TestClient, chaos_harness):
harness, hub_a, hub_b = chaos_harness
record_id = "chaos-resolve-1"
harness.push_from(
hub_a,
[
{
"event_id": str(uuid4()),
"seq": 1,
"domain": "global",
"table": "component",
"record_id": record_id,
"action": "upsert",
"version": 1,
"content_hash": "orch-hash",
"payload": {
"name": "Orch",
"type": "grain",
"dry_matter": 50.0,
"id": record_id,
"version": 1,
"content_hash": "orch-hash",
},
"emitted_at": datetime.now(UTC).isoformat(),
"origin_site_id": hub_a.hub_site_id,
}
],
)
harness.push_from(
hub_b,
[
{
"event_id": str(uuid4()),
"seq": 1,
"domain": "global",
"table": "component",
"record_id": record_id,
"action": "upsert",
"version": 1,
"content_hash": "hub-hash",
"payload": {
"name": "HubB",
"type": "grain",
"dry_matter": 60.0,
"id": record_id,
"version": 1,
"content_hash": "hub-hash",
},
"emitted_at": datetime.now(UTC).isoformat(),
"origin_site_id": hub_b.hub_site_id,
}
],
)
conflicts = repo.list_conflicts(harness.enterprise_id, "pending")
assert len(conflicts) == 1
engine = SyncEngine(harness.enterprise_id, SyncEngine.ORCHESTRATOR_SITE_ID)
engine.resolve_conflict(conflicts[0].id, "admin", "keep_orchestrator")
harness.drain()
row = harness.get_orchestrator_catalog("component", record_id)
assert row is not None
assert row["content_hash"] == "orch-hash"
def test_resolve_keep_hub_fanout(client: TestClient, chaos_harness):
harness, hub_a, hub_b = chaos_harness
record_id = "chaos-resolve-hub-1"
harness.push_from(
hub_a,
[
{
"event_id": str(uuid4()),
"seq": 1,
"domain": "global",
"table": "component",
"record_id": record_id,
"action": "upsert",
"version": 1,
"content_hash": "orch-hash2",
"payload": {
"name": "Orch",
"type": "grain",
"dry_matter": 50.0,
"id": record_id,
"version": 1,
"content_hash": "orch-hash2",
},
"emitted_at": "2026-07-15T10:00:00Z",
"origin_site_id": hub_a.hub_site_id,
}
],
)
harness.push_from(
hub_b,
[
{
"event_id": str(uuid4()),
"seq": 1,
"domain": "global",
"table": "component",
"record_id": record_id,
"action": "upsert",
"version": 1,
"content_hash": "hub-b-hash2",
"payload": {
"name": "Hub B wins",
"type": "grain",
"dry_matter": 77.0,
"id": record_id,
"version": 1,
"content_hash": "hub-b-hash2",
},
"emitted_at": "2026-07-15T10:01:00Z",
"origin_site_id": hub_b.hub_site_id,
}
],
)
conflicts = repo.list_conflicts(harness.enterprise_id, "pending")
assert len(conflicts) == 1
engine = SyncEngine(harness.enterprise_id, SyncEngine.ORCHESTRATOR_SITE_ID)
engine.resolve_conflict(conflicts[0].id, "admin", "keep_hub")
harness.drain()
row = harness.get_orchestrator_catalog("component", record_id)
assert row is not None
assert row["content_hash"] == "hub-b-hash2"
assert row["name"] == "Hub B wins"
@@ -0,0 +1,187 @@
from __future__ import annotations
from uuid import uuid4
import pytest
from fastapi.testclient import TestClient
from tests.orchestrator_dual_harness import OrchestratorDualHubHarness, make_enterprise
@pytest.fixture()
def dual_harness(client: TestClient):
enterprise_id = make_enterprise(client)
harness = OrchestratorDualHubHarness(client, enterprise_id)
hub_a, hub_b = harness.pair_hubs()
return harness, hub_a, hub_b
def test_hub_a_push_reaches_hub_b(client: TestClient, dual_harness):
harness, hub_a, hub_b = dual_harness
record_id = "comp-dual-001"
event = harness.make_component_event(
hub_a, record_id, name="Corn A", version=1, content_hash="hash-a1"
)
harness.push_from(hub_a, [event])
harness.drain()
row = harness.get_catalog(hub_b, "component", record_id)
assert row is not None
assert row["name"] == "Corn A"
assert row["content_hash"] == "hash-a1"
def test_hub_b_push_back_to_hub_a(client: TestClient, dual_harness):
harness, hub_a, hub_b = dual_harness
record_id = "comp-dual-002"
harness.push_from(
hub_a,
[harness.make_component_event(hub_a, record_id, name="V1", version=1, content_hash="h1")],
)
harness.drain()
harness.push_from(
hub_b,
[harness.make_component_event(hub_b, record_id, name="V2 from B", version=2, content_hash="h2")],
)
harness.drain()
row_a = harness.get_catalog(hub_a, "component", record_id)
assert row_a is not None
assert row_a["content_hash"] == "h2"
assert row_a["name"] == "V2 from B"
def test_idempotent_replay(client: TestClient, dual_harness):
harness, hub_a, hub_b = dual_harness
record_id = "comp-dual-003"
event = harness.make_component_event(
hub_a, record_id, name="Once", version=1, content_hash="once1", event_id="evt-fixed-003"
)
harness.push_from(hub_a, [event])
harness.push_from(hub_a, [event])
harness.drain()
orch = harness.get_orchestrator_catalog("component", record_id)
assert orch is not None
assert orch["name"] == "Once"
row_b = harness.get_catalog(hub_b, "component", record_id)
assert row_b is not None
assert row_b["content_hash"] == "once1"
def test_ack_cursor_no_double_apply(client: TestClient, dual_harness):
harness, hub_a, hub_b = dual_harness
record_id = "comp-dual-004"
event = harness.make_component_event(
hub_a, record_id, name="Ack safe", version=1, content_hash="ack1", event_id="evt-ack-004"
)
harness.push_from(hub_a, [event])
events1 = harness.pull_for(hub_b)
assert len(events1) == 1
events2 = harness.pull_for(hub_b)
assert len(events2) == 0
row = harness.get_catalog(hub_b, "component", record_id)
assert row is not None
assert row["name"] == "Ack safe"
def test_no_conflict_single_editor(client: TestClient, dual_harness):
harness, hub_a, hub_b = dual_harness
record_id = "comp-dual-005"
harness.push_from(
hub_a,
[harness.make_component_event(hub_a, record_id, name="Solo", version=1, content_hash="solo1")],
)
harness.drain()
from app.modules.sync import repository as repo
conflicts = repo.list_conflicts(harness.enterprise_id, "pending")
assert conflicts == []
row = harness.get_orchestrator_catalog("component", record_id)
assert row is not None
assert row["name"] == "Solo"
def test_conflict_dual_edit(client: TestClient, dual_harness):
harness, hub_a, hub_b = dual_harness
record_id = "comp-dual-006"
base = {"name": "Base", "type": "grain", "dry_matter": 50.0}
harness.push_from(
hub_a,
[
{
"event_id": str(uuid4()),
"seq": 1,
"domain": "global",
"table": "component",
"record_id": record_id,
"action": "upsert",
"version": 1,
"content_hash": "base-hash",
"payload": {**base, "id": record_id, "version": 1, "content_hash": "base-hash"},
"emitted_at": "2026-07-15T10:00:00Z",
"origin_site_id": hub_a.hub_site_id,
}
],
)
harness.drain()
harness.push_from(
hub_b,
[
{
"event_id": str(uuid4()),
"seq": 1,
"domain": "global",
"table": "component",
"record_id": record_id,
"action": "upsert",
"version": 1,
"content_hash": "b-conflict-hash",
"payload": {
"name": "B edit",
"type": "grain",
"dry_matter": 60.0,
"id": record_id,
"version": 1,
"content_hash": "b-conflict-hash",
},
"emitted_at": "2026-07-15T10:01:00Z",
"origin_site_id": hub_b.hub_site_id,
}
],
)
from app.modules.sync import repository as repo
conflicts = repo.list_conflicts(harness.enterprise_id, "pending")
assert len(conflicts) == 1
orch = harness.get_orchestrator_catalog("component", record_id)
assert orch is not None
assert orch["content_hash"] == "base-hash"
def test_period_recipes_composite_record_id(client: TestClient, dual_harness):
harness, hub_a, hub_b = dual_harness
period_id = "737a78a5-492a-4a84-94f3-18b4d7f21666"
recipe_id = "07fd17cf-22c3-44fe-92db-46363b24cee0"
record_id = f"{period_id}:{recipe_id}"
event = {
"event_id": str(uuid4()),
"seq": 1,
"domain": "global",
"table": "period_recipes",
"record_id": record_id,
"action": "upsert",
"version": 1,
"content_hash": "pr1",
"payload": {
"period_id": period_id,
"recipe_id": recipe_id,
"order": 1,
"version": 1,
"content_hash": "pr1",
},
"emitted_at": "2026-07-15T12:00:00Z",
"origin_site_id": hub_a.hub_site_id,
}
harness.push_from(hub_a, [event])
harness.drain()
row = harness.get_catalog(hub_b, "period_recipes", record_id)
assert row is not None
assert row.get("order") == 1
@@ -0,0 +1,72 @@
from __future__ import annotations
from pathlib import Path
from uuid import uuid4
import pytest
from fastapi.testclient import TestClient
from tests.orchestrator_dual_harness import OrchestratorDualHubHarness, make_enterprise
def test_recipe_ingredient_tree_roundtrip(client: TestClient):
enterprise_id = make_enterprise(client)
harness = OrchestratorDualHubHarness(client, enterprise_id)
hub_a, hub_b = harness.pair_hubs()
recipe_id = "recipe-tree-1"
ing_id = "ing-tree-1"
comp_id = "comp-tree-1"
harness.push_from(
hub_a,
[
harness.make_component_event(hub_a, comp_id, name="Barley", version=1, content_hash="c1"),
{
"event_id": str(uuid4()),
"seq": 2,
"domain": "global",
"table": "recipe",
"record_id": recipe_id,
"action": "upsert",
"version": 1,
"content_hash": "r1",
"payload": {
"id": recipe_id,
"name": "Mix A",
"heads_per_trip": 100,
"ingredients": [{"id": ing_id, "component_id": comp_id, "name": "Barley", "amount": 10}],
"version": 1,
"content_hash": "r1",
},
"emitted_at": "2026-07-15T12:00:00Z",
"origin_site_id": hub_a.hub_site_id,
},
{
"event_id": str(uuid4()),
"seq": 3,
"domain": "global",
"table": "ingredient",
"record_id": ing_id,
"action": "upsert",
"version": 1,
"content_hash": "i1",
"payload": {
"id": ing_id,
"recipe_id": recipe_id,
"component_id": comp_id,
"name": "Barley",
"amount": 10.0,
"version": 1,
"content_hash": "i1",
},
"emitted_at": "2026-07-15T12:00:01Z",
"origin_site_id": hub_a.hub_site_id,
},
],
)
harness.drain([hub_b])
recipe = harness.get_catalog(hub_b, "recipe", recipe_id)
assert recipe is not None
assert recipe.get("name") == "Mix A"
ing = harness.get_catalog(hub_b, "ingredient", ing_id)
assert ing is not None
assert ing.get("component_id") == comp_id
@@ -0,0 +1,39 @@
from __future__ import annotations
import os
import pytest
from app.modules.sync import repository as repo
@pytest.mark.skipif(
"postgresql" not in os.environ.get("DATABASE_URL", ""),
reason="RLS tests require PostgreSQL",
)
def test_rls_cross_enterprise():
ent_a = repo.create_enterprise("Farm A", f"farm-a-{os.getpid()}")
ent_b = repo.create_enterprise("Farm B", f"farm-b-{os.getpid()}")
assert ent_a.id != ent_b.id
hub_a = repo.create_farm_hub(ent_a.id, "Hub A", f"hub-a-{os.getpid()}", None)
hubs_b = repo.list_farm_hubs(ent_b.id)
assert all(h.id != hub_a.id for h in hubs_b)
def test_viewer_farm_scope(client):
from uuid import uuid4
from app.modules.users.repository import get_user_by_email
slug = f"abac-{uuid4().hex[:8]}"
ent = repo.create_enterprise("ABAC Farm", slug)
admin = get_user_by_email("admin@compton.example")
assert admin
repo.add_member(admin.id, ent.id, "admin")
hub1 = repo.create_farm_hub(ent.id, "H1", f"h1-{uuid4().hex[:8]}", None)
hub2 = repo.create_farm_hub(ent.id, "H2", f"h2-{uuid4().hex[:8]}", None)
viewer_id = admin.id
repo.grant_farm_access(viewer_id, hub1.id)
allowed = repo.list_farm_access(viewer_id, ent.id)
assert hub1.id in allowed
assert hub2.id not in allowed
@@ -0,0 +1,97 @@
from __future__ import annotations
import pytest
from fastapi.testclient import TestClient
from app.core.crypto import hash_opaque_token
from app.modules.sync import repository as repo
from uuid import uuid4
@pytest.fixture()
def enterprise(client: TestClient):
slug = f"test-farm-{uuid4().hex[:8]}"
ent = repo.create_enterprise("Test Farm Co", slug)
admin = repo.get_member.__module__ # noqa: ensure import path
from app.modules.users.repository import get_user_by_email
user = get_user_by_email("admin@compton.example")
assert user
repo.add_member(user.id, ent.id, "admin")
return ent
def test_sync_capabilities(client: TestClient):
resp = client.get("/api/v1/sync/capabilities")
assert resp.status_code == 200
data = resp.json()
assert data["protocol_version"] == "1.0"
assert "global" in data["domains"]
def test_pairing_flow(client: TestClient, enterprise):
login = client.post("/api/v1/auth/login", json={"email": "admin@compton.example", "password": "Admin1234"})
token = login.json()["access_token"]
start = client.post(
"/api/v1/enterprise/pair/start",
headers={"Authorization": f"Bearer {token}"},
json={"enterprise_id": enterprise.id, "farm_name": "Farm A"},
)
assert start.status_code == 200
code = start.json()["code"]
confirm = client.post(
"/api/v1/enterprise/pair/confirm",
json={"code": code, "hub_site_id": "hub-site-001", "hub_name": "Farm A Hub"},
)
assert confirm.status_code == 200
body = confirm.json()
assert body["hub_site_id"] == "hub-site-001"
assert body["api_key"]
def test_hub_push_idempotent(client: TestClient, enterprise):
login = client.post("/api/v1/auth/login", json={"email": "admin@compton.example", "password": "Admin1234"})
token = login.json()["access_token"]
start = client.post(
"/api/v1/enterprise/pair/start",
headers={"Authorization": f"Bearer {token}"},
json={"enterprise_id": enterprise.id, "farm_name": "Farm B"},
)
code = start.json()["code"]
confirm = client.post(
"/api/v1/enterprise/pair/confirm",
json={"code": code, "hub_site_id": "hub-site-002", "hub_name": "Farm B Hub"},
)
api_key = confirm.json()["api_key"]
hub_auth = f"Hub hub-site-002:{api_key}"
from datetime import UTC, datetime
event = {
"event_id": "evt-001",
"seq": 1,
"domain": "global",
"table": "component",
"record_id": "comp-001",
"action": "upsert",
"version": 1,
"content_hash": "hash1",
"payload": {"name": "Corn", "type": "grain", "dry_matter": 88.0},
"emitted_at": datetime.now(UTC).isoformat(),
"origin_site_id": "hub-site-002",
}
push1 = client.post(
"/api/v1/sync/changes/push",
headers={"Authorization": hub_auth},
json={"events": [event]},
)
assert push1.status_code == 200
assert "evt-001" in push1.json()["applied_event_ids"]
push2 = client.post(
"/api/v1/sync/changes/push",
headers={"Authorization": hub_auth},
json={"events": [event]},
)
assert push2.status_code == 200
assert "evt-001" in push2.json()["applied_event_ids"]
@@ -0,0 +1,38 @@
from fastapi.testclient import TestClient
from app.core.config import settings
from app.core.email import memory_mailer
from app.main import create_app
def _test_client(monkeypatch) -> TestClient:
monkeypatch.setattr(settings, "enable_test_routes", True)
monkeypatch.setattr(settings, "email_delivery_mode", "memory")
return TestClient(create_app())
def test_latest_email_token_route(monkeypatch):
client = _test_client(monkeypatch)
memory_mailer.clear()
memory_mailer.send(
to="token-route@example.com",
subject="Verify",
body="TOKEN:route-token\n",
template="verify_email",
)
response = client.get(
"/api/v1/test/emails/latest-token",
params={"to": "token-route@example.com", "template": "verify_email"},
)
assert response.status_code == 200
assert response.json()["token"] == "route-token"
def test_latest_email_token_route_missing_token(monkeypatch):
client = _test_client(monkeypatch)
response = client.get(
"/api/v1/test/emails/latest-token",
params={"to": "missing@example.com", "template": "verify_email"},
)
assert response.status_code == 404
assert response.json()["detail"] == "TOKEN_NOT_FOUND"
@@ -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"
@@ -0,0 +1,11 @@
from app.modules.zootech.orchestrator_system_metrics import collect_orchestrator_system_metrics
def test_collect_orchestrator_system_metrics_shape():
metrics = collect_orchestrator_system_metrics()
if metrics.get("available"):
assert "memory" in metrics
assert "cpu" in metrics
assert "swap" in metrics
else:
assert "message" in metrics
@@ -0,0 +1,356 @@
from __future__ import annotations
from datetime import UTC, datetime
from uuid import uuid4
import pytest
from fastapi.testclient import TestClient
from app.modules.zootech.catalog_apply import apply_catalog_change
from tests.orchestrator_dual_harness import make_enterprise
def test_wesp_compat_feed_dispensers(client: TestClient):
enterprise_id = make_enterprise(client)
login = client.post("/api/v1/auth/login", json={"email": "admin@compton.example", "password": "Admin1234"})
token = login.json()["access_token"]
session = client.post(
f"/api/v1/enterprise/enterprises/{enterprise_id}/session",
headers={"Authorization": f"Bearer {token}"},
)
token = session.json()["access_token"]
headers = {"Authorization": f"Bearer {token}"}
resp = client.get(f"/api/feed_dispensers?enterprise_id={enterprise_id}", headers=headers)
assert resp.status_code == 200
assert isinstance(resp.json(), list)
def test_wesp_compat_components(client: TestClient):
enterprise_id = make_enterprise(client)
login = client.post("/api/v1/auth/login", json={"email": "admin@compton.example", "password": "Admin1234"})
token = login.json()["access_token"]
session = client.post(
f"/api/v1/enterprise/enterprises/{enterprise_id}/session",
headers={"Authorization": f"Bearer {token}"},
)
token = session.json()["access_token"]
headers = {"Authorization": f"Bearer {token}"}
resp = client.get(f"/api/components?enterprise_id={enterprise_id}", headers=headers)
assert resp.status_code == 200
assert isinstance(resp.json(), list)
def test_wesp_compat_recipe_includes_ingredients_and_unloading_groups(client: TestClient):
enterprise_id = make_enterprise(client)
login = client.post("/api/v1/auth/login", json={"email": "admin@compton.example", "password": "Admin1234"})
token = login.json()["access_token"]
session = client.post(
f"/api/v1/enterprise/enterprises/{enterprise_id}/session",
headers={"Authorization": f"Bearer {token}"},
)
token = session.json()["access_token"]
headers = {"Authorization": f"Bearer {token}"}
recipe_id = "recipe-compat-1"
ing_id = "ing-compat-1"
grp_id = "grp-compat-1"
apply_catalog_change(
enterprise_id,
"recipe",
recipe_id,
"upsert",
{
"id": recipe_id,
"name": "Compat Mix",
"heads_per_trip": 100,
"mixing_time": 10,
"trip_percent": 100.0,
},
1,
"r1",
)
apply_catalog_change(
enterprise_id,
"ingredient",
ing_id,
"upsert",
{
"id": ing_id,
"recipe_id": recipe_id,
"name": "Corn",
"amount": 12.5,
"order": 1,
"dry_matter": 88.0,
},
1,
"i1",
)
apply_catalog_change(
enterprise_id,
"unloading_group",
grp_id,
"upsert",
{
"id": grp_id,
"recipe_id": recipe_id,
"name": "Barn A",
"distribution_type": "percent",
"value": 50.0,
"weight": 500.0,
"order": 1,
},
1,
"g1",
)
resp = client.get(f"/api/recipes/{recipe_id}?enterprise_id={enterprise_id}", headers=headers)
assert resp.status_code == 200
body = resp.json()
assert body["name"] == "Compat Mix"
assert len(body.get("ingredients") or []) == 1
assert body["ingredients"][0]["name"] == "Corn"
assert len(body.get("unloading_groups") or body.get("unloadingGroups") or []) == 1
assert body["unloading_groups"][0]["name"] == "Barn A"
def test_wesp_compat_recipe_calculate(client: TestClient):
resp = client.post(
"/api/recipes/calculate",
json={
"ingredients": [{"weightPerHead": 10, "dryMatter": 88, "component_id": "c1"}],
"headsCount": 100,
"tripPercent": 100,
"unloadingGroups": [{"distributionType": "percent", "value": 50}],
},
)
assert resp.status_code == 200
body = resp.json()
assert len(body.get("ingredients") or []) == 1
assert len(body.get("unloadingGroups") or []) == 1
def test_wesp_compat_recipe_put_persists_unloading_group(client: TestClient):
enterprise_id = make_enterprise(client)
login = client.post("/api/v1/auth/login", json={"email": "admin@compton.example", "password": "Admin1234"})
token = login.json()["access_token"]
session = client.post(
f"/api/v1/enterprise/enterprises/{enterprise_id}/session",
headers={"Authorization": f"Bearer {token}"},
)
token = session.json()["access_token"]
headers = {"Authorization": f"Bearer {token}"}
recipe_id = "recipe-put-1"
apply_catalog_change(
enterprise_id,
"recipe",
recipe_id,
"upsert",
{"id": recipe_id, "name": "Before", "heads_per_trip": 100, "mixing_time": 10, "trip_percent": 100.0},
1,
"r1",
)
put = client.put(
f"/api/recipes/{recipe_id}?enterprise_id={enterprise_id}",
headers=headers,
json={
"name": "After",
"heads_count": 100,
"mixing_time": 10,
"trip_percent": 100,
"dry_matter_locked": False,
"unloading_link_broken": False,
"ingredients": [],
"unloading_groups": [
{
"name": "Group A",
"distribution_type": "percent",
"value": 100,
"weight": 500,
"order": 1,
}
],
},
)
assert put.status_code == 200
got = client.get(f"/api/recipes/{recipe_id}?enterprise_id={enterprise_id}", headers=headers)
assert got.status_code == 200
groups = got.json().get("unloading_groups") or []
assert len(groups) == 1
assert groups[0]["name"] == "Group A"
def test_wesp_compat_analytics_endpoints(client: TestClient):
enterprise_id = make_enterprise(client)
login = client.post("/api/v1/auth/login", json={"email": "admin@compton.example", "password": "Admin1234"})
token = login.json()["access_token"]
session = client.post(
f"/api/v1/enterprise/enterprises/{enterprise_id}/session",
headers={"Authorization": f"Bearer {token}"},
)
token = session.json()["access_token"]
headers = {"Authorization": f"Bearer {token}"}
params = f"enterprise_id={enterprise_id}&date_from=2026-07-01&date_to=2026-07-15"
finance = client.get(f"/api/analytics/finance?{params}", headers=headers)
assert finance.status_code == 200
assert "overloadRub" in finance.json()
plan_fact = client.get(f"/api/analytics/plan-fact?{params}", headers=headers)
assert plan_fact.status_code == 200
assert plan_fact.json()["items"] == []
summary = client.get(f"/api/feed-quality/alerts/summary?{params}", headers=headers)
assert summary.status_code == 200
assert "total" in summary.json()
alerts = client.get(f"/api/feed-quality/alerts?{params}", headers=headers)
assert alerts.status_code == 200
assert "items" in alerts.json()
def test_wesp_compat_reports_list(client: TestClient):
from app.modules.zootech.report_apply import apply_report_change
enterprise_id = make_enterprise(client)
login = client.post("/api/v1/auth/login", json={"email": "admin@compton.example", "password": "Admin1234"})
token = login.json()["access_token"]
session = client.post(
f"/api/v1/enterprise/enterprises/{enterprise_id}/session",
headers={"Authorization": f"Bearer {token}"},
)
token = session.json()["access_token"]
headers = {"Authorization": f"Bearer {token}"}
loading_id = "lr-test-1"
apply_report_change(
enterprise_id,
"loading_report",
loading_id,
"upsert",
{
"recipe_id": "recipe-1",
"recipe_name": "Test Mix",
"start_time": datetime.now(UTC).isoformat(),
"end_time": datetime.now(UTC).isoformat(),
"total_weight": 1200.0,
"dispenser_type": "dispenser",
"components": [{"component_name": "Corn", "actual_weight": 100.0}],
},
1,
"h1",
)
apply_report_change(
enterprise_id,
"unloading_report",
"ur-test-1",
"upsert",
{
"loading_report_id": loading_id,
"start_time": datetime.now(UTC).isoformat(),
"total_weight": 1200.0,
"total_unloaded_weight": 1100.0,
"remaining_weight": 100.0,
},
1,
"u1",
)
resp = client.get(f"/api/reports?enterprise_id={enterprise_id}", headers=headers)
assert resp.status_code == 200
body = resp.json()
assert isinstance(body, list)
assert len(body) == 1
assert body[0]["recipe_id"] == "recipe-1"
assert body[0]["unloading_data"]["total_unloaded_weight"] == 1100.0
def test_apply_report_change_update_adds_components(client: TestClient):
import json
from app.core.database import session_scope
from app.modules.zootech.report_apply import apply_report_change
from app.modules.zootech.report_models import ZootechLoadingReport
from sqlalchemy import select
enterprise_id = make_enterprise(client)
loading_id = "lr-test-components"
apply_report_change(
enterprise_id,
"loading_report",
loading_id,
"upsert",
{
"recipe_id": "recipe-1",
"recipe_name": "Test Mix",
"start_time": datetime.now(UTC).isoformat(),
"total_weight": 1200.0,
},
1,
"h1",
)
apply_report_change(
enterprise_id,
"loading_report",
loading_id,
"upsert",
{
"recipe_id": "recipe-1",
"recipe_name": "Test Mix",
"start_time": datetime.now(UTC).isoformat(),
"total_weight": 1200.0,
"components": [{"component_name": "Corn", "actual_weight": 100.0, "overload": 5.0}],
},
2,
"h2",
)
with session_scope() as db:
row = db.scalar(
select(ZootechLoadingReport).where(
ZootechLoadingReport.enterprise_id == enterprise_id,
ZootechLoadingReport.id == loading_id,
)
)
assert row is not None
payload = json.loads(row.payload_json or "{}")
assert len(payload.get("components") or []) == 1
assert payload["components"][0]["component_name"] == "Corn"
def test_apply_report_change_feed_alert(client: TestClient):
from app.core.database import session_scope
from app.modules.zootech.report_apply import apply_report_change
from app.modules.zootech.report_models import ZootechFeedAlert
from sqlalchemy import select
enterprise_id = make_enterprise(client)
alert_id = "fa-test-1"
apply_report_change(
enterprise_id,
"feed_alert",
alert_id,
"upsert",
{
"event_type": "UNDERLOAD",
"severity": "error",
"loading_report_id": "lr-1",
"recipe_id": "recipe-1",
"recipe_name": "Mix",
"detail": "Underload detected",
"deviation_kg": -5.0,
},
1,
"h1",
)
with session_scope() as db:
row = db.scalar(
select(ZootechFeedAlert).where(
ZootechFeedAlert.enterprise_id == enterprise_id,
ZootechFeedAlert.id == alert_id,
)
)
assert row is not None
assert row.alert_type == "UNDERLOAD"
assert row.message == "Underload detected"
assert row.payload_json
@@ -0,0 +1,97 @@
def test_wesp_admin_users_shape(client):
login = client.post(
"/api/v1/auth/login",
json={"email": "admin@compton.example", "password": "Admin1234"},
)
token = login.json()["access_token"]
headers = {"Authorization": f"Bearer {token}"}
response = client.get("/api/admin/users", headers=headers)
assert response.status_code == 200
body = response.json()
assert body["status"] == "success"
assert isinstance(body["users"], list)
assert body["users"]
assert "login" in body["users"][0]
def test_wesp_admin_summary_and_stubs(client):
login = client.post(
"/api/v1/auth/login",
json={"email": "admin@compton.example", "password": "Admin1234"},
)
token = login.json()["access_token"]
headers = {"Authorization": f"Bearer {token}"}
summary = client.get("/api/admin/summary", headers=headers)
assert summary.status_code == 200
data = summary.json()
assert data["status"] == "success"
assert "counts" in data
assert "sync" in data
metrics = client.get("/api/admin/system-metrics", headers=headers)
assert metrics.status_code == 200
assert metrics.json()["status"] == "success"
assert "metrics" in metrics.json()
network = client.get("/api/admin/network-settings", headers=headers)
assert network.status_code == 200
assert network.json()["network"]["public_base_url"]
sync_diag = client.get("/api/admin/sync-diagnostics", headers=headers)
assert sync_diag.status_code == 200
assert sync_diag.json()["status"] == "success"
def test_wesp_admin_server_log(client):
login = client.post(
"/api/v1/auth/login",
json={"email": "admin@compton.example", "password": "Admin1234"},
)
headers = {"Authorization": f"Bearer {login.json()['access_token']}"}
response = client.get("/api/admin/server-log?lines=10", headers=headers)
assert response.status_code == 200
body = response.json()
assert body["status"] in {"success", "error"}
assert "lines" in body
download = client.get("/api/admin/server-log/download", headers=headers)
assert download.status_code == 200
assert "text/plain" in (download.headers.get("content-type") or "")
def test_wesp_admin_client_logs_index(client):
login = client.post(
"/api/v1/auth/login",
json={"email": "admin@compton.example", "password": "Admin1234"},
)
headers = {"Authorization": f"Bearer {login.json()['access_token']}"}
response = client.get("/api/admin/client-uploaded-logs", headers=headers)
assert response.status_code == 200
body = response.json()
assert body["status"] == "success"
assert isinstance(body["clients"], list)
def test_wesp_admin_ui_activity(client):
login = client.post(
"/api/v1/auth/login",
json={"email": "admin@compton.example", "password": "Admin1234"},
)
token = login.json()["access_token"]
headers = {"Authorization": f"Bearer {token}"}
response = client.post(
"/api/admin/ui-activity",
headers=headers,
json={"text": "test event", "level": "ok"},
)
assert response.status_code == 200
assert response.json()["status"] == "success"
feed = client.get("/api/admin/activity-feed", headers=headers)
assert feed.status_code == 200
body = feed.json()
assert body["status"] == "success"
assert isinstance(body["entries"], list)
@@ -0,0 +1,24 @@
def test_wesp_auth_login(client):
response = client.post(
"/api/auth/login",
json={"login": "admin@compton.example", "password": "Admin1234"},
)
assert response.status_code == 200
body = response.json()
assert body["status"] == "success"
assert body.get("access_token")
def test_wesp_auth_check(client):
login = client.post(
"/api/v1/auth/login",
json={"email": "admin@compton.example", "password": "Admin1234"},
)
token = login.json()["access_token"]
response = client.get("/api/auth/check", headers={"Authorization": f"Bearer {token}"})
assert response.status_code == 200
body = response.json()
assert body["status"] == "success"
assert body["authenticated"] is True
assert body["user_login"] == "admin@compton.example"
assert body["is_superuser"] is True
@@ -0,0 +1,32 @@
def test_wesp_updates_status(client):
login = client.post(
"/api/v1/auth/login",
json={"email": "admin@compton.example", "password": "Admin1234"},
)
headers = {"Authorization": f"Bearer {login.json()['access_token']}"}
response = client.get("/api/updates/status", headers=headers)
assert response.status_code == 200
body = response.json()
assert body["update_available"] is False
def test_wesp_notifications_summary(client):
login = client.post(
"/api/v1/auth/login",
json={"email": "admin@compton.example", "password": "Admin1234"},
)
headers = {"Authorization": f"Bearer {login.json()['access_token']}"}
response = client.get("/api/notifications?summary=1", headers=headers)
assert response.status_code == 200
assert response.json()["unreadCount"] == 0
def test_wesp_admin_hardware_stub(client):
login = client.post(
"/api/v1/auth/login",
json={"email": "admin@compton.example", "password": "Admin1234"},
)
headers = {"Authorization": f"Bearer {login.json()['access_token']}"}
response = client.get("/api/admin/hardware-status", headers=headers)
assert response.status_code == 200
assert response.json()["status"] == "success"