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
|
||||
)
|
||||
|
||||
# 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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user