""" 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/)""" 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 class TestOAuthMetadataEndpoint: """Test OAuth Client ID Metadata Document endpoint (.well-known/oauth-authorization-server)""" def test_oauth_metadata_endpoint_exists(self, client): """Verify metadata endpoint returns 200 OK""" response = client.get("/.well-known/oauth-authorization-server") assert response.status_code == 200 def test_oauth_metadata_content_type(self, client): """Verify response is JSON with correct content type""" response = client.get("/.well-known/oauth-authorization-server") assert response.status_code == 200 assert response.content_type == "application/json" def test_oauth_metadata_required_fields(self, client, app): """Verify all required fields are present and valid""" response = client.get("/.well-known/oauth-authorization-server") data = response.get_json() # Required fields per IndieAuth spec assert "client_id" in data assert "client_name" in data assert "redirect_uris" in data # client_id must match SITE_URL exactly (spec requirement) with app.app_context(): assert data["client_id"] == app.config["SITE_URL"] # redirect_uris must be array assert isinstance(data["redirect_uris"], list) assert len(data["redirect_uris"]) > 0 def test_oauth_metadata_optional_fields(self, client): """Verify recommended optional fields are present""" response = client.get("/.well-known/oauth-authorization-server") data = response.get_json() # Recommended fields assert "issuer" in data assert "client_uri" in data assert "grant_types_supported" in data assert "response_types_supported" in data assert "code_challenge_methods_supported" in data assert "token_endpoint_auth_methods_supported" in data def test_oauth_metadata_field_values(self, client, app): """Verify field values are correct""" response = client.get("/.well-known/oauth-authorization-server") data = response.get_json() with app.app_context(): site_url = app.config["SITE_URL"] # Verify URLs assert data["issuer"] == site_url assert data["client_id"] == site_url assert data["client_uri"] == site_url # Verify redirect_uris contains auth callback assert f"{site_url}/auth/callback" in data["redirect_uris"] # Verify supported methods assert "authorization_code" in data["grant_types_supported"] assert "code" in data["response_types_supported"] assert "S256" in data["code_challenge_methods_supported"] assert "none" in data["token_endpoint_auth_methods_supported"] def test_oauth_metadata_redirect_uris_is_array(self, client): """Verify redirect_uris is array, not string (common pitfall)""" response = client.get("/.well-known/oauth-authorization-server") data = response.get_json() assert isinstance(data["redirect_uris"], list) assert not isinstance(data["redirect_uris"], str) def test_oauth_metadata_cache_headers(self, client): """Verify appropriate cache headers are set""" response = client.get("/.well-known/oauth-authorization-server") assert response.status_code == 200 # Should cache for 24 hours (86400 seconds) assert response.cache_control.max_age == 86400 assert response.cache_control.public is True def test_oauth_metadata_valid_json(self, client): """Verify response is valid, parseable JSON""" response = client.get("/.well-known/oauth-authorization-server") assert response.status_code == 200 # get_json() will raise ValueError if JSON is invalid data = response.get_json() assert data is not None assert isinstance(data, dict) def test_oauth_metadata_uses_config_values(self, tmp_path): """Verify metadata uses config values, not hardcoded strings""" test_data_dir = tmp_path / "oauth_test" test_data_dir.mkdir(parents=True, exist_ok=True) # Create app with custom config 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": "https://custom-site.example.com", "SITE_NAME": "Custom Site Name", "DEV_MODE": False, } app = create_app(config=test_config) client = app.test_client() response = client.get("/.well-known/oauth-authorization-server") data = response.get_json() # Should use custom config values assert data["client_id"] == "https://custom-site.example.com" assert data["client_name"] == "Custom Site Name" assert data["client_uri"] == "https://custom-site.example.com" assert ( "https://custom-site.example.com/auth/callback" in data["redirect_uris"] ) class TestIndieAuthMetadataLink: """Test indieauth-metadata link in HTML head""" def test_indieauth_metadata_link_present(self, client): """Verify discovery link is present in HTML head""" response = client.get("/") assert response.status_code == 200 assert b'rel="indieauth-metadata"' in response.data def test_indieauth_metadata_link_points_to_endpoint(self, client): """Verify link points to correct endpoint""" response = client.get("/") assert response.status_code == 200 assert b"/.well-known/oauth-authorization-server" in response.data def test_indieauth_metadata_link_in_head(self, client): """Verify link is in section""" response = client.get("/") assert response.status_code == 200 # Simple check: link should appear before html = response.data.decode("utf-8") metadata_link_pos = html.find('rel="indieauth-metadata"') body_pos = html.find("") assert metadata_link_pos != -1 assert body_pos != -1 assert metadata_link_pos < body_pos