Files
StarPunk/tests/test_routes_public.py
Phil Skentelbery a3bac86647 feat: Complete IndieAuth server removal (Phases 2-4)
Completed all remaining phases of ADR-030 IndieAuth provider removal.
StarPunk no longer acts as an authorization server - all IndieAuth
operations delegated to external providers.

Phase 2 - Remove Token Issuance:
- Deleted /auth/token endpoint
- Removed token_endpoint() function from routes/auth.py
- Deleted tests/test_routes_token.py

Phase 3 - Remove Token Storage:
- Deleted starpunk/tokens.py module entirely
- Created migration 004 to drop tokens and authorization_codes tables
- Deleted tests/test_tokens.py
- Removed all internal token CRUD operations

Phase 4 - External Token Verification:
- Created starpunk/auth_external.py module
- Implemented verify_external_token() for external IndieAuth providers
- Updated Micropub endpoint to use external verification
- Added TOKEN_ENDPOINT configuration
- Updated all Micropub tests to mock external verification
- HTTP timeout protection (5s) for external requests

Additional Changes:
- Created migration 003 to remove code_verifier from auth_state
- Fixed 5 migration tests that referenced obsolete code_verifier column
- Updated 11 Micropub tests for external verification
- Fixed test fixture and app context issues
- All 501 tests passing

Breaking Changes:
- Micropub clients must use external IndieAuth providers
- TOKEN_ENDPOINT configuration now required
- Existing internal tokens invalid (tables dropped)

Migration Impact:
- Simpler codebase: -500 lines of code
- Fewer database tables: -2 tables (tokens, authorization_codes)
- More secure: External providers handle token security
- More maintainable: Less authentication code to maintain

Standards Compliance:
- W3C IndieAuth specification
- OAuth 2.0 Bearer token authentication
- IndieWeb principle: delegate to external services

Related:
- ADR-030: IndieAuth Provider Removal Strategy
- ADR-050: Remove Custom IndieAuth Server
- Migration 003: Remove code_verifier from auth_state
- Migration 004: Drop tokens and authorization_codes tables

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-24 17:23:46 -07:00

284 lines
9.6 KiB
Python

"""
Tests for public routes (homepage, note permalinks)
Tests cover:
- Homepage rendering with notes list
- Note permalink rendering
- 404 behavior for missing/unpublished notes
- Microformats2 markup
- Flash message display
- Error page rendering
"""
import pytest
from starpunk import create_app
from starpunk.notes import create_note
@pytest.fixture
def app(tmp_path):
"""Create test application with dev mode disabled"""
# Create test-specific data directory
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-key-for-testing-only",
"ADMIN_ME": "https://test.example.com",
"SITE_URL": "http://localhost:5000",
"DEV_MODE": False,
}
app = create_app(config=test_config)
yield app
@pytest.fixture
def client(app):
"""Test client for making requests"""
return app.test_client()
@pytest.fixture
def sample_notes(app):
"""Create sample notes for testing"""
with app.app_context():
notes = []
for i in range(5):
note = create_note(
content=f"# Test Note {i}\n\nThis is test note number {i}.",
published=(i % 2 == 0), # Even notes published, odd are drafts
)
notes.append(note)
return notes
class TestHomepage:
"""Test homepage route (/)"""
def test_homepage_renders(self, client):
"""Test homepage renders successfully"""
response = client.get("/")
assert response.status_code == 200
assert b"StarPunk" in response.data
def test_homepage_shows_published_notes(self, client, sample_notes):
"""Test homepage shows only published notes"""
response = client.get("/")
assert response.status_code == 200
# Published notes should appear (notes 0, 2, 4)
assert b"Test Note 0" in response.data
assert b"Test Note 2" in response.data
assert b"Test Note 4" in response.data
# Draft notes should not appear (notes 1, 3)
assert b"Test Note 1" not in response.data
assert b"Test Note 3" not in response.data
def test_homepage_empty_state(self, client):
"""Test homepage with no notes"""
response = client.get("/")
assert response.status_code == 200
# Should have some message about no notes
assert b"No notes" in response.data or b"Welcome" in response.data
def test_homepage_has_feed_link(self, client):
"""Test homepage has RSS feed link"""
response = client.get("/")
assert response.status_code == 200
assert b"feed.xml" in response.data or b"RSS" in response.data
def test_homepage_has_h_feed_microformat(self, client, sample_notes):
"""Test homepage has h-feed microformat"""
response = client.get("/")
assert response.status_code == 200
assert b"h-feed" in response.data
def test_homepage_notes_have_h_entry(self, client, sample_notes):
"""Test notes on homepage have h-entry microformat"""
response = client.get("/")
assert response.status_code == 200
assert b"h-entry" in response.data
class TestNotePermalink:
"""Test individual note permalink route (/note/<slug>)"""
def test_published_note_renders(self, client, sample_notes):
"""Test published note permalink renders"""
# Get a published note (note 0)
with client.application.app_context():
from starpunk.notes import list_notes
notes = list_notes(published_only=True)
assert len(notes) > 0
slug = notes[0].slug
response = client.get(f"/note/{slug}")
assert response.status_code == 200
assert b"Test Note" in response.data
def test_note_has_full_content(self, client, sample_notes):
"""Test note page shows full content"""
with client.application.app_context():
from starpunk.notes import list_notes
notes = list_notes(published_only=True)
slug = notes[0].slug
response = client.get(f"/note/{slug}")
assert response.status_code == 200
assert b"test note number" in response.data
def test_note_has_h_entry_microformat(self, client, sample_notes):
"""Test note page has h-entry microformat"""
with client.application.app_context():
from starpunk.notes import list_notes
notes = list_notes(published_only=True)
slug = notes[0].slug
response = client.get(f"/note/{slug}")
assert response.status_code == 200
assert b"h-entry" in response.data
assert b"e-content" in response.data
assert b"dt-published" in response.data
def test_note_has_permalink_url(self, client, sample_notes):
"""Test note page has permalink URL"""
with client.application.app_context():
from starpunk.notes import list_notes
notes = list_notes(published_only=True)
slug = notes[0].slug
response = client.get(f"/note/{slug}")
assert response.status_code == 200
assert b"u-url" in response.data
def test_draft_note_returns_404(self, client, sample_notes):
"""Test draft note returns 404"""
# Get a draft note (note 1)
with client.application.app_context():
from starpunk.notes import list_notes
# Get all notes, then filter to drafts
all_notes = list_notes()
draft_notes = [n for n in all_notes if not n.published]
assert len(draft_notes) > 0
slug = draft_notes[0].slug
response = client.get(f"/note/{slug}")
assert response.status_code == 404
def test_missing_note_returns_404(self, client):
"""Test missing note returns 404"""
response = client.get("/note/nonexistent-slug")
assert response.status_code == 404
def test_note_has_back_link(self, client, sample_notes):
"""Test note page has link back to homepage"""
with client.application.app_context():
from starpunk.notes import list_notes
notes = list_notes(published_only=True)
slug = notes[0].slug
response = client.get(f"/note/{slug}")
assert response.status_code == 200
# Should have a link to home
assert b'href="/"' in response.data
class TestFlashMessages:
"""Test flash message display"""
def test_flash_messages_display(self, client):
"""Test flash messages are displayed"""
with client.session_transaction() as session:
session["_flashes"] = [("success", "Test message")]
response = client.get("/")
assert response.status_code == 200
assert b"Test message" in response.data
def test_flash_message_categories(self, client):
"""Test different flash message categories"""
categories = ["success", "error", "warning", "info"]
for category in categories:
with client.session_transaction() as session:
session["_flashes"] = [(category, f"{category} message")]
response = client.get("/")
assert response.status_code == 200
assert f"{category} message".encode() in response.data
class TestErrorPages:
"""Test error page rendering"""
def test_404_page_renders(self, client):
"""Test 404 error page"""
response = client.get("/nonexistent-page")
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-page")
assert response.status_code == 404
assert b'href="/"' in response.data
class TestDevModeIndicator:
"""Test dev mode warning display"""
def test_dev_mode_warning_not_shown_in_production(self, client):
"""Test dev mode warning not shown when DEV_MODE=false"""
response = client.get("/")
assert response.status_code == 200
assert b"DEVELOPMENT MODE" not in response.data
def test_dev_mode_warning_shown_when_enabled(self, tmp_path):
"""Test dev mode warning shown when DEV_MODE=true"""
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
class TestVersionDisplay:
"""Test version number display"""
def test_version_in_footer(self, client):
"""Test version number appears in footer"""
response = client.get("/")
assert response.status_code == 200
assert b"0.5.0" in response.data or b"StarPunk v" in response.data
# OAuth metadata endpoint tests removed in Phase 1 of IndieAuth server removal
# The /.well-known/oauth-authorization-server endpoint was removed as part of
# removing the built-in IndieAuth authorization server functionality.
# See: docs/architecture/indieauth-removal-phases.md