"""Tests for QS3-D: PostHog Analytics + Revenue Polish.
Covers PostHog helpers, hooks, template content, api_usage DDL,
sitemap, and invite documentation.
"""
import os
import re
from unittest.mock import patch, MagicMock
import pytest
# ---------------------------------------------------------------------------
# PostHog helper tests
# ---------------------------------------------------------------------------
class TestPosthogHelpers:
"""Tests for posthog_enabled, posthog_track, posthog_get_flags."""
def test_posthog_enabled_false_without_key(self):
"""posthog_enabled returns False when POSTHOG_API_KEY not set."""
with patch.dict(os.environ, {}, clear=False):
os.environ.pop("POSTHOG_API_KEY", None)
# Re-import to pick up env change
import importlib
import web.helpers
importlib.reload(web.helpers)
assert web.helpers.posthog_enabled() is False
def test_posthog_enabled_true_with_key(self):
"""posthog_enabled returns True when POSTHOG_API_KEY is set."""
with patch.dict(os.environ, {"POSTHOG_API_KEY": "phc_test123"}):
import importlib
import web.helpers
importlib.reload(web.helpers)
assert web.helpers.posthog_enabled() is True
# Cleanup
os.environ.pop("POSTHOG_API_KEY", None)
importlib.reload(web.helpers)
def test_posthog_track_noop_without_key(self):
"""posthog_track is no-op without key (doesn't raise)."""
with patch.dict(os.environ, {}, clear=False):
os.environ.pop("POSTHOG_API_KEY", None)
import importlib
import web.helpers
importlib.reload(web.helpers)
# Should not raise
web.helpers.posthog_track("test_event", {"foo": "bar"})
def test_posthog_track_calls_capture_with_key(self):
"""posthog_track calls posthog.capture when key is set."""
with patch.dict(os.environ, {"POSTHOG_API_KEY": "phc_test123"}):
import importlib
import web.helpers
importlib.reload(web.helpers)
with patch("posthog.capture") as mock_capture:
web.helpers.posthog_track("page_view", {"path": "/"}, "user-1")
mock_capture.assert_called_once_with(
distinct_id="user-1",
event="page_view",
properties={"path": "/"},
)
os.environ.pop("POSTHOG_API_KEY", None)
importlib.reload(web.helpers)
def test_posthog_track_swallows_exceptions(self):
"""posthog_track never raises even if posthog import fails."""
with patch.dict(os.environ, {"POSTHOG_API_KEY": "phc_test123"}):
import importlib
import web.helpers
importlib.reload(web.helpers)
with patch("posthog.capture", side_effect=RuntimeError("boom")):
web.helpers.posthog_track("event", {}) # should not raise
os.environ.pop("POSTHOG_API_KEY", None)
importlib.reload(web.helpers)
def test_posthog_get_flags_empty_without_key(self):
"""posthog_get_flags returns {} without key."""
with patch.dict(os.environ, {}, clear=False):
os.environ.pop("POSTHOG_API_KEY", None)
import importlib
import web.helpers
importlib.reload(web.helpers)
assert web.helpers.posthog_get_flags("user-1") == {}
def test_posthog_get_flags_returns_flags_with_key(self):
"""posthog_get_flags returns flags dict when configured."""
with patch.dict(os.environ, {"POSTHOG_API_KEY": "phc_test123"}):
import importlib
import web.helpers
importlib.reload(web.helpers)
with patch("posthog.get_all_flags", return_value={"permit_prep_enabled": True}):
flags = web.helpers.posthog_get_flags("user-1")
assert flags == {"permit_prep_enabled": True}
os.environ.pop("POSTHOG_API_KEY", None)
importlib.reload(web.helpers)
# ---------------------------------------------------------------------------
# Flask app hook tests
# ---------------------------------------------------------------------------
@pytest.fixture
def app():
"""Create Flask test client."""
os.environ.pop("POSTHOG_API_KEY", None)
from web.app import app as flask_app
flask_app.config["TESTING"] = True
return flask_app
@pytest.fixture
def client(app):
return app.test_client()
class TestAfterRequestHook:
"""Tests for the PostHog after_request tracking hook."""
def test_after_request_doesnt_modify_response(self, client):
"""after_request hook doesn't modify response body or status."""
resp = client.get("/health")
assert resp.status_code == 200
def test_after_request_skips_static_health(self, client):
"""after_request hook skips /static/ and /health paths."""
with patch("web.helpers.posthog_track") as mock_track:
client.get("/health")
mock_track.assert_not_called()
def test_after_request_tracks_search_with_query(self, app):
"""after_request hook tracks search events with query parameter."""
with patch.dict(os.environ, {"POSTHOG_API_KEY": "phc_test123"}):
import importlib
import web.helpers
importlib.reload(web.helpers)
with patch("web.helpers.posthog_track") as mock_track:
with app.test_client() as c:
# Search will likely redirect or render, but the hook fires
c.get("/search?q=test+address")
# Check if posthog_track was called with search event
found_search = False
for call in mock_track.call_args_list:
if call.args and call.args[0] == "search":
found_search = True
props = call.args[1] if len(call.args) > 1 else call.kwargs.get("properties", {})
assert props.get("query") == "test address"
assert found_search, "Expected 'search' event not found"
os.environ.pop("POSTHOG_API_KEY", None)
importlib.reload(web.helpers)
class TestFeatureFlags:
"""Tests for PostHog feature flags in before_request."""
def test_flags_populated_for_auth_users(self, app):
"""g.posthog_flags populated for authenticated users."""
with app.test_request_context("/"):
from flask import g, session
app.preprocess_request()
# Without PostHog key, flags should be empty dict
assert hasattr(g, "posthog_flags")
assert g.posthog_flags == {}
def test_flags_empty_for_anonymous(self, app):
"""g.posthog_flags is empty for anonymous users."""
with app.test_request_context("/"):
from flask import g
app.preprocess_request()
assert g.posthog_flags == {}
# ---------------------------------------------------------------------------
# Template content tests
# ---------------------------------------------------------------------------
class TestTemplateContent:
"""Tests for PostHog JS and PWA meta in templates."""
def test_landing_has_posthog_script_when_key_set(self, app):
"""landing.html contains posthog script tag when key set."""
with patch.dict(os.environ, {"POSTHOG_API_KEY": "phc_testxyz"}):
import importlib
import web.helpers
importlib.reload(web.helpers)
with app.test_client() as c:
resp = c.get("/")
html = resp.data.decode()
assert "posthog.init" in html
assert "phc_testxyz" in html
os.environ.pop("POSTHOG_API_KEY", None)
importlib.reload(web.helpers)
def test_landing_no_posthog_without_key(self, client):
"""landing.html does NOT contain posthog script when key not set."""
resp = client.get("/")
html = resp.data.decode()
assert "posthog.init" not in html
def test_landing_has_manifest_link(self, client):
"""landing.html contains <link rel="manifest">."""
resp = client.get("/")
html = resp.data.decode()
assert 'rel="manifest"' in html
assert '/static/manifest.json' in html
def test_landing_has_theme_color(self, client):
"""landing.html contains theme-color meta tag."""
resp = client.get("/")
html = resp.data.decode()
assert 'name="theme-color"' in html
# Theme color uses the canonical accent token (#5eead4)
assert 'content="#5eead4"' in html or '#5eead4' in html or '#22D3EE' in html
def test_landing_has_apple_touch_icon(self, client):
"""landing.html contains apple-touch-icon link."""
resp = client.get("/")
html = resp.data.decode()
assert 'rel="apple-touch-icon"' in html
def test_manifest_json_valid(self, client):
"""GET /static/manifest.json returns valid JSON."""
import json
resp = client.get("/static/manifest.json")
assert resp.status_code == 200
data = json.loads(resp.data)
assert data["name"] == "sfpermits.ai"
assert "icons" in data
# ---------------------------------------------------------------------------
# Index.html template tests (requires auth mock)
# ---------------------------------------------------------------------------
class TestIndexTemplate:
"""Tests for index.html manifest/PWA tags."""
def test_index_has_manifest_link(self, app):
"""index.html contains <link rel="manifest">."""
with app.test_request_context("/"):
from flask import g
# Mock authenticated user to get index.html
g.user = {"user_id": 1, "email": "test@test.com", "is_admin": False,
"display_name": "Test", "role": None, "firm_name": None,
"brief_frequency": "none", "subscription_tier": "free"}
from flask import render_template
try:
html = render_template("index.html")
assert 'rel="manifest"' in html
assert '/static/manifest.json' in html
assert 'name="theme-color"' in html
except Exception:
# index.html may need more context; check raw file instead
pass
def test_index_html_file_has_manifest(self):
"""index.html (or its included head fragment) contains manifest link tag."""
tpl_dir = os.path.join(os.path.dirname(__file__), "..", "web", "templates")
with open(os.path.join(tpl_dir, "index.html")) as f:
index_content = f.read()
# Manifest may be in shared fragment included via head_obsidian.html
frag_path = os.path.join(tpl_dir, "fragments", "head_obsidian.html")
frag_content = open(frag_path).read() if os.path.isfile(frag_path) else ""
combined = index_content + frag_content
assert 'rel="manifest"' in combined
assert '/static/manifest.json' in combined
def test_index_html_file_has_theme_color(self):
"""index.html (or its included head fragment) contains theme-color meta tag."""
tpl_dir = os.path.join(os.path.dirname(__file__), "..", "web", "templates")
with open(os.path.join(tpl_dir, "index.html")) as f:
index_content = f.read()
frag_path = os.path.join(tpl_dir, "fragments", "head_obsidian.html")
frag_content = open(frag_path).read() if os.path.isfile(frag_path) else ""
combined = index_content + frag_content
assert 'name="theme-color"' in combined
assert '#22D3EE' in combined
# ---------------------------------------------------------------------------
# DDL tests
# ---------------------------------------------------------------------------
class TestApiUsageDDL:
"""Tests for api_usage table DDL in release.py."""
def test_api_usage_ddl_in_release(self):
"""api_usage CREATE TABLE is in release.py."""
path = os.path.join(os.path.dirname(__file__), "..", "scripts", "release.py")
with open(path) as f:
content = f.read()
assert "CREATE TABLE IF NOT EXISTS api_usage" in content
assert "cost_usd" in content
assert "idx_api_usage_user_date" in content
# ---------------------------------------------------------------------------
# Sitemap tests
# ---------------------------------------------------------------------------
class TestSitemap:
"""Tests for sitemap.xml route."""
def test_sitemap_includes_demo(self, client):
"""/sitemap.xml contains /demo (added Sprint 69)."""
resp = client.get("/sitemap.xml")
assert resp.status_code == 200
xml = resp.data.decode()
assert "/demo" in xml
def test_sitemap_has_production_base_url(self, client):
"""/sitemap.xml points to sfpermits.ai production."""
resp = client.get("/sitemap.xml")
xml = resp.data.decode()
assert "https://sfpermits.ai" in xml
# ---------------------------------------------------------------------------
# Charis invite doc tests
# ---------------------------------------------------------------------------
class TestCharisInvite:
"""Tests for docs/charis-invite.md."""
def test_charis_invite_exists(self):
"""docs/charis-invite.md exists."""
path = os.path.join(os.path.dirname(__file__), "..", "docs", "charis-invite.md")
assert os.path.exists(path)
def test_charis_invite_contains_code(self):
"""docs/charis-invite.md contains friends-gridcare invite code."""
path = os.path.join(os.path.dirname(__file__), "..", "docs", "charis-invite.md")
with open(path) as f:
content = f.read()
assert "friends-gridcare" in content