import datetime from unittest.mock import patch 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") def test_public_pages_include_seo_metadata(self): pages = [ ("index2", "Home | Stonehedge"), ("about_us2", "About Us | Stonehedge"), ("calendar2", "Calendar | Stonehedge"), ("dues2", "Pay Dues | Stonehedge"), ("membership_form2", "Join Today | Stonehedge"), ("scha_board2", "SCHA Board | Stonehedge"), ("useful_links2", "Useful Links | Stonehedge"), ] for name, title_fragment in pages: with self.subTest(page=name): response = self.client.get(reverse(name)) self.assertEqual(response.status_code, 200) self.assertContains(response, f"{title_fragment}") self.assertContains(response, 'name="description"') self.assertContains(response, 'property="og:title"') self.assertContains(response, 'rel="canonical"') self.assertContains(response, 'id="main-content"') self.assertContains(response, "Skip to main content") def test_robots_txt(self): response = self.client.get(reverse("robots_txt")) self.assertEqual(response.status_code, 200) self.assertEqual(response["Content-Type"].split(";")[0], "text/plain") self.assertContains(response, "Sitemap:") self.assertContains(response, "Disallow: /admin/") def test_sitemap_xml(self): response = self.client.get(reverse("sitemap_xml")) self.assertEqual(response.status_code, 200) self.assertIn("xml", response["Content-Type"]) self.assertContains(response, reverse("index2")) self.assertContains(response, reverse("dues2")) def test_llms_txt(self): response = self.client.get(reverse("llms_txt")) self.assertEqual(response.status_code, 200) self.assertEqual(response["Content-Type"].split(";")[0], "text/plain") self.assertContains(response, "Stonehedge Community Homeowners Association") self.assertContains(response, reverse("membership_form2").lstrip("/")) @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_time_range_display(self): event = CalendarEvent.objects.create( event_name="Timed Event", start_time=datetime.time(11, 0), end_time=datetime.time(15, 0), ) self.assertEqual(event.time_range_display(), "11:00 AM - 3:00 PM") start_only = CalendarEvent.objects.create( event_name="Start Only", start_time=datetime.time(9, 30), ) self.assertEqual(start_only.time_range_display(), "9:30 AM") no_time = CalendarEvent.objects.create(event_name="No Time") self.assertEqual(no_time.time_range_display(), "") 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)