""" Tests for JSON Feed generation module Tests cover: - JSON Feed generation with various note counts - RFC 3339 date formatting - Feed structure and required fields - Entry ordering (newest first) - JSON validity """ import pytest from datetime import datetime, timezone import json import time from starpunk import create_app from starpunk.feeds.json_feed import generate_json_feed, generate_json_feed_streaming from starpunk.notes import create_note, list_notes from tests.helpers.feed_ordering import assert_feed_newest_first @pytest.fixture def app(tmp_path): """Create test application""" test_data_dir = tmp_path / "data" test_data_dir.mkdir(parents=True, exist_ok=True) test_config = { "TESTING": True, "DATABASE_PATH": test_data_dir / "starpunk.db", "DATA_PATH": test_data_dir, "NOTES_PATH": test_data_dir / "notes", "SESSION_SECRET": "test-secret-key", "ADMIN_ME": "https://test.example.com", "SITE_URL": "https://example.com", "SITE_NAME": "Test Blog", "SITE_DESCRIPTION": "A test blog", "DEV_MODE": False, } app = create_app(config=test_config) yield app @pytest.fixture def sample_notes(app): """Create sample published notes""" with app.app_context(): notes = [] for i in range(5): note = create_note( content=f"# Test Note {i}\n\nThis is test content for note {i}.", published=True, ) notes.append(note) time.sleep(0.01) # Ensure distinct timestamps return list_notes(published_only=True, limit=10) class TestGenerateJsonFeed: """Test generate_json_feed() function""" def test_generate_json_feed_basic(self, app, sample_notes): """Test basic JSON Feed generation with notes""" with app.app_context(): feed_json = generate_json_feed( site_url="https://example.com", site_name="Test Blog", site_description="A test blog", notes=sample_notes, ) # Should return JSON string assert isinstance(feed_json, str) # Parse JSON to verify structure feed = json.loads(feed_json) # Check required fields assert feed["version"] == "https://jsonfeed.org/version/1.1" assert feed["title"] == "Test Blog" assert "items" in feed assert isinstance(feed["items"], list) # Check items (should have 5 items) assert len(feed["items"]) == 5 def test_generate_json_feed_empty(self, app): """Test JSON Feed generation with no notes""" with app.app_context(): feed_json = generate_json_feed( site_url="https://example.com", site_name="Test Blog", site_description="A test blog", notes=[], ) # Should still generate valid JSON feed = json.loads(feed_json) assert feed["items"] == [] def test_generate_json_feed_respects_limit(self, app, sample_notes): """Test JSON Feed respects item limit""" with app.app_context(): feed_json = generate_json_feed( site_url="https://example.com", site_name="Test Blog", site_description="A test blog", notes=sample_notes, limit=3, ) feed = json.loads(feed_json) # Should only have 3 items (respecting limit) assert len(feed["items"]) == 3 def test_generate_json_feed_newest_first(self, app): """Test JSON Feed displays notes in newest-first order""" with app.app_context(): # Create notes with distinct timestamps for i in range(3): create_note( content=f"# Note {i}\n\nContent {i}.", published=True, ) time.sleep(0.01) # Get notes from database (should be DESC = newest first) notes = list_notes(published_only=True, limit=10) # Generate feed feed_json = generate_json_feed( site_url="https://example.com", site_name="Test Blog", site_description="A test blog", notes=notes, ) # Use shared helper to verify ordering assert_feed_newest_first(feed_json, format_type='json', expected_count=3) # Also verify manually with JSON parsing feed = json.loads(feed_json) items = feed["items"] # First item should be newest (Note 2) # Last item should be oldest (Note 0) assert "Note 2" in items[0]["title"] assert "Note 0" in items[-1]["title"] def test_generate_json_feed_requires_site_url(self): """Test JSON Feed generation requires site_url""" with pytest.raises(ValueError, match="site_url is required"): generate_json_feed( site_url="", site_name="Test Blog", site_description="A test blog", notes=[], ) def test_generate_json_feed_requires_site_name(self): """Test JSON Feed generation requires site_name""" with pytest.raises(ValueError, match="site_name is required"): generate_json_feed( site_url="https://example.com", site_name="", site_description="A test blog", notes=[], ) def test_generate_json_feed_item_structure(self, app, sample_notes): """Test individual JSON Feed item has all required fields""" with app.app_context(): feed_json = generate_json_feed( site_url="https://example.com", site_name="Test Blog", site_description="A test blog", notes=sample_notes[:1], ) feed = json.loads(feed_json) item = feed["items"][0] # Check required item fields assert "id" in item assert "url" in item assert "title" in item assert "date_published" in item # Check either content_html or content_text is present assert "content_html" in item or "content_text" in item def test_generate_json_feed_html_content(self, app): """Test JSON Feed includes HTML content""" with app.app_context(): note = create_note( content="# Test\n\nThis is **bold** and *italic*.", published=True, ) feed_json = generate_json_feed( site_url="https://example.com", site_name="Test Blog", site_description="A test blog", notes=[note], ) feed = json.loads(feed_json) item = feed["items"][0] # Should have content_html assert "content_html" in item content = item["content_html"] # Should contain HTML tags assert "" in content or "" in content def test_generate_json_feed_starpunk_extension(self, app, sample_notes): """Test JSON Feed includes StarPunk custom extension""" with app.app_context(): feed_json = generate_json_feed( site_url="https://example.com", site_name="Test Blog", site_description="A test blog", notes=sample_notes[:1], ) feed = json.loads(feed_json) item = feed["items"][0] # Should have _starpunk extension assert "_starpunk" in item assert "permalink_path" in item["_starpunk"] assert "word_count" in item["_starpunk"] def test_generate_json_feed_date_format(self, app, sample_notes): """Test JSON Feed uses RFC 3339 date format""" with app.app_context(): feed_json = generate_json_feed( site_url="https://example.com", site_name="Test Blog", site_description="A test blog", notes=sample_notes[:1], ) feed = json.loads(feed_json) item = feed["items"][0] # date_published should be in RFC 3339 format date_str = item["date_published"] # Should end with 'Z' for UTC or have timezone offset assert date_str.endswith("Z") or "+" in date_str or "-" in date_str[-6:] # Should be parseable as ISO 8601 parsed = datetime.fromisoformat(date_str.replace("Z", "+00:00")) assert parsed.tzinfo is not None class TestGenerateJsonFeedStreaming: """Test generate_json_feed_streaming() function""" def test_generate_json_feed_streaming_basic(self, app, sample_notes): """Test streaming JSON Feed generation""" with app.app_context(): generator = generate_json_feed_streaming( site_url="https://example.com", site_name="Test Blog", site_description="A test blog", notes=sample_notes, ) # Collect all chunks chunks = list(generator) assert len(chunks) > 0 # Join and verify valid JSON feed_json = ''.join(chunks) feed = json.loads(feed_json) assert len(feed["items"]) == 5 def test_generate_json_feed_streaming_yields_chunks(self, app, sample_notes): """Test streaming yields multiple chunks""" with app.app_context(): generator = generate_json_feed_streaming( site_url="https://example.com", site_name="Test Blog", site_description="A test blog", notes=sample_notes, limit=3, ) chunks = list(generator) # Should have multiple chunks (at least opening + items + closing) assert len(chunks) >= 3 def test_generate_json_feed_streaming_valid_json(self, app, sample_notes): """Test streaming produces valid JSON""" with app.app_context(): generator = generate_json_feed_streaming( site_url="https://example.com", site_name="Test Blog", site_description="A test blog", notes=sample_notes, ) feed_json = ''.join(generator) # Should be valid JSON feed = json.loads(feed_json) assert feed["version"] == "https://jsonfeed.org/version/1.1"