The auth routes were registered under /admin/* but the IndieAuth redirect_uri was configured as /auth/callback, causing 404 errors when providers redirected back after authentication. - Change auth blueprint url_prefix from "/admin" to "/auth" - Update test expectations for new auth route paths - Add ADR-022 documenting the architectural decision 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
438 lines
16 KiB
Python
438 lines
16 KiB
Python
"""
|
|
Tests for template rendering and structure
|
|
|
|
Tests cover:
|
|
- Template inheritance
|
|
- Microformats2 markup
|
|
- HTML structure and validity
|
|
- Template variables and context
|
|
- Flash message rendering
|
|
- Error templates
|
|
"""
|
|
|
|
import pytest
|
|
from starpunk import create_app
|
|
from starpunk.notes import create_note
|
|
|
|
|
|
@pytest.fixture
|
|
def app(tmp_path):
|
|
"""Create test application"""
|
|
test_data_dir = tmp_path / "data"
|
|
test_data_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
test_config = {
|
|
"TESTING": True,
|
|
"DATABASE_PATH": test_data_dir / "starpunk.db",
|
|
"DATA_PATH": test_data_dir,
|
|
"NOTES_PATH": test_data_dir / "notes",
|
|
"SESSION_SECRET": "test-secret",
|
|
"SITE_URL": "http://localhost:5000",
|
|
"ADMIN_ME": "https://test.example.com",
|
|
"DEV_MODE": False,
|
|
"SITE_NAME": "Test StarPunk",
|
|
"VERSION": "0.5.0",
|
|
}
|
|
app = create_app(config=test_config)
|
|
yield app
|
|
|
|
|
|
@pytest.fixture
|
|
def client(app):
|
|
"""Test client"""
|
|
return app.test_client()
|
|
|
|
|
|
class TestBaseTemplate:
|
|
"""Test base.html template"""
|
|
|
|
def test_base_has_doctype(self, client):
|
|
"""Test base template has HTML5 doctype"""
|
|
response = client.get("/")
|
|
assert response.status_code == 200
|
|
assert response.data.startswith(b"<!DOCTYPE html>")
|
|
|
|
def test_base_has_charset(self, client):
|
|
"""Test base template has charset meta tag"""
|
|
response = client.get("/")
|
|
assert response.status_code == 200
|
|
assert b'charset="UTF-8"' in response.data or b"charset=UTF-8" in response.data
|
|
|
|
def test_base_has_viewport(self, client):
|
|
"""Test base template has viewport meta tag"""
|
|
response = client.get("/")
|
|
assert response.status_code == 200
|
|
assert b"viewport" in response.data
|
|
assert b"width=device-width" in response.data
|
|
|
|
def test_base_has_title(self, client):
|
|
"""Test base template has title"""
|
|
response = client.get("/")
|
|
assert response.status_code == 200
|
|
assert b"<title>" in response.data
|
|
assert b"StarPunk" in response.data
|
|
|
|
def test_base_has_stylesheet(self, client):
|
|
"""Test base template links to stylesheet"""
|
|
response = client.get("/")
|
|
assert response.status_code == 200
|
|
assert b"<link" in response.data
|
|
assert b"stylesheet" in response.data
|
|
assert b"style.css" in response.data
|
|
|
|
def test_base_has_rss_link(self, client):
|
|
"""Test base template has RSS feed link"""
|
|
response = client.get("/")
|
|
assert response.status_code == 200
|
|
assert b"application/rss+xml" in response.data or b"feed.xml" in response.data
|
|
|
|
def test_base_has_header(self, client):
|
|
"""Test base template has header"""
|
|
response = client.get("/")
|
|
assert response.status_code == 200
|
|
assert b"<header" in response.data
|
|
|
|
def test_base_has_main(self, client):
|
|
"""Test base template has main content area"""
|
|
response = client.get("/")
|
|
assert response.status_code == 200
|
|
assert b"<main" in response.data
|
|
|
|
def test_base_has_footer(self, client):
|
|
"""Test base template has footer"""
|
|
response = client.get("/")
|
|
assert response.status_code == 200
|
|
assert b"<footer" in response.data
|
|
|
|
def test_base_footer_has_version(self, client):
|
|
"""Test footer shows version number"""
|
|
response = client.get("/")
|
|
assert response.status_code == 200
|
|
assert b"0.5.0" in response.data
|
|
|
|
def test_base_has_navigation(self, client):
|
|
"""Test base has navigation"""
|
|
response = client.get("/")
|
|
assert response.status_code == 200
|
|
assert b"<nav" in response.data or b'href="/"' in response.data
|
|
|
|
|
|
class TestHomepageTemplate:
|
|
"""Test index.html template"""
|
|
|
|
def test_homepage_has_h_feed(self, client):
|
|
"""Test homepage has h-feed microformat"""
|
|
with client.application.test_request_context():
|
|
create_note("# Test\n\nContent", published=True)
|
|
|
|
response = client.get("/")
|
|
assert response.status_code == 200
|
|
assert b"h-feed" in response.data
|
|
|
|
def test_homepage_notes_have_h_entry(self, client):
|
|
"""Test notes on homepage have h-entry"""
|
|
with client.application.test_request_context():
|
|
create_note("# Test\n\nContent", published=True)
|
|
|
|
response = client.get("/")
|
|
assert response.status_code == 200
|
|
assert b"h-entry" in response.data
|
|
|
|
def test_homepage_empty_state(self, client):
|
|
"""Test homepage shows message when no notes"""
|
|
response = client.get("/")
|
|
assert response.status_code == 200
|
|
# Should have some indication of empty state
|
|
data_lower = response.data.lower()
|
|
assert (
|
|
b"no notes" in data_lower
|
|
or b"welcome" in data_lower
|
|
or b"get started" in data_lower
|
|
)
|
|
|
|
|
|
class TestNoteTemplate:
|
|
"""Test note.html template"""
|
|
|
|
def test_note_has_h_entry(self, client):
|
|
"""Test note page has h-entry microformat"""
|
|
with client.application.test_request_context():
|
|
note = create_note("# Test Note\n\nContent here.", published=True)
|
|
|
|
response = client.get(f"/note/{note.slug}")
|
|
assert response.status_code == 200
|
|
assert b"h-entry" in response.data
|
|
|
|
def test_note_has_e_content(self, client):
|
|
"""Test note has e-content for content"""
|
|
with client.application.test_request_context():
|
|
note = create_note("# Test Note\n\nContent here.", published=True)
|
|
|
|
response = client.get(f"/note/{note.slug}")
|
|
assert response.status_code == 200
|
|
assert b"e-content" in response.data
|
|
|
|
def test_note_has_dt_published(self, client):
|
|
"""Test note has dt-published for date"""
|
|
with client.application.test_request_context():
|
|
note = create_note("# Test Note\n\nContent here.", published=True)
|
|
|
|
response = client.get(f"/note/{note.slug}")
|
|
assert response.status_code == 200
|
|
assert b"dt-published" in response.data
|
|
|
|
def test_note_has_u_url(self, client):
|
|
"""Test note has u-url for permalink"""
|
|
with client.application.test_request_context():
|
|
note = create_note("# Test Note\n\nContent here.", published=True)
|
|
|
|
response = client.get(f"/note/{note.slug}")
|
|
assert response.status_code == 200
|
|
assert b"u-url" in response.data
|
|
|
|
def test_note_renders_markdown(self, client):
|
|
"""Test note content is rendered as HTML"""
|
|
with client.application.test_request_context():
|
|
note = create_note("# Heading\n\n**Bold** text.", published=True)
|
|
|
|
response = client.get(f"/note/{note.slug}")
|
|
assert response.status_code == 200
|
|
# Should have HTML heading
|
|
assert b"<h1" in response.data
|
|
# Should have bold
|
|
assert b"<strong>" in response.data or b"<b>" in response.data
|
|
|
|
|
|
class TestAdminTemplates:
|
|
"""Test admin templates"""
|
|
|
|
def test_login_template_has_form(self, client):
|
|
"""Test login page has form"""
|
|
response = client.get("/auth/login")
|
|
assert response.status_code == 200
|
|
assert b"<form" in response.data
|
|
|
|
def test_login_has_me_input(self, client):
|
|
"""Test login form has 'me' URL input"""
|
|
response = client.get("/auth/login")
|
|
assert response.status_code == 200
|
|
assert b'name="me"' in response.data or b'id="me"' in response.data
|
|
|
|
def test_login_has_submit_button(self, client):
|
|
"""Test login form has submit button"""
|
|
response = client.get("/auth/login")
|
|
assert response.status_code == 200
|
|
assert b'type="submit"' in response.data or b"<button" in response.data
|
|
|
|
def test_dashboard_extends_admin_base(self, client):
|
|
"""Test dashboard uses admin base template"""
|
|
from starpunk.auth import create_session
|
|
|
|
with client.application.test_request_context():
|
|
token = create_session("https://test.example.com")
|
|
|
|
client.set_cookie("starpunk_session", token)
|
|
response = client.get("/admin/")
|
|
assert response.status_code == 200
|
|
# Should have admin-specific elements
|
|
assert b"Dashboard" in response.data or b"Admin" in response.data
|
|
|
|
def test_new_note_form_has_textarea(self, client):
|
|
"""Test new note form has textarea"""
|
|
from starpunk.auth import create_session
|
|
|
|
with client.application.test_request_context():
|
|
token = create_session("https://test.example.com")
|
|
|
|
client.set_cookie("starpunk_session", token)
|
|
response = client.get("/admin/new")
|
|
assert response.status_code == 200
|
|
assert b"<textarea" in response.data
|
|
|
|
def test_new_note_form_has_published_checkbox(self, client):
|
|
"""Test new note form has published checkbox"""
|
|
from starpunk.auth import create_session
|
|
|
|
with client.application.test_request_context():
|
|
token = create_session("https://test.example.com")
|
|
|
|
client.set_cookie("starpunk_session", token)
|
|
response = client.get("/admin/new")
|
|
assert response.status_code == 200
|
|
assert b'type="checkbox"' in response.data
|
|
|
|
def test_edit_form_prefilled(self, client):
|
|
"""Test edit form is prefilled with content"""
|
|
from starpunk.auth import create_session
|
|
|
|
with client.application.test_request_context():
|
|
token = create_session("https://test.example.com")
|
|
note = create_note("# Edit Test\n\nContent.", published=True)
|
|
|
|
client.set_cookie("starpunk_session", token)
|
|
response = client.get(f"/admin/edit/{note.id}")
|
|
assert response.status_code == 200
|
|
assert b"Edit Test" in response.data
|
|
|
|
|
|
class TestFlashMessages:
|
|
"""Test flash message rendering"""
|
|
|
|
def test_flash_message_success(self, client):
|
|
"""Test success flash message renders"""
|
|
with client.session_transaction() as session:
|
|
session["_flashes"] = [("success", "Operation successful")]
|
|
|
|
response = client.get("/")
|
|
assert response.status_code == 200
|
|
assert b"Operation successful" in response.data
|
|
assert b"flash" in response.data or b"success" in response.data
|
|
|
|
def test_flash_message_error(self, client):
|
|
"""Test error flash message renders"""
|
|
with client.session_transaction() as session:
|
|
session["_flashes"] = [("error", "An error occurred")]
|
|
|
|
response = client.get("/")
|
|
assert response.status_code == 200
|
|
assert b"An error occurred" in response.data
|
|
assert b"flash" in response.data or b"error" in response.data
|
|
|
|
def test_flash_message_warning(self, client):
|
|
"""Test warning flash message renders"""
|
|
with client.session_transaction() as session:
|
|
session["_flashes"] = [("warning", "Be careful")]
|
|
|
|
response = client.get("/")
|
|
assert response.status_code == 200
|
|
assert b"Be careful" in response.data
|
|
|
|
def test_multiple_flash_messages(self, client):
|
|
"""Test multiple flash messages render"""
|
|
with client.session_transaction() as session:
|
|
session["_flashes"] = [
|
|
("success", "First message"),
|
|
("error", "Second message"),
|
|
]
|
|
|
|
response = client.get("/")
|
|
assert response.status_code == 200
|
|
assert b"First message" in response.data
|
|
assert b"Second message" in response.data
|
|
|
|
|
|
class TestErrorTemplates:
|
|
"""Test error page templates"""
|
|
|
|
def test_404_template(self, client):
|
|
"""Test 404 error page"""
|
|
response = client.get("/nonexistent")
|
|
assert response.status_code == 404
|
|
assert b"404" in response.data or b"Not Found" in response.data
|
|
|
|
def test_404_has_home_link(self, client):
|
|
"""Test 404 page has link to homepage"""
|
|
response = client.get("/nonexistent")
|
|
assert response.status_code == 404
|
|
assert b'href="/"' in response.data
|
|
|
|
|
|
class TestDevModeIndicator:
|
|
"""Test dev mode warning in templates"""
|
|
|
|
def test_dev_mode_warning_shown(self, tmp_path):
|
|
"""Test dev mode warning appears when enabled"""
|
|
test_data_dir = tmp_path / "dev_data"
|
|
test_data_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
test_config = {
|
|
"TESTING": True,
|
|
"DATABASE_PATH": test_data_dir / "starpunk.db",
|
|
"DATA_PATH": test_data_dir,
|
|
"NOTES_PATH": test_data_dir / "notes",
|
|
"SESSION_SECRET": "test-secret",
|
|
"SITE_URL": "http://localhost:5000",
|
|
"DEV_MODE": True,
|
|
"DEV_ADMIN_ME": "https://dev.example.com",
|
|
}
|
|
app = create_app(config=test_config)
|
|
|
|
client = app.test_client()
|
|
response = client.get("/")
|
|
assert response.status_code == 200
|
|
assert b"DEVELOPMENT MODE" in response.data or b"DEV MODE" in response.data
|
|
|
|
def test_dev_mode_warning_not_shown(self, client):
|
|
"""Test dev mode warning not shown in production"""
|
|
response = client.get("/")
|
|
assert response.status_code == 200
|
|
assert b"DEVELOPMENT MODE" not in response.data
|
|
|
|
|
|
class TestTemplateVariables:
|
|
"""Test template variables are available"""
|
|
|
|
def test_config_available(self, client):
|
|
"""Test config is available in templates"""
|
|
response = client.get("/")
|
|
assert response.status_code == 200
|
|
# VERSION should be rendered
|
|
assert b"0.5.0" in response.data
|
|
|
|
def test_site_name_available(self, client):
|
|
"""Test SITE_NAME is available"""
|
|
response = client.get("/")
|
|
assert response.status_code == 200
|
|
# Should have site name in title or header
|
|
assert b"<title>" in response.data
|
|
|
|
def test_url_for_works(self, client):
|
|
"""Test url_for generates correct URLs"""
|
|
response = client.get("/")
|
|
assert response.status_code == 200
|
|
# Should have URLs like /admin, /admin/login, etc.
|
|
assert b"href=" in response.data
|
|
|
|
|
|
class TestIndieAuthClientDiscovery:
|
|
"""Test IndieAuth client discovery (h-app microformats)"""
|
|
|
|
def test_h_app_microformats_present(self, client):
|
|
"""Verify h-app client discovery markup exists"""
|
|
response = client.get("/")
|
|
assert response.status_code == 200
|
|
assert b'class="h-app"' in response.data
|
|
|
|
def test_h_app_contains_url_and_name_properties(self, client):
|
|
"""Verify h-app contains u-url and p-name properties"""
|
|
response = client.get("/")
|
|
assert response.status_code == 200
|
|
assert b'class="u-url p-name"' in response.data
|
|
|
|
def test_h_app_contains_site_url(self, client, app):
|
|
"""Verify h-app contains correct site URL"""
|
|
response = client.get("/")
|
|
assert response.status_code == 200
|
|
assert app.config["SITE_URL"].encode() in response.data
|
|
|
|
def test_h_app_contains_site_name(self, client, app):
|
|
"""Verify h-app contains site name"""
|
|
response = client.get("/")
|
|
assert response.status_code == 200
|
|
site_name = app.config.get("SITE_NAME", "StarPunk").encode()
|
|
assert site_name in response.data
|
|
|
|
def test_h_app_is_hidden(self, client):
|
|
"""Verify h-app has hidden attribute for visual hiding"""
|
|
response = client.get("/")
|
|
assert response.status_code == 200
|
|
# h-app div should have hidden attribute
|
|
assert b'class="h-app" hidden' in response.data
|
|
|
|
def test_h_app_is_aria_hidden(self, client):
|
|
"""Verify h-app has aria-hidden for screen reader hiding"""
|
|
response = client.get("/")
|
|
assert response.status_code == 200
|
|
# h-app div should have aria-hidden="true"
|
|
assert b'aria-hidden="true"' in response.data
|