test(microformats): Add v1.3.0 validation tests for tags and h-feed
Phase 4: Validation per microformats-tags-design.md Added test fixtures: - published_note_with_tags: Creates note with test tags for p-category validation - published_note_with_media: Creates note with media for u-photo placement testing Added v1.3.0 microformats2 validation tests: - test_hfeed_has_required_properties: Validates name, author, url per spec - test_hfeed_author_is_valid_hcard: Validates h-card structure - test_hentry_has_pcategory_for_tags: Validates p-category markup - test_uphoto_outside_econtent: Validates u-photo placement per draft spec Test results: - All 18 microformats tests pass - All 116 related tests pass (microformats, notes, micropub) - Confirms Phases 1-3 implementation correctness Updated BACKLOG.md with tag-filtered feeds feature (medium priority) Implementation report: docs/design/v1.3.0/2025-12-10-phase4-implementation.md Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -64,3 +64,41 @@ def sample_note(app):
|
||||
published=True,
|
||||
)
|
||||
return note
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def published_note_with_tags(app):
|
||||
"""Create a published note with tags for microformats testing (v1.3.0)"""
|
||||
from starpunk.notes import create_note
|
||||
|
||||
with app.app_context():
|
||||
note = create_note(
|
||||
content="Test note with tags for microformats validation",
|
||||
published=True,
|
||||
tags=["Python", "IndieWeb", "Testing"]
|
||||
)
|
||||
return note
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def published_note_with_media(app):
|
||||
"""Create a published note with media for u-photo testing (v1.3.0)"""
|
||||
from starpunk.notes import create_note
|
||||
from starpunk.media import save_media, attach_media_to_note
|
||||
from PIL import Image
|
||||
import io
|
||||
|
||||
with app.app_context():
|
||||
note = create_note(content="Test note with media", published=True)
|
||||
|
||||
# Create minimal valid JPEG for testing
|
||||
img = Image.new('RGB', (100, 100), color='red')
|
||||
img_bytes = io.BytesIO()
|
||||
img.save(img_bytes, format='JPEG')
|
||||
img_bytes.seek(0)
|
||||
|
||||
# Save media and attach to note
|
||||
media_info = save_media(img_bytes.getvalue(), 'test.jpg')
|
||||
attach_media_to_note(note.id, [media_info['id']], ['Test image'])
|
||||
|
||||
return note
|
||||
|
||||
@@ -299,3 +299,85 @@ class TestRelMe:
|
||||
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"
|
||||
|
||||
|
||||
class TestV130Microformats:
|
||||
"""v1.3.0 microformats2 compliance tests"""
|
||||
|
||||
def test_hfeed_has_required_properties(self, client, app):
|
||||
"""h-feed has name, author, url per spec (v1.3.0)"""
|
||||
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('/')
|
||||
|
||||
parsed = mf2py.parse(doc=response.data.decode(), url='http://localhost/')
|
||||
|
||||
hfeed = [i for i in parsed['items'] if 'h-feed' in i.get('type', [])][0]
|
||||
props = hfeed.get('properties', {})
|
||||
|
||||
assert 'name' in props, "h-feed must have p-name"
|
||||
assert 'author' in props, "h-feed must have p-author"
|
||||
assert 'url' in props, "h-feed must have u-url"
|
||||
|
||||
def test_hfeed_author_is_valid_hcard(self, client, app):
|
||||
"""h-feed author is valid h-card (v1.3.0)"""
|
||||
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('/')
|
||||
|
||||
parsed = mf2py.parse(doc=response.data.decode(), url='http://localhost/')
|
||||
|
||||
hfeed = [i for i in parsed['items'] if 'h-feed' in i.get('type', [])][0]
|
||||
author = hfeed.get('properties', {}).get('author', [{}])[0]
|
||||
|
||||
assert isinstance(author, dict), "author should be parsed as dict"
|
||||
assert 'h-card' in author.get('type', []), "author must be h-card"
|
||||
assert 'name' in author.get('properties', {}), "h-card must have name"
|
||||
assert 'url' in author.get('properties', {}), "h-card must have url"
|
||||
|
||||
def test_hentry_has_pcategory_for_tags(self, client, app, published_note_with_tags):
|
||||
"""h-entry has p-category for each tag (v1.3.0)"""
|
||||
response = client.get(f'/note/{published_note_with_tags.slug}')
|
||||
parsed = mf2py.parse(doc=response.data.decode(), url=f'http://localhost/note/{published_note_with_tags.slug}')
|
||||
|
||||
hentry = [i for i in parsed['items'] if 'h-entry' in i.get('type', [])][0]
|
||||
categories = hentry.get('properties', {}).get('category', [])
|
||||
|
||||
assert len(categories) > 0, "h-entry with tags must have p-category"
|
||||
# Check that our test tags are present
|
||||
assert 'Python' in categories or 'python' in categories
|
||||
assert 'IndieWeb' in categories or 'indieweb' in categories
|
||||
|
||||
def test_uphoto_outside_econtent(self, client, app, published_note_with_media):
|
||||
"""u-photo is direct child of h-entry, not inside e-content (v1.3.0)"""
|
||||
response = client.get(f'/note/{published_note_with_media.slug}')
|
||||
parsed = mf2py.parse(doc=response.data.decode(), url=f'http://localhost/note/{published_note_with_media.slug}')
|
||||
|
||||
hentry = [i for i in parsed['items'] if 'h-entry' in i.get('type', [])][0]
|
||||
props = hentry.get('properties', {})
|
||||
|
||||
# u-photo should be at h-entry level
|
||||
assert 'photo' in props, "h-entry with media must have u-photo"
|
||||
|
||||
# Verify it's not nested in e-content
|
||||
content = props.get('content', [{}])[0]
|
||||
if isinstance(content, dict):
|
||||
content_html = content.get('html', '')
|
||||
# u-photo class should NOT appear inside content
|
||||
assert 'u-photo' not in content_html, "u-photo should not be inside e-content"
|
||||
|
||||
Reference in New Issue
Block a user