Files
MobilityOps/backend/tests/test_auth.py
T

155 lines
5.9 KiB
Python

def test_unauthenticated_dashboard_is_rejected(client):
response = client.get("/api/v1/dashboard")
assert response.status_code == 401
assert response.json()["error"]["code"] == "401"
def test_oidc_status_is_disabled_without_configuration(client):
assert client.get("/api/v1/auth/oidc/status").json() == {
"enabled": False,
"provider_name": None,
}
assert client.get("/api/v1/auth/oidc/login").status_code == 404
def test_oidc_callback_auto_provisions_and_logs_in(client, monkeypatch):
import app.api.routers.auth as auth_router
class FakeClient:
async def authorize_access_token(self, _request):
return {
"userinfo": {
"sub": "external-subject-1",
"email": "oidc.user@example.test",
"email_verified": True,
"name": "OIDC User",
}
}
class FakeOAuth:
def create_client(self, _name):
return FakeClient()
monkeypatch.setattr(auth_router.settings, "oidc_enabled", True)
monkeypatch.setattr(auth_router.settings, "oidc_issuer_url", "https://id.example.test")
monkeypatch.setattr(auth_router.settings, "oidc_client_id", "client")
monkeypatch.setattr(auth_router.settings, "oidc_client_secret", "secret")
monkeypatch.setattr(auth_router.settings, "oidc_allowed_email_domains", "example.test")
monkeypatch.setattr(auth_router, "oauth", FakeOAuth())
response = client.get("/api/v1/auth/oidc/callback", follow_redirects=False)
assert response.status_code == 307
assert response.headers["location"].endswith("/dashboard")
session = client.get("/api/v1/auth/session")
assert session.status_code == 200
assert session.json()["display_name"] == "OIDC User"
assert session.json()["role"] == "rental_employee"
def test_demo_login_grants_access(ops_client):
response = ops_client.get("/api/v1/dashboard")
assert response.status_code == 200
def test_rental_employee_cannot_reset_demo(employee_client):
response = employee_client.post("/api/v1/demo/reset")
assert response.status_code == 403
def test_operations_manager_can_reset_demo(ops_client):
response = ops_client.post("/api/v1/demo/reset")
assert response.status_code == 200
body = response.json()
assert body["counts"]["vehicles"] == 50
assert body["anchor_date"]
assert body["seeded_at"]
assert body["scenario_integrity"]["all_ready"] is True
assert body["scenario_integrity"]["not_ready"] == []
def test_reset_is_rejected_when_demo_allow_reset_is_disabled(ops_client, monkeypatch):
import app.api.routers.demo as demo_router
monkeypatch.setattr(demo_router.settings, "demo_allow_reset", False)
response = ops_client.post("/api/v1/demo/reset")
assert response.status_code == 403
# Restore real demo data: this test intentionally disabled reset, so a following test
# module must not inherit a database left mid-mutation by an earlier test.
monkeypatch.setattr(demo_router.settings, "demo_allow_reset", True)
assert ops_client.post("/api/v1/demo/reset").status_code == 200
def test_session_endpoint_requires_authentication(client):
response = client.get("/api/v1/demo/session")
assert response.status_code == 401
def test_session_endpoint_confirms_logged_in_user(ops_client):
response = ops_client.get("/api/v1/demo/session")
assert response.status_code == 200
body = response.json()
assert body["role"] == "operations_manager"
assert body["public_ref"] == "USR-OPS"
def test_logout_invalidates_session(ops_client):
confirmed = ops_client.get("/api/v1/demo/session")
assert confirmed.status_code == 200
logout = ops_client.post("/api/v1/demo/logout")
assert logout.status_code == 200
after = ops_client.get("/api/v1/demo/session")
assert after.status_code == 401
def test_logout_rejects_a_cookie_even_if_the_browser_retains_it(ops_client):
from app.core.config import get_settings
cookie_name = get_settings().session_cookie_name
stolen_token = ops_client.cookies.get(cookie_name)
assert stolen_token
logout = ops_client.post("/api/v1/auth/logout")
assert logout.status_code == 200
# Simulate the browser cookie race (or a copied cookie): server-side revocation is
# authoritative and must reject the original signed token independently of deletion.
ops_client.cookies.set(cookie_name, stolen_token)
assert ops_client.get("/api/v1/auth/session").status_code == 401
# Remove the deliberately injected hostless cookie before exercising a normal browser
# login. Otherwise httpx sends it alongside the real testserver cookie, which is not a
# state a browser can create for the same origin/path pair.
ops_client.cookies.clear()
# A fresh login in the same second receives a distinct signed token and remains valid.
fresh_login = ops_client.post("/api/v1/demo/login", json={"role": "operations_manager"})
assert fresh_login.status_code == 200
assert ops_client.get("/api/v1/auth/session").status_code == 200
def test_logout_without_a_session_is_safe(client):
response = client.post("/api/v1/demo/logout")
assert response.status_code == 200
def test_demo_reset_preserves_integration_telemetry(ops_client, client):
from app.core.config import get_settings
settings = get_settings()
probe = client.get(
"/api/v1/integrations/mcp/operations-summary",
headers={
"X-Service-Token": settings.mcp_hub_service_token,
"X-Client-Id": "itworx-mcp-hub:mobilityops:reset-probe",
},
)
assert probe.status_code == 200
assert ops_client.post("/api/v1/demo/reset").status_code == 200
assert client.post("/api/v1/demo/login", json={"role": "operations_manager"}).status_code == 200
events = client.get("/api/v1/audit", params={"action": "mcp_tool_request"}).json()
assert any(event["actor_label"].endswith(":reset-probe") for event in events)