From b3fa2272c8279aaa33352eac0f2fa65d7bae6426 Mon Sep 17 00:00:00 2001 From: Ryan Westfall Date: Tue, 14 Jul 2026 03:48:01 -0700 Subject: [PATCH] Add unit tests and gate PR/deploy on them (#1) (#22) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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: https://git.aimloperations.com/ai_ml_operations/scha/pulls/22 --- schasite/tests.py | 412 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 410 insertions(+), 2 deletions(-) diff --git a/schasite/tests.py b/schasite/tests.py index 7ce503c..a8b0bed 100644 --- a/schasite/tests.py +++ b/schasite/tests.py @@ -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)