fix: Extract mp-slug before property normalization

Fix bug where custom slugs (mp-slug) were being ignored because they
were extracted from normalized properties after being filtered out.

The root cause: normalize_properties() filters out all mp-* parameters
(line 139) because they're Micropub server extensions, not properties.
The old code tried to extract mp-slug from the normalized properties
dict, but it had already been removed.

The fix: Extract mp-slug directly from raw request data BEFORE calling
normalize_properties(). This preserves the custom slug through to
create_note().

Changes:
- Move mp-slug extraction to before property normalization (line 290-299)
- Handle both form-encoded (list) and JSON (string or list) formats
- Add comprehensive tests for custom slug with both request formats
- All 13 Micropub tests pass

Fixes the issue reported in production where Quill-specified slugs
were being replaced with auto-generated ones.

References:
- docs/reports/custom-slug-bug-diagnosis.md (architect's analysis)
- Micropub spec: mp-slug is a server extension parameter

Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-25 11:03:28 -07:00
parent 7231d97d3e
commit 894e5e3906
2 changed files with 69 additions and 8 deletions

View File

@@ -287,6 +287,17 @@ def handle_create(data: dict, token_info: dict):
"insufficient_scope", "Token lacks create scope", status_code=403
)
# Extract mp-slug BEFORE normalizing properties (it's not a property!)
# mp-slug is a Micropub server extension parameter that gets filtered during normalization
custom_slug = None
if isinstance(data, dict) and 'mp-slug' in data:
# Handle both form-encoded (list) and JSON (could be string or list)
slug_value = data.get('mp-slug')
if isinstance(slug_value, list) and slug_value:
custom_slug = slug_value[0]
elif isinstance(slug_value, str):
custom_slug = slug_value
# Normalize and extract properties
try:
properties = normalize_properties(data)
@@ -295,14 +306,6 @@ def handle_create(data: dict, token_info: dict):
tags = extract_tags(properties)
published_date = extract_published_date(properties)
# Extract custom slug if provided (Micropub extension)
custom_slug = None
if 'mp-slug' in properties:
# mp-slug is an array in Micropub format
slug_values = properties.get('mp-slug', [])
if slug_values and len(slug_values) > 0:
custom_slug = slug_values[0]
except MicropubValidationError as e:
raise e
except Exception as e:

View File

@@ -188,6 +188,64 @@ def test_micropub_create_with_categories(client, app, mock_valid_token):
assert 'Location' in response.headers
def test_micropub_create_with_custom_slug_form(client, app, mock_valid_token):
"""Test creating a note with custom slug via form-encoded request"""
with patch('starpunk.routes.micropub.verify_external_token', mock_valid_token):
response = client.post(
'/micropub',
data={
'h': 'entry',
'content': 'This is a test for custom slugs',
'mp-slug': 'my-custom-slug'
},
headers={'Authorization': 'Bearer valid_token'}
)
assert response.status_code == 201
assert 'Location' in response.headers
# Verify the custom slug was used
location = response.headers['Location']
assert location.endswith('/notes/my-custom-slug')
# Verify note exists with the custom slug
with app.app_context():
note = get_note('my-custom-slug')
assert note is not None
assert note.slug == 'my-custom-slug'
assert note.content == 'This is a test for custom slugs'
def test_micropub_create_with_custom_slug_json(client, app, mock_valid_token):
"""Test creating a note with custom slug via JSON request"""
with patch('starpunk.routes.micropub.verify_external_token', mock_valid_token):
response = client.post(
'/micropub',
json={
'type': ['h-entry'],
'properties': {
'content': ['JSON test with custom slug']
},
'mp-slug': 'json-custom-slug'
},
headers={'Authorization': 'Bearer valid_token'}
)
assert response.status_code == 201
assert 'Location' in response.headers
# Verify the custom slug was used
location = response.headers['Location']
assert location.endswith('/notes/json-custom-slug')
# Verify note exists with the custom slug
with app.app_context():
note = get_note('json-custom-slug')
assert note is not None
assert note.slug == 'json-custom-slug'
assert note.content == 'JSON test with custom slug'
# Query Tests