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:
@@ -0,0 +1,142 @@
|
||||
"""Test role switching lifecycle — owner→engineer→reset, etc.
|
||||
|
||||
These tests verify that the session remains consistent after role switches.
|
||||
The critical check: after a reset, the original role's access is fully restored.
|
||||
"""
|
||||
|
||||
import httpx
|
||||
from tests.conftest import SERVER_URL, PASSWORD
|
||||
|
||||
|
||||
class TestRoleSwitch:
|
||||
|
||||
def _login(self, role: str) -> httpx.Client:
|
||||
"""Create a fresh authenticated client."""
|
||||
c = httpx.Client(base_url=SERVER_URL, follow_redirects=False, timeout=10)
|
||||
resp = c.post("/service/login", data={"login": role, "password": PASSWORD})
|
||||
assert resp.status_code == 302
|
||||
return c
|
||||
|
||||
def _switch_role(self, client: httpx.Client, target: str):
|
||||
resp = client.post("/service/api/role/switch", json={"role": target})
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["ok"] is True
|
||||
return data
|
||||
|
||||
def _reset_role(self, client: httpx.Client):
|
||||
resp = client.post("/service/api/role/reset")
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["ok"] is True
|
||||
return data
|
||||
|
||||
def test_owner_to_engineer_and_back(self):
|
||||
"""Owner switches to engineer, then resets — full access restored."""
|
||||
client = self._login("owner")
|
||||
|
||||
# Owner can access owner pages
|
||||
r = client.get("/service/users")
|
||||
assert r.status_code == 200
|
||||
|
||||
# Switch to engineer
|
||||
data = self._switch_role(client, "engineer")
|
||||
assert data["role"] == "engineer"
|
||||
assert data["original_role"] == "owner"
|
||||
|
||||
# Engineer cannot access owner pages
|
||||
r = client.get("/service/users")
|
||||
assert r.status_code == 403, "Engineer should be denied owner pages"
|
||||
|
||||
# Engineer CAN access shared pages
|
||||
r = client.get("/service/customers")
|
||||
assert r.status_code == 200
|
||||
|
||||
# Reset to owner
|
||||
data = self._reset_role(client)
|
||||
assert data["role"] == "owner"
|
||||
|
||||
# CRITICAL: Owner access restored
|
||||
r = client.get("/service/users")
|
||||
assert r.status_code == 200, "Owner access not restored after reset!"
|
||||
|
||||
# Dashboard still works
|
||||
r = client.get("/service/dashboard")
|
||||
assert r.status_code == 200
|
||||
|
||||
def test_owner_to_technician_and_back(self):
|
||||
"""Owner switches directly to technician."""
|
||||
client = self._login("owner")
|
||||
|
||||
r = client.get("/service/users")
|
||||
assert r.status_code == 200
|
||||
|
||||
self._switch_role(client, "technician")
|
||||
|
||||
r = client.get("/service/users")
|
||||
assert r.status_code == 403
|
||||
|
||||
r = client.get("/service/tasks")
|
||||
assert r.status_code == 200
|
||||
|
||||
self._reset_role(client)
|
||||
|
||||
r = client.get("/service/users")
|
||||
assert r.status_code == 200
|
||||
|
||||
def test_engineer_to_technician_and_back(self):
|
||||
"""Engineer switches to technician, then resets."""
|
||||
client = self._login("engineer")
|
||||
|
||||
# Engineer can access engineer pages
|
||||
r = client.get("/service/customers")
|
||||
assert r.status_code == 200
|
||||
|
||||
self._switch_role(client, "technician")
|
||||
|
||||
# Technician cannot access engineer pages
|
||||
r = client.get("/service/customers")
|
||||
assert r.status_code == 403
|
||||
|
||||
r = client.get("/service/checklist")
|
||||
assert r.status_code == 200
|
||||
|
||||
self._reset_role(client)
|
||||
|
||||
# Engineer access restored
|
||||
r = client.get("/service/customers")
|
||||
assert r.status_code == 200
|
||||
|
||||
def test_double_switch_owner_engineer_technician_then_reset(self):
|
||||
"""Multi-step: owner→engineer→technician→reset→owner."""
|
||||
client = self._login("owner")
|
||||
|
||||
self._switch_role(client, "engineer")
|
||||
r = client.get("/service/users")
|
||||
assert r.status_code == 403
|
||||
|
||||
# Switch deeper while already impersonating
|
||||
self._switch_role(client, "technician")
|
||||
r = client.get("/service/users")
|
||||
assert r.status_code == 403
|
||||
r = client.get("/service/tasks")
|
||||
assert r.status_code == 200
|
||||
|
||||
# Single reset should restore owner
|
||||
self._reset_role(client)
|
||||
r = client.get("/service/users")
|
||||
assert r.status_code == 200
|
||||
|
||||
def test_forbidden_switch_technician_cannot_switch(self):
|
||||
"""Technician cannot use role switch (not in allowed_roles)."""
|
||||
client = self._login("technician")
|
||||
resp = client.post("/service/api/role/switch", json={"role": "owner"})
|
||||
assert resp.status_code == 403
|
||||
|
||||
def test_reset_without_switch_does_not_error(self):
|
||||
"""Reset without an active override should still work."""
|
||||
client = self._login("owner")
|
||||
resp = client.post("/service/api/role/reset")
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["role"] == "owner"
|
||||
Reference in New Issue
Block a user