Add unit tests and gate PR/deploy on them (#1) (#22)
Unit Tests / test (push) Successful in 3s

## Summary

Closes #1.

- Adds real Django unit tests in `schasite/tests.py` (public pages, membership form + form validation, model helpers, calendar helpers, Stripe endpoints with mocks, auth/login_required behavior).
- Includes the dockerize + Gitea Actions CI/CD base so PRs run tests and deploy only after Unit Tests succeed on a `master` push (company_site pattern).

## Test plan

- [x] `DJANGO_ENV=dev DJANGO_SECRET_KEY=test-secret-key uv run python manage.py test schasite` — 30 tests OK
- [ ] Open this PR and confirm Unit Tests / CI workflows run on the self-hosted runner
- [ ] After merge to `master`, confirm Unit Tests run on push and deploy only proceeds when they pass

Reviewed-on: #22
This commit was merged in pull request #22.
This commit is contained in:
2026-07-14 03:48:01 -07:00
parent 5acddd37e2
commit b3fa2272c8
+410 -2
View File
@@ -1,3 +1,411 @@
from django.test import TestCase import datetime
from unittest.mock import patch
# Create your tests here. from django.test import Client, TestCase, override_settings
from django.urls import reverse
from scha.settings.base import build_csrf_trusted_origins
from .forms import AddressForm, PeopleForm
from .models import (
AddressModel1,
CalendarEvent,
Membership,
MembershipPerson,
SCHAOfficer,
UsefulLinks,
)
class CsrfTrustedOriginsTests(TestCase):
def test_derives_https_origins_from_public_hosts(self):
origins = build_csrf_trusted_origins(
["schawheaton.com", "www.schawheaton.com"]
)
self.assertEqual(
origins,
[
"https://schawheaton.com",
"https://www.schawheaton.com",
],
)
def test_derives_http_origins_for_local_hosts(self):
origins = build_csrf_trusted_origins(["localhost", "127.0.0.1"])
self.assertEqual(origins, ["http://localhost", "http://127.0.0.1"])
def test_explicit_origins_win(self):
origins = build_csrf_trusted_origins(
["schawheaton.com"],
["https://custom.example"],
)
self.assertEqual(origins, ["https://custom.example"])
class PublicPageTests(TestCase):
def setUp(self):
self.client = Client()
def test_index_renders(self):
response = self.client.get(reverse("index2"))
self.assertEqual(response.status_code, 200)
def test_about_us_renders(self):
response = self.client.get(reverse("about_us2"))
self.assertEqual(response.status_code, 200)
def test_calendar_renders(self):
response = self.client.get(reverse("calendar2"))
self.assertEqual(response.status_code, 200)
def test_newsletters_renders(self):
response = self.client.get(reverse("newsletters2"))
self.assertEqual(response.status_code, 200)
def test_dues_renders(self):
response = self.client.get(reverse("dues2"))
self.assertEqual(response.status_code, 200)
def test_scha_board_renders_officers(self):
SCHAOfficer.objects.create(
name="Jane President",
position="President",
email="jane@example.com",
)
response = self.client.get(reverse("scha_board2"))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Jane President")
def test_useful_links_renders_links(self):
UsefulLinks.objects.create(name="City of Wheaton", url="https://example.com")
response = self.client.get(reverse("useful_links2"))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "City of Wheaton")
@override_settings(
RECAPTCHA_PUBLIC_KEY="test-public-key",
RECAPTCHA_PRIVATE_KEY="test-private-key",
)
class MembershipFormViewTests(TestCase):
def setUp(self):
self.client = Client()
self.url = reverse("membership_form2")
def test_get_renders_form(self):
response = self.client.get(self.url)
self.assertEqual(response.status_code, 200)
self.assertIn("peopleForm1", response.context)
self.assertIn("addressForm", response.context)
self.assertIn("captchaForm", response.context)
@patch("schasite.views.CaptchaForm")
def test_post_success_creates_membership_and_redirects(self, mock_captcha_form):
mock_captcha_form.return_value.is_valid.return_value = True
payload = {
"streetAddress": "123 Main St",
"unit": "",
"city": "Wheaton",
"state": "IL",
"zipCode": "60189",
"firstName1": "Pat",
"lastName1": "Neighbor",
"email1": "pat@example.com",
"phone1": "6305551212",
"firstName2": "",
"lastName2": "",
"email2": "",
"phone2": "",
"captcha": "passed",
}
response = self.client.post(self.url, payload)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, reverse("index2"))
self.assertEqual(Membership.objects.count(), 1)
membership = Membership.objects.get()
self.assertEqual(membership.addressmodel1.address_1, "123 Main St")
self.assertEqual(
MembershipPerson.objects.filter(membership=membership).count(),
1,
)
@patch("schasite.views.CaptchaForm")
def test_post_missing_address_rerenders_form(self, mock_captcha_form):
mock_captcha_form.return_value.is_valid.return_value = True
payload = {
"streetAddress": "",
"unit": "",
"city": "Wheaton",
"state": "IL",
"zipCode": "60189",
"firstName1": "Pat",
"lastName1": "Neighbor",
"email1": "pat@example.com",
"phone1": "6305551212",
"firstName2": "",
"lastName2": "",
"email2": "",
"phone2": "",
"captcha": "passed",
}
response = self.client.post(self.url, payload)
self.assertEqual(response.status_code, 200)
self.assertEqual(Membership.objects.count(), 0)
self.assertIn("addressForm", response.context)
class AddressFormTests(TestCase):
def test_valid_address(self):
form = AddressForm(
data={
"address_1": "123 Main St",
"address_2": "",
"city": "Wheaton",
"state": "IL",
"zip_code": "60189",
}
)
self.assertTrue(form.is_valid())
def test_missing_street_is_invalid(self):
form = AddressForm(
data={
"address_1": "",
"city": "Wheaton",
"state": "IL",
"zip_code": "60189",
}
)
self.assertFalse(form.is_valid())
self.assertIn("address_1", form.errors)
class PeopleFormTests(TestCase):
def test_valid_person(self):
form = PeopleForm(
data={
"first_name": "Pat",
"last_name": "Neighbor",
"email": "pat@example.com",
"phone_number": "+16305551212",
}
)
self.assertTrue(form.is_valid())
def test_missing_phone_is_invalid(self):
form = PeopleForm(
data={
"first_name": "Pat",
"last_name": "Neighbor",
"email": "pat@example.com",
"phone_number": "",
}
)
self.assertFalse(form.is_valid())
class MembershipModelTests(TestCase):
def test_get_address_str_and_person_1(self):
membership = Membership.objects.create()
AddressModel1.objects.create(
membership=membership,
address_1="456 Oak Ave",
city="Wheaton",
state="IL",
zip_code="60189",
)
MembershipPerson.objects.create(
membership=membership,
first_name="Alex",
last_name="Resident",
email="alex@example.com",
phone_number="+16305559876",
)
self.assertEqual(membership.get_address_str(), "456 Oak Ave")
self.assertEqual(membership.get_person_1(), "alex@example.com")
self.assertIn("456 Oak Ave", str(membership))
def test_get_person_1_with_no_people(self):
membership = Membership.objects.create()
AddressModel1.objects.create(
membership=membership,
address_1="789 Pine St",
city="Wheaton",
state="IL",
zip_code="60189",
)
self.assertEqual(membership.get_person_1(), "No Person")
class CalendarEventTests(TestCase):
def test_has_date_and_future_past_helpers(self):
today = datetime.date.today()
past = CalendarEvent.objects.create(
event_name="Past Picnic",
start_date=today - datetime.timedelta(days=7),
)
future = CalendarEvent.objects.create(
event_name="Future Picnic",
start_date=today + datetime.timedelta(days=7),
)
undated = CalendarEvent.objects.create(event_name="TBD Event")
self.assertTrue(past.has_date())
self.assertTrue(past.past_event())
self.assertFalse(past.future_event())
self.assertTrue(future.future_event())
self.assertFalse(future.past_event())
self.assertTrue(undated.no_date())
self.assertFalse(undated.has_date())
self.assertFalse(undated.past_event())
self.assertFalse(undated.future_event())
def test_calendar_page_splits_past_and_future(self):
today = datetime.date.today()
CalendarEvent.objects.create(
event_name="Past Event",
start_date=today - datetime.timedelta(days=3),
)
CalendarEvent.objects.create(
event_name="Future Event",
start_date=today + datetime.timedelta(days=3),
)
response = self.client.get(reverse("calendar2"))
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.context["past_events"]), 1)
self.assertEqual(len(response.context["future_events"]), 1)
self.assertEqual(response.context["past_events"][0].event_name, "Past Event")
self.assertEqual(
response.context["future_events"][0].event_name, "Future Event"
)
@override_settings(
STRIPE_PUBLISHABLE_KEY="pk_test_123",
STRIPE_SECRET_KEY="sk_test_123",
)
class StripeEndpointTests(TestCase):
def setUp(self):
self.client = Client()
def test_stripe_config_returns_publishable_key(self):
response = self.client.get("/config/")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), {"publicKey": "pk_test_123"})
@patch("schasite.views.stripe.checkout.Session.create")
def test_create_checkout_session_returns_session_id(self, mock_create):
mock_create.return_value = {"id": "cs_test_abc"}
response = self.client.get("/create-checkout-session/")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), {"sessionId": "cs_test_abc"})
mock_create.assert_called_once()
@patch("schasite.views.stripe.checkout.Session.create")
def test_create_checkout_session_returns_error_payload(self, mock_create):
mock_create.side_effect = Exception("stripe down")
response = self.client.get("/create-checkout-session/")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), {"error": "stripe down"})
class AuthViewTests(TestCase):
"""Auth member routes are gated on settings.DEBUG at URLconf import time.
Django forces DEBUG=False during tests, so those named URLs are not
registered. Call views via RequestFactory instead of reverse()/Client.
"""
def setUp(self):
from django.contrib.auth.models import AnonymousUser
from django.test import RequestFactory
from schasite import views
self.factory = RequestFactory()
self.views = views
self.AnonymousUser = AnonymousUser
@patch("schasite.views.render")
def test_login_get_uses_signin_template(self, mock_render):
from django.http import HttpResponse
mock_render.return_value = HttpResponse(status=200)
request = self.factory.get("/login/")
response = self.views.login(request)
self.assertEqual(response.status_code, 200)
mock_render.assert_called_once_with(request, "schasite/signin.html", {})
@patch("schasite.views.render")
def test_signup_get_uses_signup_template(self, mock_render):
from django.http import HttpResponse
mock_render.return_value = HttpResponse(status=200)
request = self.factory.get("/signup/")
response = self.views.signup(request)
self.assertEqual(response.status_code, 200)
mock_render.assert_called_once_with(request, "schasite/signup.html", {})
@patch("schasite.views.render")
def test_password_reset_get_uses_template(self, mock_render):
from django.http import HttpResponse
mock_render.return_value = HttpResponse(status=200)
request = self.factory.get("/password_reset/")
response = self.views.password_reset(request)
self.assertEqual(response.status_code, 200)
mock_render.assert_called_once_with(
request, "schasite/password_reset.html", {}
)
def test_member_directory_redirects_anonymous(self):
request = self.factory.get("/directory/")
request.user = self.AnonymousUser()
response = self.views.member_directory(request)
self.assertEqual(response.status_code, 302)
self.assertIn("/login", response.url)
def test_member_dashboard_redirects_anonymous(self):
request = self.factory.get("/dashboard/")
request.user = self.AnonymousUser()
response = self.views.member_dashboard(request)
self.assertEqual(response.status_code, 302)
self.assertIn("/login", response.url)
def test_member_posts_redirects_anonymous(self):
request = self.factory.get("/posts/")
request.user = self.AnonymousUser()
response = self.views.member_posts(request)
self.assertEqual(response.status_code, 302)
self.assertIn("/login", response.url)