511 lines
21 KiB
Python
511 lines
21 KiB
Python
from unittest.mock import patch
|
|
|
|
from django.contrib.auth.models import User
|
|
from django.test import Client, TestCase, override_settings
|
|
from django.urls import reverse
|
|
|
|
from company_site.settings.base import build_csrf_trusted_origins
|
|
|
|
from .models import Contact, EmailMessage, PageVisit
|
|
from .seo import SERVICE_URL_NAMES, get_service_entries
|
|
from .traffic import TrafficType, classify_user_agent
|
|
|
|
|
|
class CsrfTrustedOriginsTests(TestCase):
|
|
def test_derives_https_origins_from_public_hosts(self):
|
|
origins = build_csrf_trusted_origins(
|
|
["aimloperations.com", "www.aimloperations.com"]
|
|
)
|
|
|
|
self.assertEqual(
|
|
origins,
|
|
[
|
|
"https://aimloperations.com",
|
|
"https://www.aimloperations.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(
|
|
["aimloperations.com"],
|
|
["https://custom.example"],
|
|
)
|
|
|
|
self.assertEqual(origins, ["https://custom.example"])
|
|
|
|
|
|
class LogoutCsrfTests(TestCase):
|
|
def setUp(self):
|
|
self.client = Client(enforce_csrf_checks=True)
|
|
self.user = User.objects.create_user(username="logout_user", password="pass")
|
|
|
|
def test_logout_post_with_csrf_succeeds(self):
|
|
self.client.login(username="logout_user", password="pass")
|
|
self.client.get("/")
|
|
csrf = self.client.cookies["csrftoken"].value
|
|
|
|
response = self.client.post(
|
|
reverse("logout"),
|
|
{"csrfmiddlewaretoken": csrf},
|
|
)
|
|
|
|
self.assertEqual(response.status_code, 302)
|
|
self.assertEqual(response.url, "/")
|
|
self.assertNotIn("_auth_user_id", self.client.session)
|
|
|
|
|
|
class PreviewEmailAuthTests(TestCase):
|
|
def setUp(self):
|
|
self.client = Client()
|
|
self.user = User.objects.create_user(username="previewer", password="pass")
|
|
self.email = EmailMessage.objects.create(
|
|
subject="Preview subject",
|
|
body="Preview body content",
|
|
recipient="recipient@example.com",
|
|
)
|
|
self.url = reverse("preview_email", kwargs={"pk": self.email.pk})
|
|
|
|
def test_unauthenticated_user_is_redirected_to_login(self):
|
|
response = self.client.get(self.url)
|
|
|
|
self.assertEqual(response.status_code, 302)
|
|
self.assertIn("/accounts/login/", response.url)
|
|
|
|
def test_authenticated_user_can_preview_email(self):
|
|
self.client.login(username="previewer", password="pass")
|
|
|
|
response = self.client.get(self.url)
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertContains(response, "Preview subject")
|
|
self.assertContains(response, "Preview body content")
|
|
|
|
|
|
@override_settings(
|
|
DEBUG=True,
|
|
EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend",
|
|
)
|
|
class ContactViewTests(TestCase):
|
|
def setUp(self):
|
|
self.client = Client()
|
|
self.url = reverse("contact")
|
|
self.valid_payload = {
|
|
"name": "Jane Doe",
|
|
"email": "jane@example.com",
|
|
"subject": "Automation inquiry",
|
|
"message": "We need help automating our workflow.",
|
|
}
|
|
|
|
def test_contact_get_renders_form(self):
|
|
response = self.client.get(self.url)
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertContains(response, "Send Us a Message")
|
|
self.assertContains(response, 'id="contact-name"')
|
|
self.assertContains(response, 'for="contact-name"')
|
|
self.assertContains(response, 'id="contact-email"')
|
|
self.assertContains(response, 'for="contact-email"')
|
|
self.assertContains(response, 'id="contact-subject"')
|
|
self.assertContains(response, 'for="contact-subject"')
|
|
self.assertContains(response, 'id="contact-message"')
|
|
self.assertContains(response, 'for="contact-message"')
|
|
|
|
@patch("public.views.send_contact_email")
|
|
def test_contact_post_success_saves_contact_and_sends_email(self, mock_send_email):
|
|
response = self.client.post(self.url, self.valid_payload)
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertContains(response, "We'll be in contact shortly!")
|
|
|
|
contact = Contact.objects.get()
|
|
self.assertEqual(contact.name, "Jane Doe")
|
|
self.assertEqual(contact.email, "jane@example.com")
|
|
self.assertEqual(contact.subject, "Automation inquiry")
|
|
self.assertEqual(contact.blurb, "We need help automating our workflow.")
|
|
self.assertFalse(contact.contacted)
|
|
|
|
mock_send_email.assert_called_once_with(
|
|
"jane@example.com",
|
|
"Automation inquiry",
|
|
"We need help automating our workflow.",
|
|
)
|
|
|
|
@patch("public.views.send_contact_email")
|
|
def test_contact_post_without_message_still_succeeds(self, mock_send_email):
|
|
payload = {**self.valid_payload, "message": ""}
|
|
|
|
response = self.client.post(self.url, payload)
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertContains(response, "We'll be in contact shortly!")
|
|
self.assertEqual(Contact.objects.count(), 1)
|
|
self.assertEqual(Contact.objects.get().blurb, "")
|
|
mock_send_email.assert_called_once()
|
|
|
|
def test_contact_post_missing_name_shows_error(self):
|
|
payload = {**self.valid_payload, "name": ""}
|
|
|
|
response = self.client.post(self.url, payload)
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertContains(response, "Name is required")
|
|
self.assertEqual(Contact.objects.count(), 0)
|
|
|
|
def test_contact_post_missing_email_shows_error(self):
|
|
payload = {**self.valid_payload, "email": ""}
|
|
|
|
response = self.client.post(self.url, payload)
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertContains(response, "Email is required")
|
|
self.assertEqual(Contact.objects.count(), 0)
|
|
|
|
def test_contact_post_missing_name_and_email_shows_error(self):
|
|
payload = {**self.valid_payload, "name": "", "email": ""}
|
|
|
|
response = self.client.post(self.url, payload)
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertContains(response, "Both name and email are required")
|
|
self.assertEqual(Contact.objects.count(), 0)
|
|
|
|
def test_contact_post_missing_subject_does_not_save(self):
|
|
payload = {**self.valid_payload, "subject": ""}
|
|
|
|
response = self.client.post(self.url, payload)
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertNotContains(response, "We'll be in contact shortly!")
|
|
self.assertEqual(Contact.objects.count(), 0)
|
|
|
|
@override_settings(DEBUG=False)
|
|
@patch("public.views.FormWithCaptcha.is_valid", return_value=False)
|
|
def test_contact_post_invalid_captcha_shows_error(self, _mock_is_valid):
|
|
response = self.client.post(self.url, self.valid_payload)
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertContains(response, "There was an error submitting. Try again")
|
|
self.assertEqual(Contact.objects.count(), 0)
|
|
|
|
|
|
@override_settings(DEBUG=True, TIANJI_ENABLED=True)
|
|
class TianjiTrackingTests(TestCase):
|
|
def test_homepage_includes_analytics_notice_when_enabled(self):
|
|
response = self.client.get(reverse("public_index"))
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertContains(response, "cookie-consent-banner")
|
|
self.assertContains(response, "cookie-consent-acknowledge")
|
|
self.assertContains(response, "Acknowledge")
|
|
self.assertNotContains(response, "cookie-consent-decline")
|
|
self.assertNotContains(response, "Accept Analytics")
|
|
self.assertContains(response, "tianji-config")
|
|
self.assertContains(response, "tianji-consent.js")
|
|
|
|
@override_settings(TIANJI_ENABLED=False)
|
|
def test_homepage_omits_analytics_notice_when_disabled(self):
|
|
response = self.client.get(reverse("public_index"))
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertNotContains(response, "cookie-consent-banner")
|
|
self.assertNotContains(response, "tianji-config")
|
|
|
|
|
|
class AgenticBrowsingSeoTests(TestCase):
|
|
def test_robots_txt_is_available(self):
|
|
response = self.client.get(reverse("robots_txt"))
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertEqual(response["Content-Type"], "text/plain; charset=utf-8")
|
|
self.assertContains(response, "User-agent: *")
|
|
self.assertContains(response, "Sitemap:")
|
|
self.assertContains(response, "llms.txt")
|
|
|
|
def test_sitemap_xml_lists_public_pages(self):
|
|
response = self.client.get(reverse("sitemap_xml"))
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertEqual(response["Content-Type"], "application/xml; charset=utf-8")
|
|
self.assertContains(response, "<urlset")
|
|
self.assertContains(response, reverse("contact"))
|
|
self.assertContains(response, reverse("forward_deployed"))
|
|
|
|
def test_llms_txt_is_available(self):
|
|
response = self.client.get(reverse("llms_txt"))
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertEqual(response["Content-Type"], "text/plain; charset=utf-8")
|
|
self.assertContains(response, "# AI ML Operations, LLC")
|
|
self.assertContains(response, "## Key pages")
|
|
self.assertContains(response, reverse("contact"))
|
|
self.assertContains(response, "## Web design & hosting")
|
|
self.assertContains(response, "estimate_web_design_cost")
|
|
self.assertContains(response, "email_sms")
|
|
self.assertContains(response, "three instances")
|
|
self.assertContains(response, "Tailored to your brand")
|
|
self.assertContains(response, "You own the site")
|
|
self.assertContains(response, "Grafana metrics")
|
|
self.assertContains(response, "brand-tailored")
|
|
|
|
def test_homepage_uses_semantic_nav_controls(self):
|
|
response = self.client.get(reverse("public_index"))
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertContains(response, 'id="services-menu-button"')
|
|
self.assertContains(response, 'aria-haspopup="true"')
|
|
self.assertContains(response, 'aria-label="Main navigation"')
|
|
|
|
|
|
@override_settings(DEBUG=True, WEBMCP_ENABLED=True)
|
|
class WebMcpTests(TestCase):
|
|
def test_homepage_includes_webmcp_when_enabled(self):
|
|
response = self.client.get(reverse("public_index"))
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertContains(response, "webmcp-config")
|
|
self.assertContains(response, "webmcp-tools.js")
|
|
self.assertContains(response, 'data-page-name="public_index"')
|
|
self.assertContains(response, "forward_deployed")
|
|
|
|
@override_settings(WEBMCP_ENABLED=False)
|
|
def test_homepage_omits_webmcp_when_disabled(self):
|
|
response = self.client.get(reverse("public_index"))
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertNotContains(response, "webmcp-config")
|
|
self.assertNotContains(response, "webmcp-tools.js")
|
|
|
|
def test_contact_page_includes_webmcp_form_annotations(self):
|
|
response = self.client.get(reverse("contact"))
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertContains(response, 'id="contact-form"')
|
|
self.assertContains(response, 'toolname="submit_contact_inquiry"')
|
|
self.assertContains(response, "tooldescription=")
|
|
self.assertContains(response, "toolparamdescription=")
|
|
self.assertContains(response, "submit_contact_inquiry")
|
|
|
|
@override_settings(DEBUG=False)
|
|
def test_contact_page_renders_captcha_outside_annotated_form(self):
|
|
response = self.client.get(reverse("contact"))
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
form_end = response.content.index(b"</form>")
|
|
captcha_index = response.content.index(b"g-recaptcha")
|
|
self.assertGreater(captcha_index, form_end)
|
|
self.assertContains(response, b'form="contact-form"')
|
|
|
|
def test_webmcp_services_json_includes_all_service_pages(self):
|
|
response = self.client.get(reverse("public_index"))
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
for slug in SERVICE_URL_NAMES:
|
|
self.assertContains(response, slug)
|
|
|
|
def test_service_entries_match_public_seo_config(self):
|
|
entries = get_service_entries()
|
|
self.assertEqual(len(entries), len(SERVICE_URL_NAMES))
|
|
self.assertEqual({entry["slug"] for entry in entries}, SERVICE_URL_NAMES)
|
|
|
|
def test_webmcp_static_script_defines_expected_tools(self):
|
|
from pathlib import Path
|
|
|
|
script_path = Path(__file__).resolve().parent / "static/public/js/webmcp-tools.js"
|
|
script = script_path.read_text(encoding="utf-8")
|
|
|
|
for tool_name in (
|
|
"list_services",
|
|
"get_page_content",
|
|
"navigate_to_service",
|
|
"open_contact_with_subject",
|
|
"estimate_web_design_cost",
|
|
"submit_contact_inquiry",
|
|
):
|
|
self.assertIn("name: '" + tool_name + "'", script)
|
|
|
|
self.assertIn("readOnlyHint: true", script)
|
|
self.assertIn("readOnlyHint: false", script)
|
|
self.assertIn("navigator.modelContext || document.modelContext", script)
|
|
self.assertIn("webDesignPricing", script)
|
|
|
|
def test_webmcp_config_includes_web_design_pricing(self):
|
|
response = self.client.get(reverse("public_index"))
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertContains(response, "data-web-design-pricing=")
|
|
self.assertContains(response, "email_sms")
|
|
self.assertContains(response, "client_portal")
|
|
|
|
|
|
class WebDesignPricingTests(TestCase):
|
|
def test_web_design_page_renders_estimator(self):
|
|
response = self.client.get(reverse("web_design"))
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertContains(response, "Build Your Package")
|
|
self.assertContains(response, "pricingEstimator")
|
|
self.assertContains(response, "Public site")
|
|
self.assertContains(response, "Client portal + UTM")
|
|
self.assertContains(response, "three instances")
|
|
self.assertContains(response, "UTM tracking")
|
|
self.assertContains(response, "Grafana metrics & alerts")
|
|
self.assertContains(response, "You own the site")
|
|
self.assertContains(response, "Tailored to your brand")
|
|
self.assertContains(response, reverse("contact"))
|
|
self.assertContains(response, 'data-tianji-event="web_design_estimate_submit"')
|
|
self.assertContains(response, 'data-tianji-event="web_design_contact_cta"')
|
|
self.assertContains(response, "web_design_estimate_toggle")
|
|
self.assertContains(response, "aimlTrackWhenReady")
|
|
self.assertContains(response, "data-tianji-event-build=")
|
|
self.assertContains(response, "data-tianji-event-features=")
|
|
|
|
def test_base_estimate_includes_required_features_only(self):
|
|
from .web_design_pricing import estimate_web_design_cost
|
|
|
|
estimate = estimate_web_design_cost()
|
|
self.assertEqual(estimate["one_time_build"], 600)
|
|
self.assertEqual(estimate["monthly"], 40)
|
|
self.assertEqual(estimate["selected_count"], 2)
|
|
|
|
def test_payments_auto_selects_email_sms(self):
|
|
from .web_design_pricing import estimate_web_design_cost
|
|
|
|
estimate = estimate_web_design_cost(["payments"])
|
|
selected_ids = {item["id"] for item in estimate["selected"]}
|
|
self.assertIn("email_sms", selected_ids)
|
|
self.assertIn("payments", selected_ids)
|
|
self.assertEqual(estimate["one_time_build"], 1600)
|
|
self.assertEqual(estimate["monthly"], 70)
|
|
|
|
def test_ai_social_auto_selects_social(self):
|
|
from .web_design_pricing import estimate_web_design_cost
|
|
|
|
estimate = estimate_web_design_cost(["ai_social"])
|
|
selected_ids = {item["id"] for item in estimate["selected"]}
|
|
self.assertIn("social", selected_ids)
|
|
self.assertIn("ai_social", selected_ids)
|
|
self.assertEqual(estimate["one_time_build"], 1400)
|
|
self.assertEqual(estimate["monthly"], 80)
|
|
|
|
|
|
class TrafficClassificationTests(TestCase):
|
|
def test_classifies_common_agents(self):
|
|
cases = [
|
|
("Mozilla/5.0 (Macintosh) Chrome/120.0.0.0 Safari/537.36", TrafficType.HUMAN),
|
|
("Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)", TrafficType.SEARCH_INDEXER),
|
|
("Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; GPTBot/1.0)", TrafficType.AI_BOT),
|
|
("ClaudeBot/1.0", TrafficType.AI_BOT),
|
|
("facebookexternalhit/1.1", TrafficType.SOCIAL_BOT),
|
|
("UptimeRobot/2.0", TrafficType.MONITORING),
|
|
("python-requests/2.31.0", TrafficType.OTHER_BOT),
|
|
("", TrafficType.UNKNOWN),
|
|
]
|
|
for ua, expected in cases:
|
|
with self.subTest(ua=ua):
|
|
self.assertEqual(classify_user_agent(ua), expected)
|
|
|
|
|
|
class UTMTrackingTests(TestCase):
|
|
def setUp(self):
|
|
self.client = Client(
|
|
HTTP_USER_AGENT="Mozilla/5.0 (Macintosh) Chrome/120.0.0.0 Safari/537.36"
|
|
)
|
|
self.staff = User.objects.create_user(
|
|
username="staff_utm", password="pass", is_staff=True
|
|
)
|
|
self.regular = User.objects.create_user(username="plain_utm", password="pass")
|
|
|
|
def test_page_visit_records_utm_and_human_type(self):
|
|
response = self.client.get(
|
|
"/?utm_source=linkedin&utm_medium=social&utm_campaign=spring"
|
|
)
|
|
self.assertEqual(response.status_code, 200)
|
|
visit = PageVisit.objects.latest("created")
|
|
self.assertEqual(visit.path, "/")
|
|
self.assertEqual(visit.utm_source, "linkedin")
|
|
self.assertEqual(visit.utm_medium, "social")
|
|
self.assertEqual(visit.utm_campaign, "spring")
|
|
self.assertTrue(visit.is_landing)
|
|
self.assertEqual(visit.traffic_type, TrafficType.HUMAN)
|
|
|
|
def test_utm_persists_on_next_page_via_session(self):
|
|
self.client.get("/?utm_source=newsletter&utm_medium=email&utm_campaign=march")
|
|
self.client.get("/contact")
|
|
visit = PageVisit.objects.filter(path="/contact").latest("created")
|
|
self.assertEqual(visit.utm_source, "newsletter")
|
|
self.assertEqual(visit.utm_campaign, "march")
|
|
self.assertFalse(visit.is_landing)
|
|
|
|
def test_dashboard_requires_staff(self):
|
|
url = reverse("utm_dashboard")
|
|
anon = self.client.get(url)
|
|
self.assertEqual(anon.status_code, 302)
|
|
self.assertIn("login", anon.url)
|
|
|
|
self.client.login(username="plain_utm", password="pass")
|
|
denied = self.client.get(url)
|
|
self.assertEqual(denied.status_code, 302)
|
|
|
|
self.client.login(username="staff_utm", password="pass")
|
|
ok = self.client.get(url)
|
|
self.assertEqual(ok.status_code, 200)
|
|
self.assertContains(ok, "UTM & Traffic Analytics")
|
|
|
|
def test_search_bot_classified(self):
|
|
bot_client = Client(
|
|
HTTP_USER_AGENT="Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)"
|
|
)
|
|
bot_client.get("/")
|
|
visit = PageVisit.objects.latest("created")
|
|
self.assertEqual(visit.traffic_type, TrafficType.SEARCH_INDEXER)
|
|
|
|
|
|
class LeadsDashboardTests(TestCase):
|
|
def setUp(self):
|
|
self.client = Client()
|
|
self.staff = User.objects.create_user(
|
|
username="staff_leads", password="pass", is_staff=True
|
|
)
|
|
self.regular = User.objects.create_user(username="plain_leads", password="pass")
|
|
self.lead = Contact.objects.create(
|
|
name="Ada Lovelace",
|
|
email="ada@example.com",
|
|
subject="AI help",
|
|
blurb="Need forward-deployed support.",
|
|
utm_source="linkedin",
|
|
utm_medium="social",
|
|
utm_campaign="spring",
|
|
)
|
|
|
|
def test_leads_list_requires_staff(self):
|
|
url = reverse("leads_list")
|
|
self.assertEqual(self.client.get(url).status_code, 302)
|
|
|
|
self.client.login(username="plain_leads", password="pass")
|
|
self.assertEqual(self.client.get(url).status_code, 302)
|
|
|
|
self.client.login(username="staff_leads", password="pass")
|
|
response = self.client.get(url)
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertContains(response, "Ada Lovelace")
|
|
self.assertContains(response, "ada@example.com")
|
|
|
|
def test_lead_detail_and_toggle(self):
|
|
self.client.login(username="staff_leads", password="pass")
|
|
detail = reverse("lead_detail", kwargs={"pk": self.lead.pk})
|
|
response = self.client.get(detail)
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertContains(response, "Need forward-deployed support.")
|
|
self.assertContains(response, "linkedin")
|
|
|
|
toggle = reverse("lead_toggle_contacted", kwargs={"pk": self.lead.pk})
|
|
response = self.client.post(toggle, {"next": detail})
|
|
self.assertEqual(response.status_code, 302)
|
|
self.lead.refresh_from_db()
|
|
self.assertTrue(self.lead.contacted)
|