v1.5.4: test infrastructure + bug fixes + CI/CD + deploy gate

- 6 new test files: route discovery, strict permissions, role switch,
  CRUD operations, 500 error protection
- permission_config.py: central permission definitions for all 110+ routes
- Bug fixes: is_active checkbox in users_edit, int(id) 500 crash,
  missing validation in users_create/users_delete
- Pre-commit hooks: lint + static routing tests
- GitHub Actions CI: full pytest suite on push
- Deploy gate: pytest runs before deploy, aborts on failure
- All 3 deploy scripts: --skip-tests flag support
This commit is contained in:
2026-05-22 20:45:27 +03:00
parent 64991f0228
commit bd048ea23d
18 changed files with 1989 additions and 10 deletions
@@ -0,0 +1,52 @@
"""Strict permission tests — every role × every route with exact expected codes.
ALLOWED → status must be 200 or 302 (never 403/404/500)
DENIED → status must be 403 (strict — not 302!)
ANONYMOUS → protected routes get 302 (redirect to login)
→ public routes get 200
Dynamic routes (with path params like {slug}) are skipped here.
"""
import pytest
from tests.permission_config import ROUTE_PERMISSIONS, DYNAMIC_ROUTES, ROLES
def _generate_test_cases():
"""Yield (role, method, path, expected) tuples."""
for (method, path), allowed_roles in ROUTE_PERMISSIONS.items():
if (method, path) in DYNAMIC_ROUTES:
continue
for role in ROLES:
if role in allowed_roles or "authenticated" in allowed_roles:
yield (role, method, path, "allow")
else:
yield (role, method, path, "deny")
# Anonymous
if "anonymous" in allowed_roles:
yield ("anonymous", method, path, "allow")
else:
yield ("anonymous", method, path, "deny")
@pytest.mark.parametrize("role,method,path,expected", list(_generate_test_cases()))
def test_access(role_client, role, method, path, expected):
client = role_client[role]
resp = client.request(method, path)
if expected == "allow":
assert resp.status_code not in (403, 404), (
f"[{role}] {method} {path}: got {resp.status_code}, expected non-403/404"
)
if resp.status_code == 500:
pytest.fail(f"[{role}] {method} {path}: 500 Internal Server Error")
else:
if role == "anonymous":
assert resp.status_code in (302, 403), (
f"[{role}] {method} {path}: got {resp.status_code}, expected 302/403"
)
else:
assert resp.status_code == 403, (
f"[{role}] {method} {path}: got {resp.status_code}, expected 403"
)