Files
cyclone/backend/tests/test_auth_login_rate_limit.py
T
Nora 8fc3d9adda 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.
2026-06-22 15:59:08 -06:00

87 lines
2.8 KiB
Python

"""Login rate limit: 5 fails / 5 min per username."""
from __future__ import annotations
import pytest
from fastapi.testclient import TestClient
from sqlalchemy import delete
from cyclone.api import app
from cyclone.auth import rate_limit, users
from cyclone.db import Session as DbSession
from cyclone.db import SessionLocal, User
@pytest.fixture(autouse=True)
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:
db.execute(delete(DbSession))
db.execute(delete(User))
db.commit()
# Reset the in-memory rate-limit counter so a previous test's 5
# failures don't poison this test. The plan recipe calls this out.
rate_limit.reset("victim")
yield
with SessionLocal()() as db:
db.execute(delete(DbSession))
db.execute(delete(User))
db.commit()
rate_limit.reset("victim")
def test_5_fails_then_429():
with SessionLocal()() as db:
users.create(db, username="victim", password="hunter2hunter2", role="user")
client = TestClient(app)
for _ in range(5):
r = client.post(
"/api/auth/login",
json={"username": "victim", "password": "WRONG"},
)
assert r.status_code == 401
# 6th should be 429.
r = client.post(
"/api/auth/login",
json={"username": "victim", "password": "WRONG"},
)
assert r.status_code == 429
assert "Retry-After" in r.headers
# And the module-level helper confirms we're now over the threshold.
assert rate_limit.check("victim") > 0
def test_successful_login_resets_counter():
with SessionLocal()() as db:
users.create(db, username="victim", password="hunter2hunter2", role="user")
client = TestClient(app)
for _ in range(4):
r = client.post(
"/api/auth/login",
json={"username": "victim", "password": "WRONG"},
)
assert r.status_code == 401
# Successful login — should reset the counter.
r = client.post(
"/api/auth/login",
json={"username": "victim", "password": "hunter2hunter2"},
)
assert r.status_code == 200
# Counter reset — 5 more fails allowed.
for _ in range(5):
r = client.post(
"/api/auth/login",
json={"username": "victim", "password": "WRONG"},
)
assert r.status_code == 401
# 6th is now throttled.
r = client.post(
"/api/auth/login",
json={"username": "victim", "password": "WRONG"},
)
assert r.status_code == 429