- tests/test_auth.py: enable/disable via env, valid/invalid creds - tests/test_rate_limit.py: bucket math + window expiry - tests/test_router_media.py: path traversal (file/thumb), paginated subreddits/authors, blacklist preview shape, DELETE flow, cleanup-by-type 400 paths - tests/test_router_stats.py: cache TTL, .json exclusion, missing dir - tests/test_router_favorites.py: paginated favorites/authors, no N+1 (query count), favorites_only filter - tests/test_router_config.py: CRUD subreddits/users/blacklist - tests/test_config_manager.py: case-insensitive dedupe, blacklist removes from collection, domain normalization - tests/test_extractors_imgur.py: i.imgur direct, gifv->mp4, albums/galleries -> None src/database.py: resolve RMC_DB_PATH lazily in Database() so tests can monkeypatch the env per case (no more module-import capture) Result: 135 passed (was 77). Coverage 55% global; 100% on auth/imgur, 96% rate_limit, 84% stats router, 80% config_manager.
61 lines
1.6 KiB
Python
61 lines
1.6 KiB
Python
"""Tests for the in-memory rate limiter."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import time
|
|
|
|
import pytest
|
|
from fastapi import FastAPI
|
|
from fastapi.testclient import TestClient
|
|
|
|
from src.web.rate_limit import _reset_buckets, rate_limit
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def reset_buckets():
|
|
_reset_buckets()
|
|
yield
|
|
_reset_buckets()
|
|
|
|
|
|
def _make_app(max_calls: int, window: float) -> FastAPI:
|
|
app = FastAPI()
|
|
|
|
@app.get("/limited", dependencies=[pytest.importorskip("fastapi").Depends(rate_limit(max_calls, window))])
|
|
async def _route():
|
|
return {"ok": True}
|
|
|
|
return app
|
|
|
|
|
|
def test_allows_within_window():
|
|
app = _make_app(3, 60.0)
|
|
client = TestClient(app)
|
|
for _ in range(3):
|
|
assert client.get("/limited").status_code == 200
|
|
|
|
|
|
def test_blocks_when_exceeded():
|
|
app = _make_app(2, 60.0)
|
|
client = TestClient(app)
|
|
assert client.get("/limited").status_code == 200
|
|
assert client.get("/limited").status_code == 200
|
|
response = client.get("/limited")
|
|
assert response.status_code == 429
|
|
assert response.headers.get("retry-after") is not None
|
|
|
|
|
|
def test_resets_after_window():
|
|
app = _make_app(1, 0.1)
|
|
client = TestClient(app)
|
|
assert client.get("/limited").status_code == 200
|
|
assert client.get("/limited").status_code == 429
|
|
time.sleep(0.15)
|
|
assert client.get("/limited").status_code == 200
|
|
|
|
|
|
def test_returns_dependency_callable():
|
|
"""Direct unit test on the closure (no FastAPI dependency injection)."""
|
|
dep = rate_limit(2, 60.0)
|
|
assert asyncio.iscoroutinefunction(dep)
|