""" Tests for Micropub endpoint Tests the /micropub endpoint for creating posts via IndieWeb clients. Covers both form-encoded and JSON requests, authentication, and error handling. Note: After Phase 4 (ADR-030), StarPunk no longer issues tokens. Tests mock external token verification responses. """ import pytest from unittest.mock import patch from starpunk.notes import get_note # Mock token verification responses @pytest.fixture def mock_valid_token(): """Mock response from external token verification (valid token)""" def verify_token(token): if token == "valid_token": return { "me": "https://user.example", "client_id": "https://client.example", "scope": "create" } return None return verify_token @pytest.fixture def mock_invalid_token(): """Mock response from external token verification (invalid token)""" def verify_token(token): return None return verify_token @pytest.fixture def mock_read_only_token(): """Mock response for token without create scope""" def verify_token(token): if token == "read_only_token": return { "me": "https://user.example", "client_id": "https://client.example", "scope": "read" } return None return verify_token # Authentication Tests def test_micropub_no_token(client): """Test Micropub endpoint rejects requests without token""" response = client.post('/micropub', data={ 'h': 'entry', 'content': 'Test post' }) assert response.status_code == 401 data = response.get_json() assert data['error'] == 'unauthorized' assert 'No access token' in data['error_description'] def test_micropub_invalid_token(client, mock_invalid_token): """Test Micropub endpoint rejects invalid tokens""" with patch('starpunk.routes.micropub.verify_external_token', mock_invalid_token): response = client.post( '/micropub', data={'h': 'entry', 'content': 'Test post'}, headers={'Authorization': 'Bearer invalid_token'} ) assert response.status_code == 401 data = response.get_json() assert data['error'] == 'unauthorized' assert 'Invalid or expired' in data['error_description'] def test_micropub_insufficient_scope(client, mock_read_only_token): """Test Micropub endpoint rejects token without create scope""" with patch('starpunk.routes.micropub.verify_external_token', mock_read_only_token): response = client.post( '/micropub', data={'h': 'entry', 'content': 'Test post'}, headers={'Authorization': 'Bearer read_only_token'} ) assert response.status_code == 403 data = response.get_json() assert data['error'] == 'insufficient_scope' # Create Post Tests def test_micropub_create_note_form(client, app, mock_valid_token): """Test creating a note 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 note from Micropub', }, headers={'Authorization': 'Bearer valid_token'} ) assert response.status_code == 201 assert 'Location' in response.headers # Verify note was created location = response.headers['Location'] slug = location.split('/')[-1] with app.app_context(): note = get_note(slug) assert note is not None assert note.content == 'This is a test note from Micropub' assert note.published is True def test_micropub_create_note_json(client, app, mock_valid_token): """Test creating a note 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 note'] } }, headers={'Authorization': 'Bearer valid_token'} ) assert response.status_code == 201 assert 'Location' in response.headers location = response.headers['Location'] slug = location.split('/')[-1] with app.app_context(): note = get_note(slug) assert note.content == 'JSON test note' def test_micropub_create_with_name(client, app, mock_valid_token): """Test creating a note with a title (name property)""" with patch('starpunk.routes.micropub.verify_external_token', mock_valid_token): response = client.post( '/micropub', json={ 'type': ['h-entry'], 'properties': { 'name': ['My Test Title'], 'content': ['Content goes here'] } }, headers={'Authorization': 'Bearer valid_token'} ) # Verify note was created successfully assert response.status_code == 201 assert 'Location' in response.headers def test_micropub_create_with_categories(client, app, mock_valid_token): """Test creating a note with tags (category property)""" with patch('starpunk.routes.micropub.verify_external_token', mock_valid_token): response = client.post( '/micropub', json={ 'type': ['h-entry'], 'properties': { 'content': ['Tagged post'], 'category': ['test', 'micropub', 'indieweb'] } }, headers={'Authorization': 'Bearer valid_token'} ) # Verify note was created successfully assert response.status_code == 201 assert 'Location' in response.headers # Query Tests def test_micropub_query_config(client, mock_valid_token): """Test q=config endpoint""" with patch('starpunk.routes.micropub.verify_external_token', mock_valid_token): response = client.get( '/micropub?q=config', headers={'Authorization': 'Bearer valid_token'} ) assert response.status_code == 200 data = response.get_json() # Config endpoint returns server capabilities # Check that it's a valid config response assert isinstance(data, dict) def test_micropub_query_source(client, mock_valid_token): """Test q=source endpoint""" with patch('starpunk.routes.micropub.verify_external_token', mock_valid_token): # First create a note create_response = client.post( '/micropub', json={ 'type': ['h-entry'], 'properties': { 'content': ['Source test'] } }, headers={'Authorization': 'Bearer valid_token'} ) location = create_response.headers['Location'] # Query for source response = client.get( f'/micropub?q=source&url={location}', headers={'Authorization': 'Bearer valid_token'} ) assert response.status_code == 200 data = response.get_json() assert 'properties' in data assert data['properties']['content'][0] == 'Source test' # Error Handling Tests def test_micropub_missing_content(client, mock_valid_token): """Test creating note without content fails""" with patch('starpunk.routes.micropub.verify_external_token', mock_valid_token): response = client.post( '/micropub', json={ 'type': ['h-entry'], 'properties': {} }, headers={'Authorization': 'Bearer valid_token'} ) assert response.status_code == 400 data = response.get_json() assert data['error'] == 'invalid_request' def test_micropub_unsupported_action(client, mock_valid_token): """Test unsupported actions (update, delete) return error""" with patch('starpunk.routes.micropub.verify_external_token', mock_valid_token): # Test update response = client.post( '/micropub', json={ 'action': 'update', 'url': 'https://example.com/note/123' }, headers={'Authorization': 'Bearer valid_token'} ) assert response.status_code == 400 data = response.get_json() assert 'not supported' in data['error_description'] # Test delete response = client.post( '/micropub', json={ 'action': 'delete', 'url': 'https://example.com/note/123' }, headers={'Authorization': 'Bearer valid_token'} ) assert response.status_code == 400 data = response.get_json() assert 'not supported' in data['error_description']