Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion backend/secuscan/rate_limiter.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
import redis.asyncio as aioredis
from fastapi import HTTPException, Request, status

from .ratelimit_helpers import RateLimiterKeyBuilder

logger = logging.getLogger(__name__)


Expand Down Expand Up @@ -67,7 +69,7 @@ def _get_client_ip(self, request: Request) -> str:

def _make_key(self, ip: str, window_type: str, window_value: int) -> str:
"""Build a namespaced Redis key for this IP and time window."""
return f"rate_limit:scan:{ip}:{window_type}:{window_value}"
return RateLimiterKeyBuilder.make_key(ip, window_type, window_value)

async def check(self, request: Request) -> None:
"""
Expand Down
34 changes: 34 additions & 0 deletions backend/secuscan/ratelimit_helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""
Redis key-builder helpers for ScanRateLimiter.

Extracted from rate_limiter.py so _make_key can be unit-tested without
pulling in the redis.asyncio / fastapi import chain.

Public API
----------
RateLimiterKeyBuilder.make_key(ip: str, window_type: str, window_value: int) -> str
Builds the namespaced Redis key for a per-IP rate-limit bucket.

Key format: ``rate_limit:scan:{ip}:{window_type}:{window_value}``
"""

from __future__ import annotations


class RateLimiterKeyBuilder:
"""Pure key-formatter for ScanRateLimiter Redis keys."""

@staticmethod
def make_key(ip: str, window_type: str, window_value: int) -> str:
"""Build a namespaced Redis key for this IP and time window.

Args:
ip: Client IP address (e.g. "192.168.1.1").
window_type: Time-window label, typically ``"minute"`` or ``"hour"``.
window_value: Unix timestamp of the window start (seconds).

Returns:
Redis key string of the form:
``rate_limit:scan:{ip}:{window_type}:{window_value}``
"""
return f"rate_limit:scan:{ip}:{window_type}:{window_value}"
48 changes: 48 additions & 0 deletions testing/backend/unit/test_ratelimit_helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"""
Unit tests for RateLimiterKeyBuilder.make_key in
backend.secuscan.ratelimit_helpers.

Tests the key-formatting logic for ScanRateLimiter Redis buckets.
"""

import pytest
from backend.secuscan.ratelimit_helpers import RateLimiterKeyBuilder


class TestRateLimiterKeyBuilderMakeKey:
def test_standard_ipv4_minute_window(self):
key = RateLimiterKeyBuilder.make_key("192.168.1.100", "minute", 1700000000)
assert key == "rate_limit:scan:192.168.1.100:minute:1700000000"

def test_standard_ipv4_hour_window(self):
key = RateLimiterKeyBuilder.make_key("10.0.0.5", "hour", 1700000000)
assert key == "rate_limit:scan:10.0.0.5:hour:1700000000"

def test_localhost_minute_window(self):
key = RateLimiterKeyBuilder.make_key("127.0.0.1", "minute", 1700003600)
assert key == "rate_limit:scan:127.0.0.1:minute:1700003600"

def test_ipv6_address(self):
key = RateLimiterKeyBuilder.make_key("::1", "minute", 1700000000)
assert key == "rate_limit:scan:::1:minute:1700000000"

def test_window_value_zero(self):
key = RateLimiterKeyBuilder.make_key("1.2.3.4", "minute", 0)
assert key == "rate_limit:scan:1.2.3.4:minute:0"

def test_window_value_large(self):
key = RateLimiterKeyBuilder.make_key("8.8.8.8", "hour", 9999999999)
assert key == "rate_limit:scan:8.8.8.8:hour:9999999999"

def test_key_format_exactly_four_components(self):
key = RateLimiterKeyBuilder.make_key("5.5.5.5", "minute", 123)
parts = key.split(":")
assert parts == ["rate_limit", "scan", "5.5.5.5", "minute", "123"]

def test_empty_ip_produces_valid_key(self):
key = RateLimiterKeyBuilder.make_key("", "minute", 100)
assert key == "rate_limit:scan::minute:100"

def test_unknown_window_type_produces_valid_key(self):
key = RateLimiterKeyBuilder.make_key("1.1.1.1", "second", 60)
assert key == "rate_limit:scan:1.1.1.1:second:60"
Loading