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:
2025-12-10 11:51:39 -07:00
parent 372064b116
commit 3d80e1af51
4 changed files with 327 additions and 0 deletions

View File

@@ -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"