test(auth): add test_existing_endpoints_require_auth + per-test AUTH_DISABLED flips
- New test_existing_endpoints_require_auth.py: spot-check that existing /api/* endpoints now require auth (gated via Depends(matrix_gate)) when AUTH_DISABLED is False. Health remains public. - conftest.py: flip AUTH_DISABLED=True for the suite so the legacy pre-auth tests keep passing without login. Auth tests flip it back off via their own autouse fixture (now patched to use monkeypatch for cleanup). Verified: 53 auth tests pass; 222 pre-existing non-auth failures are unchanged.
This commit is contained in:
@@ -24,17 +24,28 @@ def _auto_init_db(tmp_path, monkeypatch):
|
|||||||
Also wires a fresh ``EventBus`` onto ``app.state`` because ``TestClient``
|
Also wires a fresh ``EventBus`` onto ``app.state`` because ``TestClient``
|
||||||
does not invoke the FastAPI lifespan handler unless used as a context
|
does not invoke the FastAPI lifespan handler unless used as a context
|
||||||
manager. The bus is reset between tests so subscribers don't leak.
|
manager. The bus is reset between tests so subscribers don't leak.
|
||||||
|
|
||||||
|
Auth gating is enabled at the router/endpoint level via the
|
||||||
|
``Depends(matrix_gate)`` wiring in ``cyclone.api``. The auth tests
|
||||||
|
(``test_auth_*``) explicitly flip ``AUTH_DISABLED = False`` and
|
||||||
|
authenticate via the public login route to exercise the real
|
||||||
|
auth path. Every other test — the original test suite predates
|
||||||
|
auth — gets ``AUTH_DISABLED = True`` here so the existing tests
|
||||||
|
keep working without each one having to login first.
|
||||||
"""
|
"""
|
||||||
monkeypatch.setenv("CYCLONE_DB_URL", f"sqlite:///{tmp_path}/test.db")
|
monkeypatch.setenv("CYCLONE_DB_URL", f"sqlite:///{tmp_path}/test.db")
|
||||||
from cyclone import db
|
from cyclone import db
|
||||||
from cyclone.api import app
|
from cyclone.api import app
|
||||||
from cyclone.pubsub import EventBus
|
from cyclone.pubsub import EventBus
|
||||||
|
from cyclone.auth import deps
|
||||||
|
|
||||||
db._reset_for_tests()
|
db._reset_for_tests()
|
||||||
db.init_db()
|
db.init_db()
|
||||||
app.state.event_bus = EventBus()
|
app.state.event_bus = EventBus()
|
||||||
|
deps.AUTH_DISABLED = True
|
||||||
try:
|
try:
|
||||||
yield
|
yield
|
||||||
finally:
|
finally:
|
||||||
|
deps.AUTH_DISABLED = False
|
||||||
app.state.event_bus = None
|
app.state.event_bus = None
|
||||||
db._reset_for_tests()
|
db._reset_for_tests()
|
||||||
@@ -13,7 +13,11 @@ from cyclone.db import SessionLocal, User
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def _clear():
|
def _clear(monkeypatch):
|
||||||
|
# conftest sets AUTH_DISABLED=True so pre-auth tests keep passing
|
||||||
|
# without a login. The auth-admin tests need the real auth path
|
||||||
|
# exercised, so flip it back off here.
|
||||||
|
monkeypatch.setattr("cyclone.auth.deps.AUTH_DISABLED", False)
|
||||||
with SessionLocal()() as db:
|
with SessionLocal()() as db:
|
||||||
db.execute(delete(DbSession))
|
db.execute(delete(DbSession))
|
||||||
db.execute(delete(User))
|
db.execute(delete(User))
|
||||||
|
|||||||
@@ -13,7 +13,12 @@ from cyclone.db import SessionLocal, User
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def _clear():
|
def _clear(monkeypatch):
|
||||||
|
# conftest sets AUTH_DISABLED=True so pre-auth tests keep passing
|
||||||
|
# without a login. The auth-login-rate-limit tests need the real
|
||||||
|
# auth path exercised (the rate limiter sits in front of /login),
|
||||||
|
# so flip it back off here.
|
||||||
|
monkeypatch.setattr("cyclone.auth.deps.AUTH_DISABLED", False)
|
||||||
with SessionLocal()() as db:
|
with SessionLocal()() as db:
|
||||||
db.execute(delete(DbSession))
|
db.execute(delete(DbSession))
|
||||||
db.execute(delete(User))
|
db.execute(delete(User))
|
||||||
|
|||||||
@@ -13,7 +13,11 @@ from cyclone.db import SessionLocal, User
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def _clear():
|
def _clear(monkeypatch):
|
||||||
|
# conftest sets AUTH_DISABLED=True so pre-auth tests keep passing
|
||||||
|
# without a login. The auth-route tests need the real auth path
|
||||||
|
# exercised, so flip it back off here.
|
||||||
|
monkeypatch.setattr("cyclone.auth.deps.AUTH_DISABLED", False)
|
||||||
with SessionLocal()() as db:
|
with SessionLocal()() as db:
|
||||||
db.execute(delete(DbSession))
|
db.execute(delete(DbSession))
|
||||||
db.execute(delete(User))
|
db.execute(delete(User))
|
||||||
|
|||||||
@@ -13,7 +13,11 @@ from cyclone.db import SessionLocal, User
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def _clear():
|
def _clear(monkeypatch):
|
||||||
|
# conftest sets AUTH_DISABLED=True so pre-auth tests keep passing
|
||||||
|
# without a login. The auth-sessions tests need the real auth path
|
||||||
|
# exercised, so flip it back off here.
|
||||||
|
monkeypatch.setattr("cyclone.auth.deps.AUTH_DISABLED", False)
|
||||||
with SessionLocal()() as db:
|
with SessionLocal()() as db:
|
||||||
db.execute(delete(DbSession))
|
db.execute(delete(DbSession))
|
||||||
db.execute(delete(User))
|
db.execute(delete(User))
|
||||||
|
|||||||
@@ -11,7 +11,11 @@ from cyclone.db import SessionLocal, User
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def _clear_users():
|
def _clear_users(monkeypatch):
|
||||||
|
# conftest sets AUTH_DISABLED=True so pre-auth tests keep passing
|
||||||
|
# without a login. The auth-users tests need the real auth path
|
||||||
|
# exercised, so flip it back off here.
|
||||||
|
monkeypatch.setattr("cyclone.auth.deps.AUTH_DISABLED", False)
|
||||||
with SessionLocal()() as db:
|
with SessionLocal()() as db:
|
||||||
db.execute(delete(User))
|
db.execute(delete(User))
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
"""Spot-check that existing endpoints now require auth (when AUTH_DISABLED is not set).
|
||||||
|
|
||||||
|
The conftest in ``tests/conftest.py`` flips ``AUTH_DISABLED=True`` for
|
||||||
|
every test module that does NOT start with ``test_auth`` — this module
|
||||||
|
deliberately is NOT named ``test_auth_*`` so it inherits the default
|
||||||
|
disabled posture... wait, that's the opposite of what we want.
|
||||||
|
|
||||||
|
This module is named ``test_existing_endpoints_require_auth`` so the
|
||||||
|
conftest will treat it as a legacy test and set ``AUTH_DISABLED=True``,
|
||||||
|
bypassing the auth check entirely. To actually verify the gate, this
|
||||||
|
test's ``client`` fixture flips ``AUTH_DISABLED`` to ``False``
|
||||||
|
*just for this test*, then restores it afterwards. This way the
|
||||||
|
rest of the suite still sees the disabled posture and existing
|
||||||
|
tests keep passing.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
from sqlalchemy import delete
|
||||||
|
|
||||||
|
from cyclone.api import app
|
||||||
|
from cyclone.auth import deps as _auth_deps
|
||||||
|
from cyclone.auth.deps import AUTH_DISABLED
|
||||||
|
from cyclone.db import Session as DbSession
|
||||||
|
from cyclone.db import SessionLocal, User
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def _clear():
|
||||||
|
with SessionLocal()() as db:
|
||||||
|
db.execute(delete(DbSession))
|
||||||
|
db.execute(delete(User))
|
||||||
|
db.commit()
|
||||||
|
yield
|
||||||
|
with SessionLocal()() as db:
|
||||||
|
db.execute(delete(DbSession))
|
||||||
|
db.execute(delete(User))
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def client():
|
||||||
|
# Force AUTH_DISABLED=False for these tests so the dep actually checks the cookie.
|
||||||
|
original = AUTH_DISABLED
|
||||||
|
_auth_deps.AUTH_DISABLED = False
|
||||||
|
try:
|
||||||
|
yield TestClient(app)
|
||||||
|
finally:
|
||||||
|
_auth_deps.AUTH_DISABLED = original
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("method,path", [
|
||||||
|
("GET", "/api/claims"),
|
||||||
|
("GET", "/api/remittances"),
|
||||||
|
("GET", "/api/providers"),
|
||||||
|
("GET", "/api/batches"),
|
||||||
|
("GET", "/api/payers/p1/summary"),
|
||||||
|
("GET", "/api/activity"),
|
||||||
|
])
|
||||||
|
def test_existing_get_endpoints_require_auth(client, method, path):
|
||||||
|
resp = client.request(method, path)
|
||||||
|
assert resp.status_code == 401, f"{method} {path} returned {resp.status_code}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_health_is_public(client):
|
||||||
|
"""``/api/health`` is the public healthcheck and must remain reachable."""
|
||||||
|
resp = client.get("/api/health")
|
||||||
|
assert resp.status_code != 401, f"/api/health returned {resp.status_code}"
|
||||||
Reference in New Issue
Block a user