diff --git a/Dockerfile b/Dockerfile index 77f6325..77125ff 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,7 +26,8 @@ COPY --chown=app:app djangoapi_guard/ /app/djangoapi_guard/ COPY --chown=app:app tests/ /app/tests/ COPY --chown=app:app examples/ /app/examples/ -RUN mkdir -p /app/data/ipinfo +RUN uv pip install --python .venv/bin/python --no-deps -e /app \ + && mkdir -p /app/data/ipinfo # Probes the app service on port 8000. Any HTTP answer counts as alive; a refused connection or a timeout fails. HEALTHCHECK --interval=10s --timeout=5s --start-period=20s --retries=3 \ diff --git a/compose.yml b/compose.yml index 9cf0f4e..8c9c92b 100644 --- a/compose.yml +++ b/compose.yml @@ -5,14 +5,14 @@ services: args: PYTHON_VERSION: ${PYTHON_VERSION:-3.10} image: djapi-guard-example-py${PYTHON_VERSION:-3.10} - command: gunicorn examples.simple.example_project.wsgi:application --bind 0.0.0.0:8000 --reload + command: gunicorn --chdir examples/simple example_project.wsgi:application --bind 0.0.0.0:8000 --reload ports: - "8000:8000" environment: - REDIS_URL=redis://redis:6379 - REDIS_PREFIX=${REDIS_PREFIX:-"djangoapi_guard:"} - IPINFO_TOKEN=${IPINFO_TOKEN:-"test_token"} - - DJANGO_SETTINGS_MODULE=examples.simple.example_project.settings + - DJANGO_SETTINGS_MODULE=example_project.settings volumes: - ./examples:/app/examples:z - ./djangoapi_guard:/app/djangoapi_guard:z @@ -38,7 +38,6 @@ services: - ./djangoapi_guard:/app/djangoapi_guard:z - ./tests:/app/tests:z - ./pyproject.toml:/app/pyproject.toml:z - - ./uv.lock:/app/uv.lock:z - ./vulture_whitelist.py:/app/vulture_whitelist.py:z depends_on: redis: diff --git a/pyproject.toml b/pyproject.toml index ca7e6cf..0559b42 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,6 +38,7 @@ dev = [ "bandit[toml]", "deptry", "django-stubs", + "gunicorn", "guard-agent", "httpx", "mkdocs", @@ -220,6 +221,7 @@ known_first_party = ["djangoapi_guard", "guard_core"] per_rule_ignores = { "DEP002" = [ "bandit", "django-stubs", + "gunicorn", "guard-agent", "guard-core", "httpx", diff --git a/tests/conftest.py b/tests/conftest.py index 34e89f8..97cfacd 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -13,6 +13,16 @@ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.settings") +@pytest.fixture(scope="session", autouse=True) +def _engine_default_redis_url_follows_redis_url() -> None: + # A SecurityConfig built without redis_url falls back to localhost:6379, which + # is not where Redis lives inside the compose network. + SecurityConfig.model_fields["redis_url"].default = ( + os.getenv("REDIS_URL") or "redis://localhost:6379" + ) + SecurityConfig.model_rebuild(force=True) + + @pytest.fixture def request_factory() -> RequestFactory: return RequestFactory() diff --git a/tests/test_decorators/test_advanced.py b/tests/test_decorators/test_advanced.py index 904f0ee..dc64a95 100644 --- a/tests/test_decorators/test_advanced.py +++ b/tests/test_decorators/test_advanced.py @@ -149,7 +149,7 @@ def test_honeypot_form_detection(security_config: SecurityConfig) -> None: "application/x-www-form-urlencoded" if key == "content-type" else default ) mock_request.body.return_value = b"bot_trap=filled" - mock_request.read_body_prefix.return_value = b"bot_trap=filled" + mock_request.read_body_prefix = lambda _max_bytes: b"bot_trap=filled" result = validator(mock_request) assert result is not None @@ -202,7 +202,7 @@ def test_honeypot_json_detection(security_config: SecurityConfig) -> None: ) body_bytes = json.dumps({"spam_check": "filled"}).encode() mock_request.body.return_value = body_bytes - mock_request.read_body_prefix.return_value = body_bytes + mock_request.read_body_prefix = lambda _max_bytes: body_bytes result = validator(mock_request) assert result is not None diff --git a/tests/test_middleware.py b/tests/test_middleware.py index e267ee6..fa86566 100644 --- a/tests/test_middleware.py +++ b/tests/test_middleware.py @@ -1,4 +1,5 @@ import logging +import os import sys import time from collections.abc import Awaitable, Callable, Iterator @@ -25,6 +26,8 @@ from djangoapi_guard.adapters import DjangoGuardRequest, DjangoGuardResponse from djangoapi_guard.middleware import DjangoAPIGuard +REDIS_URL = os.getenv("REDIS_URL") or "redis://localhost:6379" + @pytest.fixture(autouse=True) def reset_rate_limiter() -> Iterator[None]: @@ -782,7 +785,7 @@ def test_redis_initialization(self) -> None: config = SecurityConfig( enable_redis=True, enable_agent=False, - redis_url="redis://localhost:6379", + redis_url=REDIS_URL, block_cloud_providers={"AWS"}, ) with ( @@ -815,7 +818,7 @@ def test_redis_initialization_without_cloud(self) -> None: config = SecurityConfig( enable_redis=True, enable_agent=False, - redis_url="redis://localhost:6379", + redis_url=REDIS_URL, ) with ( patch( @@ -1144,7 +1147,7 @@ def test_lua_script_execution(self) -> None: config = SecurityConfig( enable_redis=True, enable_agent=False, - redis_url="redis://localhost:6379", + redis_url=REDIS_URL, rate_limit=2, rate_limit_window=1, enable_rate_limiting=True, @@ -1196,7 +1199,7 @@ def test_fallback_to_pipeline(self) -> None: config = SecurityConfig( enable_redis=True, enable_agent=False, - redis_url="redis://localhost:6379", + redis_url=REDIS_URL, rate_limit=2, rate_limit_window=1, enable_rate_limiting=True, @@ -1278,7 +1281,7 @@ def test_rate_limiter_redis_errors(self) -> None: config = SecurityConfig( enable_redis=True, enable_agent=False, - redis_url="redis://localhost:6379", + redis_url=REDIS_URL, rate_limit=2, rate_limit_window=1, enable_rate_limiting=True, @@ -1362,7 +1365,7 @@ def test_rate_limiter_init_redis_exception(self) -> None: config = SecurityConfig( enable_redis=True, enable_agent=False, - redis_url="redis://localhost:6379", + redis_url=REDIS_URL, ) handler = rate_limit_handler(config) @@ -1390,7 +1393,7 @@ def test_rate_limit_reset_with_redis_errors(self) -> None: config = SecurityConfig( enable_redis=True, enable_agent=False, - redis_url="redis://localhost:6379", + redis_url=REDIS_URL, rate_limit=2, enable_rate_limiting=True, )