"""Login hardening: generic failure message, timing-safe unknown-user path, rate limiting, and the import size cap.""" import io import pytest from ten31portal import storage from tests.conftest import make_user def test_login_failure_is_generic_for_unknown_and_wrong_password(client, session): make_user(session, username="alice", password="correct-horse") unknown = client.post("/api/auth/login", json={"login": "nobody", "password": "x"}) wrong = client.post("/api/auth/login", json={"login": "alice", "password": "nope"}) assert unknown.status_code == 401 assert wrong.status_code == 401 # Same message either way, so it doesn't reveal whether the username exists. assert unknown.json()["detail"] == wrong.json()["detail"] == "Invalid username or password" def test_login_is_rate_limited_after_repeated_failures(client, session): make_user(session, username="bob", password="s3cret-pass") for _ in range(10): r = client.post("/api/auth/login", json={"login": "bob", "password": "wrong"}) assert r.status_code == 401 blocked = client.post("/api/auth/login", json={"login": "bob", "password": "wrong"}) assert blocked.status_code == 429 assert "Retry-After" in blocked.headers # Even the correct password is refused while the source IP is throttled. correct = client.post("/api/auth/login", json={"login": "bob", "password": "s3cret-pass"}) assert correct.status_code == 429 def test_successful_login_clears_the_failure_counter(client, session): make_user(session, username="carol", password="right-pass") for _ in range(9): # one shy of the limit assert client.post("/api/auth/login", json={"login": "carol", "password": "no"}).status_code == 401 assert client.post("/api/auth/login", json={"login": "carol", "password": "right-pass"}).status_code == 200 # Counter reset — a fresh run of failures doesn't immediately trip the limit. assert client.post("/api/auth/login", json={"login": "carol", "password": "no"}).status_code == 401 class _StubUpload: """Minimal stand-in for UploadFile: read_capped only touches `.file.read`.""" def __init__(self, data: bytes): self.file = io.BytesIO(data) def test_read_capped_enforces_the_limit(): assert storage.read_capped(_StubUpload(b"x" * 100), limit=1000) == b"x" * 100 with pytest.raises(storage.UploadTooLarge): storage.read_capped(_StubUpload(b"x" * 2000), limit=1000)