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:
parent
4c8cdba3d2
commit
97e59cd235
13 changed files with 204 additions and 2 deletions
16
.dockerignore
Normal file
16
.dockerignore
Normal 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/
|
||||
0
apps/accounts/tests/__init__.py
Normal file
0
apps/accounts/tests/__init__.py
Normal file
51
apps/accounts/tests/test_models.py
Normal file
51
apps/accounts/tests/test_models.py
Normal 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
|
||||
0
apps/blog/tests/__init__.py
Normal file
0
apps/blog/tests/__init__.py
Normal file
34
apps/blog/tests/test_views.py
Normal file
34
apps/blog/tests/test_views.py
Normal 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
|
||||
0
apps/contact/tests/__init__.py
Normal file
0
apps/contact/tests/__init__.py
Normal file
12
apps/contact/tests/test_views.py
Normal file
12
apps/contact/tests/test_views.py
Normal 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
|
||||
|
|
@ -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
21
conftest.py
Normal 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",
|
||||
)
|
||||
|
|
@ -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
38
requirements/base.txt
Normal 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
12
requirements/dev.txt
Normal 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
|
||||
7
requirements/production.txt
Normal file
7
requirements/production.txt
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
-r base.txt
|
||||
|
||||
# WSGI server
|
||||
gunicorn>=21.2.0
|
||||
|
||||
# Error tracking
|
||||
sentry-sdk[django]>=2.0.0
|
||||
Loading…
Add table
Add a link
Reference in a new issue