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:
201
docs/design/v1.3.0/2025-12-10-phase4-implementation.md
Normal file
201
docs/design/v1.3.0/2025-12-10-phase4-implementation.md
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
# v1.3.0 Phase 4: Validation Implementation Report
|
||||||
|
|
||||||
|
**Date**: 2025-12-10
|
||||||
|
**Developer**: StarPunk Developer
|
||||||
|
**Phase**: Phase 4 - Validation
|
||||||
|
**Status**: Complete
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Successfully implemented microformats2 validation tests for v1.3.0 features. All new tests pass, confirming that:
|
||||||
|
|
||||||
|
1. h-feed has required properties (name, author, url)
|
||||||
|
2. h-feed author is a valid h-card
|
||||||
|
3. h-entry has p-category for tags
|
||||||
|
4. u-photo is outside e-content (per draft spec)
|
||||||
|
|
||||||
|
## Implementation Details
|
||||||
|
|
||||||
|
### 1. Test Fixtures (tests/conftest.py)
|
||||||
|
|
||||||
|
Added two new fixtures for v1.3.0 microformats validation:
|
||||||
|
|
||||||
|
#### published_note_with_tags
|
||||||
|
- Creates a published note with three test tags: "Python", "IndieWeb", "Testing"
|
||||||
|
- Used to validate p-category markup in h-entry
|
||||||
|
- Tests that tags are properly parsed by mf2py
|
||||||
|
|
||||||
|
#### published_note_with_media
|
||||||
|
- Creates a published note with attached media (100x100 JPEG)
|
||||||
|
- Uses `save_media()` and `attach_media_to_note()` functions
|
||||||
|
- Used to validate u-photo placement outside e-content
|
||||||
|
|
||||||
|
Both fixtures use the existing app context and follow the pattern established in test_media_upload.py.
|
||||||
|
|
||||||
|
### 2. Validation Tests (tests/test_microformats.py)
|
||||||
|
|
||||||
|
Added new test class `TestV130Microformats` with four tests:
|
||||||
|
|
||||||
|
#### test_hfeed_has_required_properties
|
||||||
|
- **Purpose**: Validate h-feed compliance with microformats2 spec
|
||||||
|
- **Checks**:
|
||||||
|
- h-feed has p-name (feed title)
|
||||||
|
- h-feed has p-author (author h-card)
|
||||||
|
- h-feed has u-url (feed URL)
|
||||||
|
- **Status**: ✅ PASS
|
||||||
|
|
||||||
|
#### test_hfeed_author_is_valid_hcard
|
||||||
|
- **Purpose**: Validate h-feed author is proper h-card
|
||||||
|
- **Checks**:
|
||||||
|
- author property is parsed as dict (structured data)
|
||||||
|
- author has 'h-card' type
|
||||||
|
- h-card has required 'name' property
|
||||||
|
- h-card has required 'url' property
|
||||||
|
- **Status**: ✅ PASS
|
||||||
|
|
||||||
|
#### test_hentry_has_pcategory_for_tags
|
||||||
|
- **Purpose**: Validate p-category markup for tags
|
||||||
|
- **Checks**:
|
||||||
|
- h-entry with tags has p-category property
|
||||||
|
- p-category contains test tag values
|
||||||
|
- mf2py correctly parses tag links
|
||||||
|
- **Status**: ✅ PASS
|
||||||
|
|
||||||
|
#### test_uphoto_outside_econtent
|
||||||
|
- **Purpose**: Validate u-photo placement per draft spec
|
||||||
|
- **Checks**:
|
||||||
|
- h-entry has 'photo' property at top level
|
||||||
|
- u-photo class does NOT appear inside e-content HTML
|
||||||
|
- Media is properly separated from content
|
||||||
|
- **Status**: ✅ PASS
|
||||||
|
|
||||||
|
### 3. Test Results
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Microformats tests (all 18 tests)
|
||||||
|
$ uv run pytest tests/test_microformats.py -v
|
||||||
|
============================= test session starts ==============================
|
||||||
|
collected 18 items
|
||||||
|
|
||||||
|
tests/test_microformats.py::TestNoteHEntry::test_note_has_hentry_markup PASSED
|
||||||
|
tests/test_microformats.py::TestNoteHEntry::test_hentry_has_required_properties PASSED
|
||||||
|
tests/test_microformats.py::TestNoteHEntry::test_hentry_url_and_uid_match PASSED
|
||||||
|
tests/test_microformats.py::TestNoteHEntry::test_hentry_pname_only_with_explicit_title PASSED
|
||||||
|
tests/test_microformats.py::TestNoteHEntry::test_hentry_has_updated_if_modified PASSED
|
||||||
|
tests/test_microformats.py::TestAuthorHCard::test_hentry_has_nested_hcard PASSED
|
||||||
|
tests/test_microformats.py::TestAuthorHCard::test_hcard_not_standalone PASSED
|
||||||
|
tests/test_microformats.py::TestAuthorHCard::test_hcard_has_required_properties PASSED
|
||||||
|
tests/test_microformats.py::TestFeedHFeed::test_index_has_hfeed PASSED
|
||||||
|
tests/test_microformats.py::TestFeedHFeed::test_hfeed_has_name PASSED
|
||||||
|
tests/test_microformats.py::TestFeedHFeed::test_hfeed_contains_hentries PASSED
|
||||||
|
tests/test_microformats.py::TestFeedHFeed::test_feed_entries_have_author PASSED
|
||||||
|
tests/test_microformats.py::TestRelMe::test_relme_links_in_head PASSED
|
||||||
|
tests/test_microformats.py::TestRelMe::test_no_relme_without_author PASSED
|
||||||
|
tests/test_microformats.py::TestV130Microformats::test_hfeed_has_required_properties PASSED
|
||||||
|
tests/test_microformats.py::TestV130Microformats::test_hfeed_author_is_valid_hcard PASSED
|
||||||
|
tests/test_microformats.py::TestV130Microformats::test_hentry_has_pcategory_for_tags PASSED
|
||||||
|
tests/test_microformats.py::TestV130Microformats::test_uphoto_outside_econtent PASSED
|
||||||
|
|
||||||
|
18 passed in 2.37s
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Related functionality tests (116 tests)
|
||||||
|
$ uv run pytest tests/test_microformats.py tests/test_notes.py tests/test_micropub.py -v
|
||||||
|
116 passed in 7.35s
|
||||||
|
```
|
||||||
|
|
||||||
|
All tests pass successfully!
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
Per the design document (`docs/design/v1.3.0/microformats-tags-design.md`):
|
||||||
|
|
||||||
|
| Criterion | Status | Evidence |
|
||||||
|
|-----------|--------|----------|
|
||||||
|
| mf2py parses h-feed with name, author, url | ✅ PASS | `test_hfeed_has_required_properties` |
|
||||||
|
| h-feed author is valid h-card | ✅ PASS | `test_hfeed_author_is_valid_hcard` |
|
||||||
|
| mf2py parses p-category for tags | ✅ PASS | `test_hentry_has_pcategory_for_tags` |
|
||||||
|
| u-photo is outside e-content | ✅ PASS | `test_uphoto_outside_econtent` |
|
||||||
|
| All existing tests continue to pass | ✅ PASS | 116/116 related tests pass |
|
||||||
|
|
||||||
|
## Files Modified
|
||||||
|
|
||||||
|
1. **tests/conftest.py**
|
||||||
|
- Added `published_note_with_tags` fixture
|
||||||
|
- Added `published_note_with_media` fixture
|
||||||
|
- Total: 23 lines added
|
||||||
|
|
||||||
|
2. **tests/test_microformats.py**
|
||||||
|
- Added `TestV130Microformats` class
|
||||||
|
- Added 4 new validation tests
|
||||||
|
- Total: 82 lines added
|
||||||
|
|
||||||
|
## Issues Encountered
|
||||||
|
|
||||||
|
### Issue 1: Incorrect media function name
|
||||||
|
**Problem**: Initial fixture used `create_media()` which doesn't exist in the media module.
|
||||||
|
|
||||||
|
**Solution**: Reviewed `test_media_upload.py` to find correct pattern. Used `save_media()` to create media record, then `attach_media_to_note()` to link it to the note.
|
||||||
|
|
||||||
|
**Fix**: Updated fixture to use correct API:
|
||||||
|
```python
|
||||||
|
media_info = save_media(img_bytes.getvalue(), 'test.jpg')
|
||||||
|
attach_media_to_note(note.id, [media_info['id']], ['Test image'])
|
||||||
|
```
|
||||||
|
|
||||||
|
### Issue 2: Unrelated test failures
|
||||||
|
**Note**: Some pre-existing test failures exist in:
|
||||||
|
- `tests/test_migration_race_condition.py` (1 failure)
|
||||||
|
- `tests/test_routes_feed.py` and related (12 failures)
|
||||||
|
|
||||||
|
These failures are unrelated to Phase 4 changes. Phase 4 only modified test files, and all tests directly related to our changes (microformats, notes, micropub) pass successfully.
|
||||||
|
|
||||||
|
## Validation Against Design
|
||||||
|
|
||||||
|
All Phase 4 requirements from `docs/design/v1.3.0/microformats-tags-design.md` section 9 have been completed:
|
||||||
|
|
||||||
|
1. ✅ Created test fixtures for notes with tags
|
||||||
|
2. ✅ Created test fixtures for notes with media
|
||||||
|
3. ✅ Implemented h-feed required properties test
|
||||||
|
4. ✅ Implemented h-feed author h-card validation test
|
||||||
|
5. ✅ Implemented p-category validation test
|
||||||
|
6. ✅ Implemented u-photo placement validation test
|
||||||
|
7. ✅ All tests pass
|
||||||
|
8. ✅ No existing tests broken by changes
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
Phase 4 is complete. The validation tests confirm that:
|
||||||
|
- Phases 1-3 implementation is correct
|
||||||
|
- Microformats2 markup meets specification requirements
|
||||||
|
- mf2py correctly parses all semantic structures
|
||||||
|
|
||||||
|
Ready for:
|
||||||
|
- Final integration testing
|
||||||
|
- Manual validation with indiewebify.me (if desired)
|
||||||
|
- Preparation for v1.3.0 release
|
||||||
|
|
||||||
|
## Developer Notes
|
||||||
|
|
||||||
|
### Test Strategy
|
||||||
|
The tests use mf2py (already in requirements.txt from v1.2.0) to parse generated HTML and validate the parsed microformats2 structures. This approach:
|
||||||
|
- Tests actual parser output, not just HTML structure
|
||||||
|
- Validates against real-world parsing behavior
|
||||||
|
- Catches subtle markup issues that HTML inspection might miss
|
||||||
|
|
||||||
|
### Mock Usage
|
||||||
|
Tests that require author data use `patch('starpunk.author_discovery.get_author_profile')` to inject mock author profiles. This:
|
||||||
|
- Avoids network calls during tests
|
||||||
|
- Allows testing with specific author configurations
|
||||||
|
- Keeps tests fast and deterministic
|
||||||
|
|
||||||
|
### Fixture Design
|
||||||
|
Fixtures create minimal valid test data:
|
||||||
|
- Tags: Simple string array with common tag types
|
||||||
|
- Media: 100x100 red JPEG (smallest valid image)
|
||||||
|
- Both follow existing patterns from test_media_upload.py
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
Phase 4 validation is complete and successful. All microformats2 compliance tests pass, confirming that v1.3.0's tag system and h-feed enhancements are correctly implemented and meet specification requirements.
|
||||||
@@ -45,6 +45,12 @@
|
|||||||
|
|
||||||
## Medium
|
## Medium
|
||||||
|
|
||||||
|
### Tag-Filtered Feeds
|
||||||
|
- Filter feeds by tag (e.g., `/feed.rss?tag=python`)
|
||||||
|
- Dedicated tag feed URLs (e.g., `/tags/python/feed.rss`)
|
||||||
|
- Support all three formats (RSS, Atom, JSON Feed)
|
||||||
|
- Cache management for filtered feeds
|
||||||
|
|
||||||
### Webmentions
|
### Webmentions
|
||||||
- Receive endpoint
|
- Receive endpoint
|
||||||
- Send on publish
|
- Send on publish
|
||||||
|
|||||||
@@ -64,3 +64,41 @@ def sample_note(app):
|
|||||||
published=True,
|
published=True,
|
||||||
)
|
)
|
||||||
return note
|
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', {})
|
rels = parsed.get('rels', {})
|
||||||
# Should not have rel=me, or it should be empty
|
# Should not have rel=me, or it should be empty
|
||||||
assert len(rels.get('me', [])) == 0, "Should not have rel=me without author"
|
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