- Environment-split settings: base/local/test/prod with django-environ - Postgres + Redis + Celery wiring (broker, beat, result backend) - All 9 domain app stubs: accounts, courses, cms, tracking, quizzes, training, certificates, reports, notifications - api app: /healthz/ endpoint, custom DRF exception handler, SecurityAuditMiddleware, permissions/throttle/upload-validation stubs - DRF global baseline: JWT+session auth, closed-by-default permissions, cursor/page pagination, drf-spectacular schema generation - Dockerfile (multi-env build arg), docker-compose.yml (local), docker-compose.test.yml (CI-friendly tmpfs Postgres) - pytest.ini with smoke + settings marker definitions - tests/test_smoke.py: startup, URL resolution, healthcheck shape - tests/test_settings_matrix.py: per-profile security assertions - .github/workflows/ci.yml: test, lint, schema CI jobs - .env.example with all required vars documented - .gitignore Co-Authored-By: Paperclip <noreply@paperclip.ing>
121 lines
4.2 KiB
Python
121 lines
4.2 KiB
Python
"""
|
|
Settings matrix tests.
|
|
|
|
Verify that each settings profile has the required keys and safe
|
|
defaults — without needing to boot each profile as a full Django app.
|
|
These run under the test settings module and import the other modules
|
|
directly for inspection.
|
|
"""
|
|
import importlib
|
|
import os
|
|
import pytest
|
|
from unittest.mock import patch
|
|
|
|
|
|
def _load_settings(module_path: str):
|
|
"""Import a settings module with minimal env vars patched in."""
|
|
env_patch = {
|
|
"DJANGO_SECRET_KEY": "test-secret-key",
|
|
"DJANGO_ALLOWED_HOSTS": "localhost",
|
|
"DATABASE_URL": "postgres://training:training@localhost:5432/training",
|
|
"REDIS_URL": "redis://localhost:6379/0",
|
|
# prod extras
|
|
"AWS_STORAGE_BUCKET_NAME": "test-bucket",
|
|
"EMAIL_HOST": "smtp.example.com",
|
|
"EMAIL_HOST_USER": "user@example.com",
|
|
"EMAIL_HOST_PASSWORD": "password",
|
|
"DEFAULT_FROM_EMAIL": "noreply@example.com",
|
|
}
|
|
with patch.dict(os.environ, env_patch, clear=False):
|
|
if module_path in [mod for mod in list(importlib._bootstrap._installed_safeguards or [])]:
|
|
importlib.reload(importlib.import_module(module_path))
|
|
return importlib.import_module(module_path)
|
|
|
|
|
|
@pytest.mark.settings
|
|
class TestBaseSettings:
|
|
def test_secret_key_required(self):
|
|
"""SECRET_KEY must be set from environment (not a default fallback)."""
|
|
mod = _load_settings("config.settings.base")
|
|
assert mod.SECRET_KEY == "test-secret-key"
|
|
|
|
def test_debug_is_false(self):
|
|
mod = _load_settings("config.settings.base")
|
|
assert mod.DEBUG is False
|
|
|
|
def test_rest_framework_has_exception_handler(self):
|
|
mod = _load_settings("config.settings.base")
|
|
assert mod.REST_FRAMEWORK["EXCEPTION_HANDLER"] == "api.exceptions.custom_exception_handler"
|
|
|
|
def test_rest_framework_default_closed(self):
|
|
mod = _load_settings("config.settings.base")
|
|
perms = mod.REST_FRAMEWORK["DEFAULT_PERMISSION_CLASSES"]
|
|
assert "rest_framework.permissions.IsAuthenticated" in perms
|
|
|
|
def test_celery_serializer_json(self):
|
|
mod = _load_settings("config.settings.base")
|
|
assert mod.CELERY_TASK_SERIALIZER == "json"
|
|
assert mod.CELERY_RESULT_SERIALIZER == "json"
|
|
assert "json" in mod.CELERY_ACCEPT_CONTENT
|
|
|
|
def test_language_code_german(self):
|
|
mod = _load_settings("config.settings.base")
|
|
assert mod.LANGUAGE_CODE == "de-de"
|
|
|
|
def test_timezone_europe_berlin(self):
|
|
mod = _load_settings("config.settings.base")
|
|
assert mod.TIME_ZONE == "Europe/Berlin"
|
|
|
|
|
|
@pytest.mark.settings
|
|
class TestLocalSettings:
|
|
def test_debug_is_true(self):
|
|
mod = _load_settings("config.settings.local")
|
|
assert mod.DEBUG is True
|
|
|
|
def test_debug_toolbar_in_installed_apps(self):
|
|
mod = _load_settings("config.settings.local")
|
|
assert "debug_toolbar" in mod.INSTALLED_APPS
|
|
|
|
def test_email_console_backend(self):
|
|
mod = _load_settings("config.settings.local")
|
|
assert "console" in mod.EMAIL_BACKEND.lower()
|
|
|
|
|
|
@pytest.mark.settings
|
|
class TestTestSettings:
|
|
def test_celery_always_eager(self):
|
|
mod = _load_settings("config.settings.test")
|
|
assert getattr(mod, "CELERY_TASK_ALWAYS_EAGER", False) is True
|
|
|
|
def test_debug_is_false(self):
|
|
mod = _load_settings("config.settings.test")
|
|
assert mod.DEBUG is False
|
|
|
|
def test_fast_password_hasher(self):
|
|
mod = _load_settings("config.settings.test")
|
|
assert any("MD5" in h for h in mod.PASSWORD_HASHERS)
|
|
|
|
|
|
@pytest.mark.settings
|
|
class TestProdSettings:
|
|
def test_debug_is_false(self):
|
|
mod = _load_settings("config.settings.prod")
|
|
assert mod.DEBUG is False
|
|
|
|
def test_ssl_redirect_enabled(self):
|
|
mod = _load_settings("config.settings.prod")
|
|
assert mod.SECURE_SSL_REDIRECT is True
|
|
|
|
def test_hsts_configured(self):
|
|
mod = _load_settings("config.settings.prod")
|
|
assert mod.SECURE_HSTS_SECONDS >= 31536000
|
|
|
|
def test_session_cookie_secure(self):
|
|
mod = _load_settings("config.settings.prod")
|
|
assert mod.SESSION_COOKIE_SECURE is True
|
|
|
|
def test_csrf_cookie_secure(self):
|
|
mod = _load_settings("config.settings.prod")
|
|
assert mod.CSRF_COOKIE_SECURE is True
|