Files
scha/schasite/tests.py
T
westfarn ac9330f366
CI / test (pull_request) Successful in 4s
Unit Tests / test (pull_request) Successful in 3s
Add start/end time fields to CalendarEvent.
Closes #28. Replace hardcoded calendar times with admin-editable
start_time/end_time so event cards show real schedules.
2026-08-04 12:59:58 -05:00

472 lines
16 KiB
Python

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>{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)