Compare commits
15 commits
41a8cb5836
...
f0f727fd99
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f0f727fd99 | ||
|
|
a0a2b1ea53 | ||
|
|
7c825e15cf | ||
|
|
16503bea7a | ||
|
|
246c22ce39 | ||
|
|
aa5d19c5e5 | ||
|
|
4605dedc69 | ||
|
|
051f0b964e | ||
|
|
86ccdd58ee | ||
|
|
2e31cf8047 | ||
|
|
4d96f60b46 | ||
|
|
e175071d3b | ||
|
|
2f8db3c67d | ||
|
|
f27dc5a0d3 | ||
|
|
2163a425fd |
12 changed files with 138 additions and 34 deletions
|
|
@ -1,3 +1,7 @@
|
||||||
|
import random
|
||||||
|
import string
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from django.test import Client
|
from django.test import Client
|
||||||
|
|
||||||
|
|
@ -10,3 +14,12 @@ 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
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,21 @@
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
@ -25,11 +26,46 @@ 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"))
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import logging
|
import structlog
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
|
|
@ -6,10 +6,11 @@ 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 = logging.getLogger(__name__)
|
logger = structlog.get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_TEILE_NUMBER = 10
|
DEFAULT_TEILE_NUMBER = 10
|
||||||
|
|
@ -44,11 +45,16 @@ 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.objects.create(name=request.POST["new_name"])
|
teil = 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"))
|
||||||
|
|
|
||||||
|
|
@ -8,14 +8,17 @@ 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 environ
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
import environ
|
||||||
|
import structlog
|
||||||
|
|
||||||
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]
|
||||||
|
|
||||||
|
|
@ -23,6 +26,7 @@ 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
|
||||||
|
|
@ -31,6 +35,7 @@ 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",
|
||||||
|
|
@ -39,7 +44,7 @@ INSTALLED_APPS = [
|
||||||
"django.contrib.staticfiles",
|
"django.contrib.staticfiles",
|
||||||
"collector.apps.CollectorConfig",
|
"collector.apps.CollectorConfig",
|
||||||
]
|
]
|
||||||
if env("DEPLOYMENT") == "Dev":
|
if DEPLOYMENT == "DEV":
|
||||||
INSTALLED_APPS += [
|
INSTALLED_APPS += [
|
||||||
"django_extensions",
|
"django_extensions",
|
||||||
"debug_toolbar",
|
"debug_toolbar",
|
||||||
|
|
@ -47,6 +52,7 @@ if env("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",
|
||||||
|
|
@ -55,7 +61,7 @@ MIDDLEWARE = [
|
||||||
"django.contrib.messages.middleware.MessageMiddleware",
|
"django.contrib.messages.middleware.MessageMiddleware",
|
||||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||||
]
|
]
|
||||||
if env("DEPLOYMENT") == "Dev":
|
if DEPLOYMENT == "DEV":
|
||||||
MIDDLEWARE += [
|
MIDDLEWARE += [
|
||||||
"debug_toolbar.middleware.DebugToolbarMiddleware",
|
"debug_toolbar.middleware.DebugToolbarMiddleware",
|
||||||
]
|
]
|
||||||
|
|
@ -89,23 +95,6 @@ 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"
|
||||||
|
|
@ -120,3 +109,50 @@ 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
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ from django.contrib import admin
|
||||||
from django.urls import include, path
|
from django.urls import include, path
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("collector/", include("collector.urls")),
|
path("", include("collector.urls")),
|
||||||
path("admin/", admin.site.urls),
|
path("admin/", admin.site.urls),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,6 +23,7 @@ addopts = [
|
||||||
"--no-migrations",
|
"--no-migrations",
|
||||||
"--capture=sys",
|
"--capture=sys",
|
||||||
]
|
]
|
||||||
|
logot_capturer = "logot.structlog.StructlogCapturer"
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["pdm-backend"]
|
requires = ["pdm-backend"]
|
||||||
|
|
|
||||||
|
|
@ -4,5 +4,6 @@ django-debug-toolbar
|
||||||
django-stubs
|
django-stubs
|
||||||
ipython
|
ipython
|
||||||
pip-compile-multi
|
pip-compile-multi
|
||||||
|
rich
|
||||||
ruff
|
ruff
|
||||||
uv
|
uv
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
# SHA1:8a2089b98bf40ef6e9739b167a6c3bdc35a7c180
|
# SHA1:dabc24f4b9a41d5fae3314c9a05a98434dd126e3
|
||||||
#
|
#
|
||||||
# This file is autogenerated by pip-compile-multi
|
# This file is autogenerated by pip-compile-multi
|
||||||
# To update, run:
|
# To update, run:
|
||||||
|
|
@ -17,7 +17,9 @@ 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
|
||||||
|
|
@ -28,6 +30,7 @@ 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
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
django
|
django
|
||||||
django-environ
|
django-environ
|
||||||
django-extensions
|
django-extensions
|
||||||
|
django-structlog[commands]
|
||||||
psycopg[binary,pool]
|
psycopg[binary,pool]
|
||||||
|
structlog>24,<25
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
# SHA1:0534e06bf8de6836b424e914cfbef002223d6f48
|
# SHA1:48c3af154036b224a6b21f175ea0a949febaf6f5
|
||||||
#
|
#
|
||||||
# This file is autogenerated by pip-compile-multi
|
# This file is autogenerated by pip-compile-multi
|
||||||
# To update, run:
|
# To update, run:
|
||||||
|
|
@ -9,8 +9,12 @@ 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
|
||||||
|
|
|
||||||
|
|
@ -3,3 +3,4 @@
|
||||||
hypothesis[django]
|
hypothesis[django]
|
||||||
pytest
|
pytest
|
||||||
pytest-django
|
pytest-django
|
||||||
|
logot[pytest,structlog]
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
# SHA1:9bf147356ad56dbe073ef0973141bdfb94631c9b
|
# SHA1:40e4b481355fc16525f8944d39904264b6e382a6
|
||||||
#
|
#
|
||||||
# This file is autogenerated by pip-compile-multi
|
# This file is autogenerated by pip-compile-multi
|
||||||
# To update, run:
|
# To update, run:
|
||||||
|
|
@ -7,8 +7,9 @@
|
||||||
#
|
#
|
||||||
-r prod.txt
|
-r prod.txt
|
||||||
attrs==25.3.0
|
attrs==25.3.0
|
||||||
hypothesis==6.129.1
|
hypothesis==6.129.2
|
||||||
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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue