from app.site_auth import LoginLimiter, TicketStore class Clock: def __init__(self): self.t = 1000.0 def __call__(self): return self.t def test_limiter_blocks_after_limit(): lim = LoginLimiter(limit=3, window=60, now=Clock()) for _ in range(3): assert not lim.blocked("ip") lim.fail("ip") assert lim.blocked("ip") assert not lim.blocked("other-ip") def test_limiter_forgets_expired_keys(): # made-up usernames make a new key per guess; once their strikes expire they're dropped clock = Clock() lim = LoginLimiter(limit=5, window=60, now=clock) for i in range(100): lim.fail(f"ip|user{i}") clock.t += 61 lim.fail("ip|someone") assert list(lim.fails) == ["ip|someone"] def test_limiter_window_slides(): clock = Clock() lim = LoginLimiter(limit=2, window=60, now=clock) lim.fail("ip") lim.fail("ip") assert lim.blocked("ip") clock.t += 61 assert not lim.blocked("ip") def test_limiter_reset(): lim = LoginLimiter(limit=1, window=60, now=Clock()) lim.fail("ip") lim.reset("ip") assert not lim.blocked("ip") def test_ticket_redeems_once(): store = TicketStore(ttl=60, now=Clock()) t = store.issue("tok") assert len(t) >= 32 assert store.redeem(t) == "tok" assert store.redeem(t) is None def test_ticket_expires(): clock = Clock() store = TicketStore(ttl=60, now=clock) t = store.issue("tok") clock.t += 61 assert store.redeem(t) is None def test_unknown_ticket(): assert TicketStore().redeem("nope") is None