Compare commits

..

No commits in common. "f0f727fd995fdd3970e38743d9af8e2677c866c1" and "41a8cb58365dcf382bbbd6f68483ce2fa507261c" have entirely different histories.

12 changed files with 34 additions and 138 deletions

View file

@ -1,7 +1,3 @@
import random
import string
from typing import Callable
import pytest import pytest
from django.test import Client from django.test import Client
@ -14,12 +10,3 @@ def enable_db_access_for_all_tests(db):
@pytest.fixture(scope="session", name="session") @pytest.fixture(scope="session", name="session")
def fixture_longlived_client() -> Client: def fixture_longlived_client() -> Client:
return Client() return Client()
@pytest.fixture(name="random_name", scope="session")
def fixture_random_name_generator() -> Callable[[int], str]:
def name_generator(length: int = 7) -> str:
name = "".join(random.choices(string.ascii_letters, k=length))
return name
return name_generator

View file

@ -1,21 +1,20 @@
from typing import Callable
import pytest import pytest
from django.test import Client from django.test import Client
from django.urls import reverse from django.urls import reverse
from hypothesis import given, strategies as st from hypothesis import given, strategies as st
from logot import Logot, logged
from .models import Teil from .models import Teil
names = st.text(alphabet=st.characters(exclude_categories=["C"]), min_size=1) names = st.text(alphabet=st.characters(exclude_categories=["C"]), min_size=1)
@given(data=names) @given(data=names)
def test_submitted_data_ends_up_in_database(data, session: Client): def test_submitted_data_ends_up_in_database(data, session: Client):
with pytest.raises(Teil.DoesNotExist): with pytest.raises(Teil.DoesNotExist):
Teil.objects.get(name=data) Teil.objects.get(name=data)
response = session.post(reverse("collector:enter"), data={"new_name": data}) response = session.post(reverse("collector:enter"), data={"new_name": data})
assert response.status_code == 302 assert response.status_code == 302
assert Teil.objects.get(name=data) assert Teil.objects.get(name=data)
@ -26,46 +25,11 @@ def test_entering_same_name_twice_does_not_change_database_entry(data, session:
response = session.post(reverse("collector:enter"), data={"new_name": data}) response = session.post(reverse("collector:enter"), data={"new_name": data})
assert response.status_code == 302 assert response.status_code == 302
assert Teil.objects.filter(name=data).count() == 1 assert Teil.objects.filter(name=data).count() == 1
response = session.post(reverse("collector:enter"), data={"new_name": data}) response = session.post(reverse("collector:enter"), data={"new_name": data})
assert response.status_code == 302 assert response.status_code == 302
assert Teil.objects.filter(name=data).count() == 1 assert Teil.objects.filter(name=data).count() == 1
@pytest.mark.parametrize(
"http_method,expected_status",
[
("GET", 405),
("PATCH", 405),
("POST", 302),
("PUT", 405),
],
)
def test_enter_endpoint_accepts_only_post_requests(
client: Client,
http_method: str,
expected_status: int,
random_name: Callable[[int], str],
):
client_method = getattr(client, http_method.lower())
response = client_method(
reverse("collector:enter"), data={"new_name": random_name(8)}
)
assert response.status_code == expected_status
@pytest.mark.xfail("WIP, kein Plan warum nichts gecaptured zu werden scheint")
def test_enter_endpoints_emits_expected_logs(
client: Client, logot: Logot, random_name: Callable[[int], str]
):
same_name = random_name(10)
response = client.post(reverse("collector:enter"), data={"new_name": same_name})
assert response.status_code == 302
logot.assert_logged(logged.info("New Teil entered"))
response = client.post(reverse("collector:enter"), data={"new_name": same_name})
assert response.status_code == 302
logot.assert_logged(logged.warning("Teil already existed"))

View file

@ -1,4 +1,4 @@
import structlog import logging
from typing import Any from typing import Any
from django.db import transaction from django.db import transaction
@ -6,11 +6,10 @@ from django.db.models import QuerySet
from django.http import HttpRequest, HttpResponse, HttpResponseRedirect from django.http import HttpRequest, HttpResponse, HttpResponseRedirect
from django.urls import reverse from django.urls import reverse
from django.views import generic from django.views import generic
from django.views.decorators.http import require_http_methods
from .models import Teil from .models import Teil
logger = structlog.get_logger(__name__) logger = logging.getLogger(__name__)
DEFAULT_TEILE_NUMBER = 10 DEFAULT_TEILE_NUMBER = 10
@ -45,16 +44,11 @@ class DetailView(generic.DetailView):
return context return context
@require_http_methods(["POST"])
def enter(request: HttpRequest) -> HttpResponse: def enter(request: HttpRequest) -> HttpResponse:
try: try:
# if .create() failed, the transaction is rolled back and we are in a
# clean (database) state to handle exceptions
with transaction.atomic(): with transaction.atomic():
teil = Teil.objects.create(name=request.POST["new_name"]) Teil.objects.create(name=request.POST["new_name"])
except Exception: except Exception:
logger.warning("Teil already existed") logger.warning("Teil already existed")
else:
logger.info("New Teil entered", teil_id=teil.pk)
return HttpResponseRedirect(reverse("collector:list")) return HttpResponseRedirect(reverse("collector:list"))

View file

@ -8,17 +8,14 @@ For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.1/ref/settings/ https://docs.djangoproject.com/en/5.1/ref/settings/
""" """
import os
from pathlib import Path
import environ import environ
import structlog from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent BASE_DIR = Path(__file__).resolve().parent.parent
env = environ.Env( env = environ.Env(
DEBUG=(bool, False), DEBUG=(bool, False),
DEPLOYMENT=(str, "DEV"), DEPLOYMENT=(str, "Dev"),
) )
env.read_env(env.str("ENV_PATH", BASE_DIR / ".env")) # pyright: ignore[reportArgumentType] env.read_env(env.str("ENV_PATH", BASE_DIR / ".env")) # pyright: ignore[reportArgumentType]
@ -26,7 +23,6 @@ env.read_env(env.str("ENV_PATH", BASE_DIR / ".env")) # pyright: ignore[reportAr
SECRET_KEY = env("SECRET_KEY") SECRET_KEY = env("SECRET_KEY")
DEBUG = env("DEBUG") DEBUG = env("DEBUG")
DEPLOYMENT = str(env("DEPLOYMENT")).upper()
ALLOWED_HOSTS = [] ALLOWED_HOSTS = []
SESSION_COOKIE_SECURE = True SESSION_COOKIE_SECURE = True
@ -35,7 +31,6 @@ CSRF_COOKIE_SECURE = True
# Application definition # Application definition
INSTALLED_APPS = [ INSTALLED_APPS = [
"django_structlog",
"django.contrib.admin", "django.contrib.admin",
"django.contrib.auth", "django.contrib.auth",
"django.contrib.contenttypes", "django.contrib.contenttypes",
@ -44,7 +39,7 @@ INSTALLED_APPS = [
"django.contrib.staticfiles", "django.contrib.staticfiles",
"collector.apps.CollectorConfig", "collector.apps.CollectorConfig",
] ]
if DEPLOYMENT == "DEV": if env("DEPLOYMENT") == "Dev":
INSTALLED_APPS += [ INSTALLED_APPS += [
"django_extensions", "django_extensions",
"debug_toolbar", "debug_toolbar",
@ -52,7 +47,6 @@ if DEPLOYMENT == "DEV":
SHELL_PLUS = "ipython" SHELL_PLUS = "ipython"
MIDDLEWARE = [ MIDDLEWARE = [
"django_structlog.middlewares.RequestMiddleware",
"django.middleware.security.SecurityMiddleware", "django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware", "django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware", "django.middleware.common.CommonMiddleware",
@ -61,7 +55,7 @@ MIDDLEWARE = [
"django.contrib.messages.middleware.MessageMiddleware", "django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware",
] ]
if DEPLOYMENT == "DEV": if env("DEPLOYMENT") == "Dev":
MIDDLEWARE += [ MIDDLEWARE += [
"debug_toolbar.middleware.DebugToolbarMiddleware", "debug_toolbar.middleware.DebugToolbarMiddleware",
] ]
@ -95,6 +89,23 @@ DATABASES = {
"default": env.db(), "default": env.db(),
} }
# Password validation
# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
},
{
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
},
{
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
},
{
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
},
]
# Internationalization # Internationalization
# https://docs.djangoproject.com/en/5.1/topics/i18n/ # https://docs.djangoproject.com/en/5.1/topics/i18n/
LANGUAGE_CODE = "en-us" LANGUAGE_CODE = "en-us"
@ -109,50 +120,3 @@ STATIC_URL = "static/"
# Default primary key field type # Default primary key field type
# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field # https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
LOGGING = {
"version": 1,
"disable_existing_loggers": True,
"formatters": {
"logftm_formatter": {
"()": structlog.stdlib.ProcessorFormatter,
"processor": structlog.processors.LogfmtRenderer(sort_keys=True, key_order=["level", "event"]),
},
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"formatter": "logftm_formatter",
},
},
"loggers": {
"django_structlog": {
"handlers": ["console"],
"level": "DEBUG",
},
"root": {
"handlers": ["console"],
"level": "DEBUG",
},
},
}
structlog.configure(
processors=[
structlog.contextvars.merge_contextvars,
structlog.stdlib.filter_by_level,
structlog.processors.TimeStamper(fmt="iso"),
structlog.stdlib.add_logger_name,
structlog.stdlib.add_log_level,
structlog.stdlib.PositionalArgumentsFormatter(),
structlog.processors.StackInfoRenderer(),
structlog.processors.format_exc_info,
structlog.processors.UnicodeDecoder(),
structlog.stdlib.ProcessorFormatter.wrap_for_formatter,
],
logger_factory=structlog.stdlib.LoggerFactory(),
cache_logger_on_first_use=True,
)
os.makedirs(BASE_DIR / "logs", exist_ok=True)
# DJANGO_STRUCTLOG_COMMAND_LOGGING_ENABLED = True

View file

@ -20,7 +20,7 @@ from django.contrib import admin
from django.urls import include, path from django.urls import include, path
urlpatterns = [ urlpatterns = [
path("", include("collector.urls")), path("collector/", include("collector.urls")),
path("admin/", admin.site.urls), path("admin/", admin.site.urls),
] ]

View file

@ -12,7 +12,7 @@ authors = [
] ]
[tool.pytest.ini_options] [tool.pytest.ini_options]
pythonpath = ["."] pythonpath = "."
python_files = [ python_files = [
"test_*.py", "test_*.py",
"tests.py", "tests.py",
@ -23,7 +23,6 @@ addopts = [
"--no-migrations", "--no-migrations",
"--capture=sys", "--capture=sys",
] ]
logot_capturer = "logot.structlog.StructlogCapturer"
[build-system] [build-system]
requires = ["pdm-backend"] requires = ["pdm-backend"]

View file

@ -4,6 +4,5 @@ django-debug-toolbar
django-stubs django-stubs
ipython ipython
pip-compile-multi pip-compile-multi
rich
ruff ruff
uv uv

View file

@ -1,4 +1,4 @@
# SHA1:dabc24f4b9a41d5fae3314c9a05a98434dd126e3 # SHA1:8a2089b98bf40ef6e9739b167a6c3bdc35a7c180
# #
# This file is autogenerated by pip-compile-multi # This file is autogenerated by pip-compile-multi
# To update, run: # To update, run:
@ -17,9 +17,7 @@ executing==2.2.0
ipython==9.0.2 ipython==9.0.2
ipython-pygments-lexers==1.1.1 ipython-pygments-lexers==1.1.1
jedi==0.19.2 jedi==0.19.2
markdown-it-py==3.0.0
matplotlib-inline==0.1.7 matplotlib-inline==0.1.7
mdurl==0.1.2
parso==0.8.4 parso==0.8.4
pexpect==4.9.0 pexpect==4.9.0
pip==25.0.1 pip==25.0.1
@ -30,7 +28,6 @@ ptyprocess==0.7.0
pure-eval==0.2.3 pure-eval==0.2.3
pygments==2.19.1 pygments==2.19.1
pyproject-hooks==1.2.0 pyproject-hooks==1.2.0
rich==13.9.4
ruff==0.11.0 ruff==0.11.0
setuptools==76.0.0 setuptools==76.0.0
stack-data==0.6.3 stack-data==0.6.3

View file

@ -1,6 +1,4 @@
django django
django-environ django-environ
django-extensions django-extensions
django-structlog[commands]
psycopg[binary,pool] psycopg[binary,pool]
structlog>24,<25

View file

@ -1,4 +1,4 @@
# SHA1:48c3af154036b224a6b21f175ea0a949febaf6f5 # SHA1:0534e06bf8de6836b424e914cfbef002223d6f48
# #
# This file is autogenerated by pip-compile-multi # This file is autogenerated by pip-compile-multi
# To update, run: # To update, run:
@ -9,12 +9,8 @@ asgiref==3.8.1
django==5.1.7 django==5.1.7
django-environ==0.12.0 django-environ==0.12.0
django-extensions==3.2.3 django-extensions==3.2.3
django-ipware==7.0.1
django-structlog==9.0.1
psycopg==3.2.6 psycopg==3.2.6
psycopg-binary==3.2.6 psycopg-binary==3.2.6
psycopg-pool==3.2.6 psycopg-pool==3.2.6
python-ipware==3.0.0
sqlparse==0.5.3 sqlparse==0.5.3
structlog==24.4.0
typing-extensions==4.12.2 typing-extensions==4.12.2

View file

@ -3,4 +3,3 @@
hypothesis[django] hypothesis[django]
pytest pytest
pytest-django pytest-django
logot[pytest,structlog]

View file

@ -1,4 +1,4 @@
# SHA1:40e4b481355fc16525f8944d39904264b6e382a6 # SHA1:9bf147356ad56dbe073ef0973141bdfb94631c9b
# #
# This file is autogenerated by pip-compile-multi # This file is autogenerated by pip-compile-multi
# To update, run: # To update, run:
@ -7,9 +7,8 @@
# #
-r prod.txt -r prod.txt
attrs==25.3.0 attrs==25.3.0
hypothesis==6.129.2 hypothesis==6.129.1
iniconfig==2.0.0 iniconfig==2.0.0
logot==1.3.0
packaging==24.2 packaging==24.2
pluggy==1.5.0 pluggy==1.5.0
pytest==8.3.5 pytest==8.3.5