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:
@@ -287,6 +287,17 @@ def handle_create(data: dict, token_info: dict):
|
|||||||
"insufficient_scope", "Token lacks create scope", status_code=403
|
"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
|
# Normalize and extract properties
|
||||||
try:
|
try:
|
||||||
properties = normalize_properties(data)
|
properties = normalize_properties(data)
|
||||||
@@ -295,14 +306,6 @@ def handle_create(data: dict, token_info: dict):
|
|||||||
tags = extract_tags(properties)
|
tags = extract_tags(properties)
|
||||||
published_date = extract_published_date(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:
|
except MicropubValidationError as e:
|
||||||
raise e
|
raise e
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -188,6 +188,64 @@ def test_micropub_create_with_categories(client, app, mock_valid_token):
|
|||||||
assert 'Location' in response.headers
|
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
|
# Query Tests
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user