""" Tests for Microformats2 markup in templates Per v1.2.0 Phase 2 and developer Q&A Q31-Q33: - Use mf2py to validate generated HTML - Test h-entry, h-card, h-feed markup - Ensure all required properties present - Validate p-name only appears with explicit titles (per Q22) """ import mf2py import pytest from unittest.mock import patch class TestNoteHEntry: """Test h-entry markup on individual note pages""" def test_note_has_hentry_markup(self, client, app, sample_note): """Note page has h-entry container""" # Sample note is already published, just get its slug response = client.get(f'/note/{sample_note.slug}') assert response.status_code == 200, f"Failed to load note at /note/{sample_note.slug}" # Parse microformats parsed = mf2py.parse(doc=response.data.decode(), url=f'http://localhost/note/{sample_note.slug}') # Should have at least one h-entry entries = [item for item in parsed['items'] if 'h-entry' in item.get('type', [])] assert len(entries) >= 1 def test_hentry_has_required_properties(self, client, app, sample_note): """h-entry has all required Microformats2 properties""" response = client.get(f'/note/{sample_note.slug}') parsed = mf2py.parse(doc=response.data.decode(), url=f'http://localhost/note/{sample_note.slug}') entry = [item for item in parsed['items'] if 'h-entry' in item.get('type', [])][0] props = entry.get('properties', {}) # Required properties per spec assert 'url' in props, "h-entry missing u-url" assert 'published' in props, "h-entry missing dt-published" assert 'content' in props, "h-entry missing e-content" assert 'author' in props, "h-entry missing p-author" def test_hentry_url_and_uid_match(self, client, app, sample_note): """u-url and u-uid are the same for notes (per Q23)""" response = client.get(f'/note/{sample_note.slug}') parsed = mf2py.parse(doc=response.data.decode(), url=f'http://localhost/note/{sample_note.slug}') entry = [item for item in parsed['items'] if 'h-entry' in item.get('type', [])][0] props = entry.get('properties', {}) # Both should exist and match assert 'url' in props assert 'uid' in props assert props['url'][0] == props['uid'][0], "u-url and u-uid should match" def test_hentry_pname_only_with_explicit_title(self, client, app, data_dir): """p-name only present when note has explicit title (per Q22)""" from starpunk.notes import create_note # Create note WITH heading (explicit title) with app.app_context(): note_with_title = create_note( content="# Explicit Title\n\nThis note has a heading.", custom_slug="note-with-title", published=True ) response = client.get('/note/note-with-title') assert response.status_code == 200, f"Failed to get note: {response.status_code}" parsed = mf2py.parse(doc=response.data.decode(), url='http://localhost/note/note-with-title') entries = [item for item in parsed['items'] if 'h-entry' in item.get('type', [])] assert len(entries) > 0, "No h-entry found in parsed HTML" entry = entries[0] props = entry.get('properties', {}) # Should have p-name assert 'name' in props, "Note with explicit title should have p-name" assert props['name'][0] == 'Explicit Title' # Create note WITHOUT heading (no explicit title) with app.app_context(): note_without_title = create_note( content="Just a simple note without a heading.", custom_slug="note-without-title", published=True ) response = client.get('/note/note-without-title') assert response.status_code == 200, f"Failed to get note: {response.status_code}" parsed = mf2py.parse(doc=response.data.decode(), url='http://localhost/note/note-without-title') entries = [item for item in parsed['items'] if 'h-entry' in item.get('type', [])] assert len(entries) > 0, "No h-entry found in parsed HTML" entry = entries[0] props = entry.get('properties', {}) # Should NOT have explicit p-name (or it should be implicit from content) # Per Q22: p-name only if has_explicit_title # If p-name exists, it shouldn't be set explicitly in our markup # (mf2py may infer it from content, but we shouldn't add class="p-name") def test_hentry_has_updated_if_modified(self, client, app, sample_note, data_dir): """dt-updated present if note was modified""" from starpunk.notes import update_note from pathlib import Path import time # Update the note time.sleep(0.1) # Ensure different timestamp with app.app_context(): update_note(sample_note.slug, content="Updated content") response = client.get(f'/note/{sample_note.slug}') parsed = mf2py.parse(doc=response.data.decode(), url=f'http://localhost/note/{sample_note.slug}') entry = [item for item in parsed['items'] if 'h-entry' in item.get('type', [])][0] props = entry.get('properties', {}) # Should have dt-updated assert 'updated' in props, "Modified note should have dt-updated" class TestAuthorHCard: """Test h-card markup for author""" def test_hentry_has_nested_hcard(self, client, app, sample_note): """h-entry has nested p-author h-card (per Q20)""" # Mock author profile in context mock_author = { 'me': 'https://author.example.com', 'name': 'Test Author', 'photo': 'https://example.com/photo.jpg', 'url': 'https://author.example.com', 'note': 'Test bio', 'rel_me_links': [], } with patch('starpunk.author_discovery.get_author_profile', return_value=mock_author): response = client.get(f'/note/{sample_note.slug}') parsed = mf2py.parse(doc=response.data.decode(), url=f'http://localhost/note/{sample_note.slug}') entry = [item for item in parsed['items'] if 'h-entry' in item.get('type', [])][0] props = entry.get('properties', {}) # Should have p-author assert 'author' in props, "h-entry should have p-author" # Author should be h-card author = props['author'][0] if isinstance(author, dict): assert 'h-card' in author.get('type', []), "p-author should be h-card" author_props = author.get('properties', {}) assert 'name' in author_props, "h-card should have p-name" assert author_props['name'][0] == 'Test Author' def test_hcard_not_standalone(self, client, app, sample_note): """h-card only within h-entry, not standalone (per Q20)""" response = client.get(f'/note/{sample_note.slug}') parsed = mf2py.parse(doc=response.data.decode(), url=f'http://localhost/note/{sample_note.slug}') # Find all h-cards at root level root_hcards = [item for item in parsed['items'] if 'h-card' in item.get('type', [])] # Should NOT have root-level h-cards (only nested in h-entry) # Note: This might not be strictly enforced by mf2py parsing, # but we can check that h-entry exists and contains h-card entries = [item for item in parsed['items'] if 'h-entry' in item.get('type', [])] assert len(entries) > 0, "Should have h-entry" def test_hcard_has_required_properties(self, client, app, sample_note): """h-card has name and url at minimum""" mock_author = { 'me': 'https://author.example.com', 'name': 'Test Author', 'photo': None, 'url': 'https://author.example.com', 'note': None, 'rel_me_links': [], } with patch('starpunk.author_discovery.get_author_profile', return_value=mock_author): response = client.get(f'/note/{sample_note.slug}') parsed = mf2py.parse(doc=response.data.decode(), url=f'http://localhost/note/{sample_note.slug}') entry = [item for item in parsed['items'] if 'h-entry' in item.get('type', [])][0] props = entry.get('properties', {}) author = props['author'][0] if isinstance(author, dict): author_props = author.get('properties', {}) assert 'name' in author_props, "h-card must have p-name" assert 'url' in author_props, "h-card must have u-url" class TestFeedHFeed: """Test h-feed markup on index page""" def test_index_has_hfeed(self, client, app): """Index page has h-feed container (per Q24)""" response = client.get('/') assert response.status_code == 200 parsed = mf2py.parse(doc=response.data.decode(), url='http://localhost/') # Should have h-feed feeds = [item for item in parsed['items'] if 'h-feed' in item.get('type', [])] assert len(feeds) >= 1, "Index should have h-feed" def test_hfeed_has_name(self, client, app): """h-feed has p-name (feed title)""" response = client.get('/') parsed = mf2py.parse(doc=response.data.decode(), url='http://localhost/') feed = [item for item in parsed['items'] if 'h-feed' in item.get('type', [])][0] props = feed.get('properties', {}) assert 'name' in props, "h-feed should have p-name" def test_hfeed_contains_hentries(self, client, app, sample_note): """h-feed contains h-entry children""" response = client.get('/') parsed = mf2py.parse(doc=response.data.decode(), url='http://localhost/') feed = [item for item in parsed['items'] if 'h-feed' in item.get('type', [])][0] # Should have children (h-entries) children = feed.get('children', []) entries = [child for child in children if 'h-entry' in child.get('type', [])] assert len(entries) > 0, "h-feed should contain h-entry children" def test_feed_entries_have_author(self, client, app, sample_note): """Each h-entry in feed has p-author h-card (per Q20)""" mock_author = { 'me': 'https://author.example.com', 'name': 'Test Author', 'photo': None, 'url': 'https://author.example.com', 'note': None, 'rel_me_links': [], } with patch('starpunk.author_discovery.get_author_profile', return_value=mock_author): response = client.get('/') parsed = mf2py.parse(doc=response.data.decode(), url='http://localhost/') feed = [item for item in parsed['items'] if 'h-feed' in item.get('type', [])][0] children = feed.get('children', []) entries = [child for child in children if 'h-entry' in child.get('type', [])] # Each entry should have author for entry in entries: props = entry.get('properties', {}) assert 'author' in props, "Feed h-entry should have p-author" class TestRelMe: """Test rel-me links in HTML head""" def test_relme_links_in_head(self, client, app): """rel=me links present in HTML head (per Q20)""" mock_author = { 'me': 'https://author.example.com', 'name': 'Test Author', 'photo': None, 'url': 'https://author.example.com', 'note': None, 'rel_me_links': [ 'https://github.com/testuser', 'https://mastodon.social/@testuser', ], } with patch('starpunk.author_discovery.get_author_profile', return_value=mock_author): response = client.get('/') parsed = mf2py.parse(doc=response.data.decode(), url='http://localhost/') # Check rel=me in rels rels = parsed.get('rels', {}) assert 'me' in rels, "Should have rel=me links" assert 'https://github.com/testuser' in rels['me'] assert 'https://mastodon.social/@testuser' in rels['me'] def test_no_relme_without_author(self, client, app): """No rel=me links if no author profile""" with patch('starpunk.author_discovery.get_author_profile', return_value=None): response = client.get('/') parsed = mf2py.parse(doc=response.data.decode(), url='http://localhost/') rels = parsed.get('rels', {}) # Should not have rel=me, or it should be empty assert len(rels.get('me', [])) == 0, "Should not have rel=me without author"