Add test suite, split requirements, Sentry, and .dockerignore

- Split requirements.txt into base/dev/production
- Update Dockerfiles to use requirements/production.txt
- Create .dockerignore to reduce build context
- Add conftest.py with user fixtures
- Add tests: accounts models (9), blog views (4), contact views (1)
- Add sentry-sdk[django] with conditional init via SENTRY_DSN
This commit is contained in:
Richard Nixon 2026-04-08 02:45:02 +02:00
parent 4c8cdba3d2
commit 97e59cd235
13 changed files with 204 additions and 2 deletions

16
.dockerignore Normal file
View file

@ -0,0 +1,16 @@
.git
.gitignore
.github
node_modules
__pycache__
*.pyc
*.pyo
media/
*.log
.env
.env.*
htmlcov/
.pytest_cache/
.mypy_cache/
.ruff_cache/
frontend/node_modules/

View file

View file

@ -0,0 +1,51 @@
import pytest
from django.contrib.auth import get_user_model
User = get_user_model()
@pytest.mark.django_db
class TestUserModel:
def test_create_user_with_email(self):
user = User.objects.create_user(email="user@example.com", password="pass1234")
assert user.email == "user@example.com"
assert user.check_password("pass1234")
assert not user.is_staff
assert not user.is_superuser
assert user.is_active
def test_create_user_without_email_raises(self):
with pytest.raises(ValueError, match="Email is required"):
User.objects.create_user(email="", password="pass1234")
def test_create_superuser(self):
user = User.objects.create_superuser(email="admin@example.com", password="admin1234")
assert user.is_staff
assert user.is_superuser
assert user.is_active
def test_create_superuser_not_staff_raises(self):
with pytest.raises(ValueError, match="is_staff=True"):
User.objects.create_superuser(email="a@b.com", password="x", is_staff=False)
def test_create_superuser_not_superuser_raises(self):
with pytest.raises(ValueError, match="is_superuser=True"):
User.objects.create_superuser(email="a@b.com", password="x", is_superuser=False)
def test_email_is_normalized(self):
user = User.objects.create_user(email="User@EXAMPLE.COM", password="pass1234")
assert user.email == "User@example.com"
def test_str_returns_email(self):
user = User.objects.create_user(email="user@test.com", password="pass1234")
assert str(user) == "user@test.com"
def test_username_field_is_email(self):
assert User.USERNAME_FIELD == "email"
def test_is_owner_property(self):
user = User.objects.create_user(email="owner@test.com", password="pass1234", role="owner")
assert user.is_owner
regular = User.objects.create_user(email="regular@test.com", password="pass1234")
assert not regular.is_owner

View file

View file

@ -0,0 +1,34 @@
import pytest
from django.test import Client
from django.urls import reverse
@pytest.mark.django_db
class TestBlogViews:
def test_home_returns_200(self):
client = Client()
response = client.get(reverse("blog:home"))
assert response.status_code == 200
def test_post_list_returns_200(self):
client = Client()
response = client.get(reverse("blog:post_list"))
assert response.status_code == 200
def test_rss_feed_returns_200(self):
client = Client()
response = client.get(reverse("blog:feed"))
assert response.status_code == 200
assert "application/rss+xml" in response["Content-Type"]
@pytest.mark.django_db
class TestHealthEndpoint:
def test_health_returns_200(self):
client = Client()
response = client.get("/health/")
assert response.status_code == 200
data = response.json()
assert data["status"] == "ok"
assert data["db"] is True

View file

View file

@ -0,0 +1,12 @@
import pytest
from django.test import Client
from django.urls import reverse
@pytest.mark.django_db
class TestContactViews:
def test_contact_form_get_returns_200(self):
client = Client()
response = client.get(reverse("contact:contact"))
assert response.status_code == 200

View file

@ -2,10 +2,21 @@
Production settings for richardnixon.dev platform.
"""
import os
import sentry_sdk
from .base import *
DEBUG = False
# Sentry error tracking
SENTRY_DSN = os.environ.get('SENTRY_DSN', '')
if SENTRY_DSN:
sentry_sdk.init(
dsn=SENTRY_DSN,
traces_sample_rate=0.1,
send_default_pii=False,
)
ALLOWED_HOSTS = [
'richardnixon.dev',
'www.richardnixon.dev',

21
conftest.py Normal file
View file

@ -0,0 +1,21 @@
import pytest
from django.contrib.auth import get_user_model
User = get_user_model()
@pytest.fixture
def user(db):
return User.objects.create_user(
email="test@example.com",
password="testpass123",
)
@pytest.fixture
def admin_user(db):
return User.objects.create_superuser(
email="admin@example.com",
password="adminpass123",
)

View file

@ -17,8 +17,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
&& rm -rf /var/lib/apt/lists/*
# Install Python dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY requirements/ requirements/
RUN pip install --no-cache-dir -r requirements/production.txt
# Copy project files
COPY . .

38
requirements/base.txt Normal file
View file

@ -0,0 +1,38 @@
# Django
Django>=5.0,<6.0
# Database
psycopg[binary]>=3.1.0
# Cache & Celery
redis>=5.0.0
celery>=5.3.0
django-celery-beat>=2.5.0
django-celery-results>=2.5.0
# Authentication
django-allauth[socialaccount]>=0.60.0
PyJWT>=2.12.0
cryptography>=46.0.6
django-two-factor-auth[phonenumbers]>=1.16.0
qrcode>=7.4.0
django-recaptcha>=4.0.0
# API
django-ninja>=1.0.0
django-cors-headers>=4.0.0
# Static files
whitenoise>=6.6.0
# Markdown & content
Markdown>=3.5.0
Pygments>=2.20.0
django-ckeditor-5>=0.2.10
django-modeltranslation>=0.18.0
# HTTP requests
requests>=2.33.0
# Image processing
Pillow>=10.0.0

12
requirements/dev.txt Normal file
View file

@ -0,0 +1,12 @@
-r base.txt
# Testing
pytest>=8.0
pytest-django>=4.8
pytest-cov>=5.0
factory-boy>=3.3
# Linting & type checking
ruff>=0.4.0
mypy>=1.10
django-stubs>=5.0

View file

@ -0,0 +1,7 @@
-r base.txt
# WSGI server
gunicorn>=21.2.0
# Error tracking
sentry-sdk[django]>=2.0.0