test(auth): login rate limit
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
"""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():
|
||||
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
|
||||
Reference in New Issue
Block a user