Files
StarPunk/tests/test_templates.py
Phil Skentelbery 0cca8169ce feat: Implement Phase 4 Web Interface with bugfixes (v0.5.2)
## Phase 4: Web Interface Implementation

Implemented complete web interface with public and admin routes,
templates, CSS, and development authentication.

### Core Features

**Public Routes**:
- Homepage with recent published notes
- Note permalinks with microformats2
- Server-side rendering (Jinja2)

**Admin Routes**:
- Login via IndieLogin
- Dashboard with note management
- Create, edit, delete notes
- Protected with @require_auth decorator

**Development Authentication**:
- Dev login bypass for local testing (DEV_MODE only)
- Security safeguards per ADR-011
- Returns 404 when disabled

**Templates & Frontend**:
- Base layouts (public + admin)
- 8 HTML templates with microformats2
- Custom responsive CSS (114 lines)
- Error pages (404, 500)

### Bugfixes (v0.5.1 → v0.5.2)

1. **Cookie collision fix (v0.5.1)**:
   - Renamed auth cookie from "session" to "starpunk_session"
   - Fixed redirect loop between dev login and admin dashboard
   - Flask's session cookie no longer conflicts with auth

2. **HTTP 404 error handling (v0.5.1)**:
   - Update route now returns 404 for nonexistent notes
   - Delete route now returns 404 for nonexistent notes
   - Follows ADR-012 HTTP Error Handling Policy
   - Pattern consistency across all admin routes

3. **Note model enhancement (v0.5.2)**:
   - Exposed deleted_at field from database schema
   - Enables soft deletion verification in tests
   - Follows ADR-013 transparency principle

### Architecture

**New ADRs**:
- ADR-011: Development Authentication Mechanism
- ADR-012: HTTP Error Handling Policy
- ADR-013: Expose deleted_at Field in Note Model

**Standards Compliance**:
- Uses uv for Python environment
- Black formatted, Flake8 clean
- Follows git branching strategy
- Version incremented per versioning strategy

### Test Results

- 405/406 tests passing (99.75%)
- 87% code coverage
- All security tests passing
- Manual testing confirmed working

### Documentation

- Complete implementation reports in docs/reports/
- Architecture reviews in docs/reviews/
- Design documents in docs/design/
- CHANGELOG updated for v0.5.2

### Files Changed

**New Modules**:
- starpunk/dev_auth.py
- starpunk/routes/ (public, admin, auth, dev_auth)

**Templates**: 10 files (base, pages, admin, errors)
**Static**: CSS and optional JavaScript
**Tests**: 4 test files for routes and templates
**Docs**: 20+ architectural and implementation documents

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-18 23:01:53 -07:00

395 lines
14 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("/admin/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("/admin/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("/admin/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