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
|
||||
from django.test import Client
|
||||
|
||||
|
|
@ -10,3 +14,12 @@ def enable_db_access_for_all_tests(db):
|
|||
@pytest.fixture(scope="session", name="session")
|
||||
def fixture_longlived_client() -> 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
|
||||
from django.test import Client
|
||||
from django.urls import reverse
|
||||
from hypothesis import given, strategies as st
|
||||
from logot import Logot, logged
|
||||
|
||||
from .models import Teil
|
||||
|
||||
names = st.text(alphabet=st.characters(exclude_categories=["C"]), min_size=1)
|
||||
|
||||
|
||||
@given(data=names)
|
||||
def test_submitted_data_ends_up_in_database(data, session: Client):
|
||||
|
||||
with pytest.raises(Teil.DoesNotExist):
|
||||
Teil.objects.get(name=data)
|
||||
|
||||
response = session.post(reverse("collector:enter"), data={"new_name": data})
|
||||
|
||||
assert response.status_code == 302
|
||||
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})
|
||||
assert response.status_code == 302
|
||||
|
||||
assert Teil.objects.filter(name=data).count() == 1
|
||||
|
||||
response = session.post(reverse("collector:enter"), data={"new_name": data})
|
||||
assert response.status_code == 302
|
||||
|
||||
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 django.db import transaction
|
||||
|
|
@ -6,10 +6,11 @@ from django.db.models import QuerySet
|
|||
from django.http import HttpRequest, HttpResponse, HttpResponseRedirect
|
||||
from django.urls import reverse
|
||||
from django.views import generic
|
||||
from django.views.decorators.http import require_http_methods
|
||||
|
||||
from .models import Teil
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
DEFAULT_TEILE_NUMBER = 10
|
||||
|
|
@ -44,11 +45,16 @@ class DetailView(generic.DetailView):
|
|||
return context
|
||||
|
||||
|
||||
@require_http_methods(["POST"])
|
||||
def enter(request: HttpRequest) -> HttpResponse:
|
||||
try:
|
||||
# if .create() failed, the transaction is rolled back and we are in a
|
||||
# clean (database) state to handle exceptions
|
||||
with transaction.atomic():
|
||||
Teil.objects.create(name=request.POST["new_name"])
|
||||
teil = Teil.objects.create(name=request.POST["new_name"])
|
||||
except Exception:
|
||||
logger.warning("Teil already existed")
|
||||
else:
|
||||
logger.info("New Teil entered", teil_id=teil.pk)
|
||||
|
||||
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/
|
||||
"""
|
||||
|
||||
import environ
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import environ
|
||||
import structlog
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
env = environ.Env(
|
||||
DEBUG=(bool, False),
|
||||
DEPLOYMENT=(str, "Dev"),
|
||||
DEPLOYMENT=(str, "DEV"),
|
||||
)
|
||||
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")
|
||||
DEBUG = env("DEBUG")
|
||||
DEPLOYMENT = str(env("DEPLOYMENT")).upper()
|
||||
|
||||
ALLOWED_HOSTS = []
|
||||
SESSION_COOKIE_SECURE = True
|
||||
|
|
@ -31,6 +35,7 @@ CSRF_COOKIE_SECURE = True
|
|||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
"django_structlog",
|
||||
"django.contrib.admin",
|
||||
"django.contrib.auth",
|
||||
"django.contrib.contenttypes",
|
||||
|
|
@ -39,7 +44,7 @@ INSTALLED_APPS = [
|
|||
"django.contrib.staticfiles",
|
||||
"collector.apps.CollectorConfig",
|
||||
]
|
||||
if env("DEPLOYMENT") == "Dev":
|
||||
if DEPLOYMENT == "DEV":
|
||||
INSTALLED_APPS += [
|
||||
"django_extensions",
|
||||
"debug_toolbar",
|
||||
|
|
@ -47,6 +52,7 @@ if env("DEPLOYMENT") == "Dev":
|
|||
SHELL_PLUS = "ipython"
|
||||
|
||||
MIDDLEWARE = [
|
||||
"django_structlog.middlewares.RequestMiddleware",
|
||||
"django.middleware.security.SecurityMiddleware",
|
||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||
"django.middleware.common.CommonMiddleware",
|
||||
|
|
@ -55,7 +61,7 @@ MIDDLEWARE = [
|
|||
"django.contrib.messages.middleware.MessageMiddleware",
|
||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||
]
|
||||
if env("DEPLOYMENT") == "Dev":
|
||||
if DEPLOYMENT == "DEV":
|
||||
MIDDLEWARE += [
|
||||
"debug_toolbar.middleware.DebugToolbarMiddleware",
|
||||
]
|
||||
|
|
@ -89,23 +95,6 @@ DATABASES = {
|
|||
"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
|
||||
# https://docs.djangoproject.com/en/5.1/topics/i18n/
|
||||
LANGUAGE_CODE = "en-us"
|
||||
|
|
@ -120,3 +109,50 @@ STATIC_URL = "static/"
|
|||
# Default primary key field type
|
||||
# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field
|
||||
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
|
||||
|
||||
urlpatterns = [
|
||||
path("collector/", include("collector.urls")),
|
||||
path("", include("collector.urls")),
|
||||
path("admin/", admin.site.urls),
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ authors = [
|
|||
]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
pythonpath = "."
|
||||
pythonpath = ["."]
|
||||
python_files = [
|
||||
"test_*.py",
|
||||
"tests.py",
|
||||
|
|
@ -23,6 +23,7 @@ addopts = [
|
|||
"--no-migrations",
|
||||
"--capture=sys",
|
||||
]
|
||||
logot_capturer = "logot.structlog.StructlogCapturer"
|
||||
|
||||
[build-system]
|
||||
requires = ["pdm-backend"]
|
||||
|
|
|
|||
|
|
@ -4,5 +4,6 @@ django-debug-toolbar
|
|||
django-stubs
|
||||
ipython
|
||||
pip-compile-multi
|
||||
rich
|
||||
ruff
|
||||
uv
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# SHA1:8a2089b98bf40ef6e9739b167a6c3bdc35a7c180
|
||||
# SHA1:dabc24f4b9a41d5fae3314c9a05a98434dd126e3
|
||||
#
|
||||
# This file is autogenerated by pip-compile-multi
|
||||
# To update, run:
|
||||
|
|
@ -17,7 +17,9 @@ executing==2.2.0
|
|||
ipython==9.0.2
|
||||
ipython-pygments-lexers==1.1.1
|
||||
jedi==0.19.2
|
||||
markdown-it-py==3.0.0
|
||||
matplotlib-inline==0.1.7
|
||||
mdurl==0.1.2
|
||||
parso==0.8.4
|
||||
pexpect==4.9.0
|
||||
pip==25.0.1
|
||||
|
|
@ -28,6 +30,7 @@ ptyprocess==0.7.0
|
|||
pure-eval==0.2.3
|
||||
pygments==2.19.1
|
||||
pyproject-hooks==1.2.0
|
||||
rich==13.9.4
|
||||
ruff==0.11.0
|
||||
setuptools==76.0.0
|
||||
stack-data==0.6.3
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
django
|
||||
django-environ
|
||||
django-extensions
|
||||
django-structlog[commands]
|
||||
psycopg[binary,pool]
|
||||
structlog>24,<25
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# SHA1:0534e06bf8de6836b424e914cfbef002223d6f48
|
||||
# SHA1:48c3af154036b224a6b21f175ea0a949febaf6f5
|
||||
#
|
||||
# This file is autogenerated by pip-compile-multi
|
||||
# To update, run:
|
||||
|
|
@ -9,8 +9,12 @@ asgiref==3.8.1
|
|||
django==5.1.7
|
||||
django-environ==0.12.0
|
||||
django-extensions==3.2.3
|
||||
django-ipware==7.0.1
|
||||
django-structlog==9.0.1
|
||||
psycopg==3.2.6
|
||||
psycopg-binary==3.2.6
|
||||
psycopg-pool==3.2.6
|
||||
python-ipware==3.0.0
|
||||
sqlparse==0.5.3
|
||||
structlog==24.4.0
|
||||
typing-extensions==4.12.2
|
||||
|
|
|
|||
|
|
@ -3,3 +3,4 @@
|
|||
hypothesis[django]
|
||||
pytest
|
||||
pytest-django
|
||||
logot[pytest,structlog]
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# SHA1:9bf147356ad56dbe073ef0973141bdfb94631c9b
|
||||
# SHA1:40e4b481355fc16525f8944d39904264b6e382a6
|
||||
#
|
||||
# This file is autogenerated by pip-compile-multi
|
||||
# To update, run:
|
||||
|
|
@ -7,8 +7,9 @@
|
|||
#
|
||||
-r prod.txt
|
||||
attrs==25.3.0
|
||||
hypothesis==6.129.1
|
||||
hypothesis==6.129.2
|
||||
iniconfig==2.0.0
|
||||
logot==1.3.0
|
||||
packaging==24.2
|
||||
pluggy==1.5.0
|
||||
pytest==8.3.5
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue