"""Тесты ограничителя частоты запросов.""" import unittest from splitapi.ratelimit import RateLimiter class FakeClock: def __init__(self): self.t = 1000.0 def __call__(self): return self.t class RateLimiterTests(unittest.TestCase): def setUp(self): self.clock = FakeClock() self.limiter = RateLimiter(clock=self.clock) def test_allows_up_to_limit_then_blocks(self): for _ in range(3): self.assertTrue(self.limiter.check("k", 3, 60)[0]) allowed, retry = self.limiter.check("k", 3, 60) self.assertFalse(allowed) self.assertGreaterEqual(retry, 1) def test_window_slides(self): for _ in range(3): self.limiter.check("k", 3, 60) self.clock.t += 61 self.assertTrue(self.limiter.check("k", 3, 60)[0]) def test_retry_after_matches_oldest_hit(self): self.limiter.check("k", 2, 60) self.clock.t += 20 self.limiter.check("k", 2, 60) allowed, retry = self.limiter.check("k", 2, 60) self.assertFalse(allowed) self.assertEqual(retry, 40) def test_keys_are_independent(self): self.assertTrue(self.limiter.check("a", 1, 60)[0]) self.assertFalse(self.limiter.check("a", 1, 60)[0]) self.assertTrue(self.limiter.check("b", 1, 60)[0]) def test_blocked_attempts_do_not_extend_the_block(self): self.limiter.check("k", 1, 60) for _ in range(50): self.limiter.check("k", 1, 60) self.clock.t += 61 self.assertTrue(self.limiter.check("k", 1, 60)[0]) def test_is_blocked_does_not_count_attempts(self): for _ in range(10): self.assertFalse(self.limiter.is_blocked("k", 2, 60)[0]) def test_recorded_failures_lead_to_block_and_expire(self): self.limiter.record("k", 60) self.limiter.record("k", 60) blocked, retry = self.limiter.is_blocked("k", 2, 60) self.assertTrue(blocked) self.assertGreaterEqual(retry, 1) self.clock.t += 61 self.assertFalse(self.limiter.is_blocked("k", 2, 60)[0]) def test_forgive_gives_back_the_latest_attempt(self): self.limiter.check("k", 2, 60) self.limiter.check("k", 2, 60) self.assertFalse(self.limiter.check("k", 2, 60)[0]) self.limiter.forgive("k") self.assertTrue(self.limiter.check("k", 2, 60)[0]) def test_forgive_unknown_key_is_harmless(self): self.limiter.forgive("never-seen") self.assertTrue(self.limiter.check("never-seen", 1, 60)[0]) def test_successful_attempts_can_be_forgiven_indefinitely(self): for _ in range(50): self.assertTrue(self.limiter.check("ok", 3, 60)[0]) self.limiter.forgive("ok") def test_expired_entries_are_dropped_before_live_ones(self): limiter = RateLimiter(clock=self.clock, max_keys=3) limiter.check("old1", 1, 60) limiter.check("old2", 1, 60) self.clock.t += 100 limiter.check("new1", 1, 60) limiter.check("new2", 1, 60) # тут превышение: просроченные уходят первыми self.assertEqual(limiter.size(), 2) self.assertFalse(limiter.check("new1", 1, 60)[0]) # живой счётчик не потерян def test_concurrent_checks_never_exceed_the_limit(self): import threading allowed = [] gate = threading.Barrier(16) def worker(): gate.wait() allowed.append(self.limiter.check("race", 5, 60)[0]) threads = [threading.Thread(target=worker) for _ in range(16)] for t in threads: t.start() for t in threads: t.join() self.assertEqual(sum(allowed), 5) def test_memory_is_bounded(self): limiter = RateLimiter(clock=self.clock, max_keys=100) for i in range(500): limiter.check(f"k{i}", 5, 60) self.clock.t += 0.01 self.assertLessEqual(limiter.size(), 100) if __name__ == "__main__": unittest.main()