Files
MajoorWaldi-ComfyUI-Majoor-…/tests/security/test_rate_limit.py
T
Ewald a2053d0cba refactor(v2): plan V2 complet — splits JS, tests frontend, durcissement securite
Chantier F - Typage Python
- ruff S101 déplacé en per-file-ignores (pyproject.toml)
- pyrightconfig.json : mode standard confirmé

Chantier G - Assertions & bare-except
- 16 blocs except documentés dans config.py
- --cov-branch activé, seuil coverage 60→68%

Chantier H - Cache settings
- Stratégie documentée dans settings.py
- _resolve_index_dir() logguée dans config.py

Chantier I - Split gros fichiers JS
- client.js 1509L → client.js 406L + clientAuth.js + clientOps.js + fetchUtils.js
- toolbar.js 927L → toolbar.js 458L + toolbarActions.js + toolbarControls.js
- FloatingViewer.js → FloatingViewer.impl.js + floatingViewer*(8 modules)
- Viewer_impl.js → ViewerRuntime.js + viewerShell.js + viewerThemeStyles.js + viewerOverlayDismiss.js
- model3dRenderer.js → model3dRenderer.impl.js + model3dCore.js + model3dLoaders.js + model3dSupport.js
- dialogs.js → dialogs.js + DialogTemplates.js

Chantier J - Tests frontend (Phases 1-2)
- use_drag_drop.vitest.mjs : 5 tests
- use_grid_state.vitest.mjs : 18 tests
- security_client_headers, security_deep_merge, security_runtime_state, runtime_store, dialog_templates
- Total: 80 fichiers test / 350 tests vitest

Chantier K - Pinia migration
- runtimeState.js entièrement migré vers Pinia
- getOptionalRuntimeStore.js + runtimeEnrichmentState.js ajoutés

Chantier L - Durcissement sécurité
- tests/security/test_rate_limit.py : 8 tests rate-limit (state machine complète)
- clientAuth.js : docstring sessionStorage security boundary
- three.js 0.183.2 audité : 0 CVE (Snyk)

Chantier M - Simplification config
- deps.py, tool_detect.py, utils.py nettoyés

docs: PLAN_REFACTO_V2_LONG_TERME.md ajouté et clôturé
.gitignore: coverage/ ajouté
2026-04-11 09:38:41 +02:00

158 lines
5.0 KiB
Python

"""Tests for the rate-limit state machine in security_rate_limit.py."""
from __future__ import annotations
import time
from typing import cast
import pytest
from aiohttp import web
from mjr_am_backend.routes.core.security_rate_limit import (
_check_rate_limit,
_reset_rate_limit_state_for_tests,
)
class _DummyTransport:
def __init__(self, peername: tuple[str, int] = ("10.0.0.1", 9999)):
self._peername = peername
def get_extra_info(self, key: str):
if key == "peername":
return self._peername
return None
class _FakeRequest:
def __init__(self, *, ip: str = "10.0.0.1", port: int = 9999):
self.headers: dict[str, str] = {}
self.transport = _DummyTransport((ip, port))
self.remote = ip
self.app: dict = {}
def _make_req(ip: str = "10.0.0.1", port: int = 9999) -> web.Request:
"""Return a fake aiohttp Request cast to satisfy mypy."""
return cast(web.Request, _FakeRequest(ip=ip, port=port))
@pytest.fixture(autouse=True)
def _clean_state():
_reset_rate_limit_state_for_tests()
yield
_reset_rate_limit_state_for_tests()
class TestCheckRateLimit:
"""Unit tests for _check_rate_limit."""
def test_allows_under_limit(self) -> None:
req = _make_req()
allowed, retry = _check_rate_limit(req, "search", max_requests=5, window_seconds=60)
assert allowed is True
assert retry is None
def test_blocks_after_limit_exceeded(self) -> None:
req = _make_req()
for _ in range(5):
allowed, _ = _check_rate_limit(req, "search", max_requests=5, window_seconds=60)
assert allowed is True
allowed, retry = _check_rate_limit(req, "search", max_requests=5, window_seconds=60)
assert allowed is False
assert isinstance(retry, int)
assert retry >= 1
def test_independent_endpoints(self) -> None:
req = _make_req()
for _ in range(3):
_check_rate_limit(req, "ep_a", max_requests=3, window_seconds=60)
allowed, _ = _check_rate_limit(req, "ep_a", max_requests=3, window_seconds=60)
assert allowed is False
allowed, _ = _check_rate_limit(req, "ep_b", max_requests=3, window_seconds=60)
assert allowed is True
def test_independent_clients(self) -> None:
req_a = _make_req(ip="10.0.0.1")
req_b = _make_req(ip="10.0.0.2")
for _ in range(3):
_check_rate_limit(req_a, "search", max_requests=3, window_seconds=60)
allowed_a, _ = _check_rate_limit(req_a, "search", max_requests=3, window_seconds=60)
assert allowed_a is False
allowed_b, _ = _check_rate_limit(req_b, "search", max_requests=3, window_seconds=60)
assert allowed_b is True
def test_window_expiry_resets_counter(self) -> None:
req = _make_req()
for _ in range(3):
_check_rate_limit(req, "search", max_requests=3, window_seconds=1)
allowed, _ = _check_rate_limit(req, "search", max_requests=3, window_seconds=1)
assert allowed is False
time.sleep(1.1)
allowed, _ = _check_rate_limit(req, "search", max_requests=3, window_seconds=1)
assert allowed is True
def test_client_eviction_on_overflow(self) -> None:
for i in range(5):
req = _make_req(ip=f"192.168.0.{i}")
_check_rate_limit(req, "x", max_requests=100, window_seconds=60, _max_clients=3)
req_first = _make_req(ip="192.168.0.0")
allowed, _ = _check_rate_limit(
req_first, "x", max_requests=1, window_seconds=60, _max_clients=3
)
assert allowed is True, "oldest client should have been evicted, counter reset"
def test_download_rate_limit_response_or_none(self) -> None:
from mjr_am_backend.features.assets.download_service import (
download_rate_limit_response_or_none,
)
req = _make_req()
def is_loopback(r: object) -> bool:
return False
check_rl = _check_rate_limit
for _ in range(30):
resp = download_rate_limit_response_or_none(
req,
preview=False,
is_loopback_request=is_loopback,
check_rate_limit=check_rl,
)
assert resp is None
resp = download_rate_limit_response_or_none(
req,
preview=False,
is_loopback_request=is_loopback,
check_rate_limit=check_rl,
)
assert isinstance(resp, web.Response)
assert resp.status == 429
def test_loopback_bypasses_rate_limit(self) -> None:
from mjr_am_backend.features.assets.download_service import (
download_rate_limit_response_or_none,
)
req = _make_req()
for _ in range(50):
resp = download_rate_limit_response_or_none(
req,
preview=False,
is_loopback_request=lambda r: True,
check_rate_limit=_check_rate_limit,
)
assert resp is None