feat: Complete Phase 2.4 - HTTP Content Negotiation
Implements HTTP content negotiation for feed format selection. Phase 2.4 Deliverables: - Content negotiation via Accept header parsing - Quality factor support (q= parameter) - 5 feed endpoints with format routing - 406 Not Acceptable responses with helpful errors - Comprehensive test coverage (63 tests) Endpoints: - /feed - Content negotiation based on Accept header - /feed.rss - Explicit RSS 2.0 - /feed.atom - Explicit ATOM 1.0 - /feed.json - Explicit JSON Feed 1.1 - /feed.xml - Backward compatibility (→ RSS) MIME Type Mapping: - application/rss+xml → RSS 2.0 - application/atom+xml → ATOM 1.0 - application/feed+json or application/json → JSON Feed 1.1 - */* → RSS 2.0 (default) Implementation: - Simple quality factor parsing (StarPunk philosophy) - Not full RFC 7231 compliance (minimal approach) - Reuses existing feed generators - No breaking changes Quality Metrics: - 132/132 tests passing (100%) - Zero breaking changes - Full backward compatibility - Standards compliant negotiation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
280
tests/test_feeds_negotiation.py
Normal file
280
tests/test_feeds_negotiation.py
Normal file
@@ -0,0 +1,280 @@
|
||||
"""
|
||||
Tests for feed content negotiation
|
||||
|
||||
This module tests the content negotiation functionality for determining
|
||||
which feed format to serve based on HTTP Accept headers.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from starpunk.feeds.negotiation import (
|
||||
negotiate_feed_format,
|
||||
get_mime_type,
|
||||
_parse_accept_header,
|
||||
_score_format,
|
||||
MIME_TYPES,
|
||||
)
|
||||
|
||||
|
||||
class TestParseAcceptHeader:
|
||||
"""Tests for Accept header parsing"""
|
||||
|
||||
def test_single_type(self):
|
||||
"""Parse single media type without quality"""
|
||||
result = _parse_accept_header('application/json')
|
||||
assert result == [('application/json', 1.0)]
|
||||
|
||||
def test_multiple_types(self):
|
||||
"""Parse multiple media types"""
|
||||
result = _parse_accept_header('application/json, text/html')
|
||||
assert len(result) == 2
|
||||
assert ('application/json', 1.0) in result
|
||||
assert ('text/html', 1.0) in result
|
||||
|
||||
def test_quality_factors(self):
|
||||
"""Parse quality factors correctly"""
|
||||
result = _parse_accept_header('application/json;q=0.9, text/html;q=0.8')
|
||||
assert result == [('application/json', 0.9), ('text/html', 0.8)]
|
||||
|
||||
def test_quality_sorting(self):
|
||||
"""Media types sorted by quality (highest first)"""
|
||||
result = _parse_accept_header('text/html;q=0.5, application/json;q=0.9')
|
||||
assert result[0] == ('application/json', 0.9)
|
||||
assert result[1] == ('text/html', 0.5)
|
||||
|
||||
def test_default_quality_1_0(self):
|
||||
"""Media type without quality defaults to 1.0"""
|
||||
result = _parse_accept_header('application/json;q=0.8, text/html')
|
||||
assert result[0] == ('text/html', 1.0)
|
||||
assert result[1] == ('application/json', 0.8)
|
||||
|
||||
def test_wildcard(self):
|
||||
"""Parse wildcard */* correctly"""
|
||||
result = _parse_accept_header('*/*')
|
||||
assert result == [('*/*', 1.0)]
|
||||
|
||||
def test_wildcard_with_quality(self):
|
||||
"""Parse wildcard with quality factor"""
|
||||
result = _parse_accept_header('application/json, */*;q=0.1')
|
||||
assert result == [('application/json', 1.0), ('*/*', 0.1)]
|
||||
|
||||
def test_whitespace_handling(self):
|
||||
"""Handle whitespace around commas and semicolons"""
|
||||
result = _parse_accept_header('application/json ; q=0.9 , text/html')
|
||||
assert len(result) == 2
|
||||
assert ('application/json', 0.9) in result
|
||||
assert ('text/html', 1.0) in result
|
||||
|
||||
def test_empty_string(self):
|
||||
"""Handle empty Accept header"""
|
||||
result = _parse_accept_header('')
|
||||
assert result == []
|
||||
|
||||
def test_invalid_quality(self):
|
||||
"""Invalid quality factor defaults to 1.0"""
|
||||
result = _parse_accept_header('application/json;q=invalid')
|
||||
assert result == [('application/json', 1.0)]
|
||||
|
||||
def test_quality_clamping(self):
|
||||
"""Quality factors clamped to 0-1 range"""
|
||||
result = _parse_accept_header('application/json;q=1.5')
|
||||
assert result == [('application/json', 1.0)]
|
||||
|
||||
def test_type_wildcard(self):
|
||||
"""Parse type wildcard application/* correctly"""
|
||||
result = _parse_accept_header('application/*')
|
||||
assert result == [('application/*', 1.0)]
|
||||
|
||||
|
||||
class TestScoreFormat:
|
||||
"""Tests for format scoring"""
|
||||
|
||||
def test_exact_match(self):
|
||||
"""Exact MIME type match gets full quality"""
|
||||
media_types = [('application/atom+xml', 1.0)]
|
||||
score = _score_format('atom', media_types)
|
||||
assert score == 1.0
|
||||
|
||||
def test_wildcard_match(self):
|
||||
"""Wildcard */* matches any format"""
|
||||
media_types = [('*/*', 0.8)]
|
||||
score = _score_format('rss', media_types)
|
||||
assert score == 0.8
|
||||
|
||||
def test_type_wildcard_match(self):
|
||||
"""Type wildcard application/* matches application types"""
|
||||
media_types = [('application/*', 0.9)]
|
||||
score = _score_format('atom', media_types)
|
||||
assert score == 0.9
|
||||
|
||||
def test_no_match(self):
|
||||
"""No matching media type returns 0"""
|
||||
media_types = [('text/html', 1.0)]
|
||||
score = _score_format('rss', media_types)
|
||||
assert score == 0.0
|
||||
|
||||
def test_best_quality_wins(self):
|
||||
"""Return highest quality among matches"""
|
||||
media_types = [
|
||||
('*/*', 0.5),
|
||||
('application/*', 0.8),
|
||||
('application/rss+xml', 1.0),
|
||||
]
|
||||
score = _score_format('rss', media_types)
|
||||
assert score == 1.0
|
||||
|
||||
def test_invalid_format(self):
|
||||
"""Invalid format name returns 0"""
|
||||
media_types = [('*/*', 1.0)]
|
||||
score = _score_format('invalid', media_types)
|
||||
assert score == 0.0
|
||||
|
||||
|
||||
class TestNegotiateFeedFormat:
|
||||
"""Tests for feed format negotiation"""
|
||||
|
||||
def test_rss_exact_match(self):
|
||||
"""Exact match for RSS"""
|
||||
result = negotiate_feed_format('application/rss+xml', ['rss', 'atom', 'json'])
|
||||
assert result == 'rss'
|
||||
|
||||
def test_atom_exact_match(self):
|
||||
"""Exact match for ATOM"""
|
||||
result = negotiate_feed_format('application/atom+xml', ['rss', 'atom', 'json'])
|
||||
assert result == 'atom'
|
||||
|
||||
def test_json_feed_exact_match(self):
|
||||
"""Exact match for JSON Feed"""
|
||||
result = negotiate_feed_format('application/feed+json', ['rss', 'atom', 'json'])
|
||||
assert result == 'json'
|
||||
|
||||
def test_json_generic_match(self):
|
||||
"""Generic application/json matches JSON Feed"""
|
||||
result = negotiate_feed_format('application/json', ['rss', 'atom', 'json'])
|
||||
assert result == 'json'
|
||||
|
||||
def test_wildcard_defaults_to_rss(self):
|
||||
"""Wildcard */* defaults to RSS"""
|
||||
result = negotiate_feed_format('*/*', ['rss', 'atom', 'json'])
|
||||
assert result == 'rss'
|
||||
|
||||
def test_quality_factor_selection(self):
|
||||
"""Higher quality factor wins"""
|
||||
result = negotiate_feed_format(
|
||||
'application/atom+xml;q=0.9, application/rss+xml;q=0.5',
|
||||
['rss', 'atom', 'json']
|
||||
)
|
||||
assert result == 'atom'
|
||||
|
||||
def test_tie_prefers_rss(self):
|
||||
"""On quality tie, prefer RSS"""
|
||||
result = negotiate_feed_format(
|
||||
'application/atom+xml;q=0.9, application/rss+xml;q=0.9',
|
||||
['rss', 'atom', 'json']
|
||||
)
|
||||
assert result == 'rss'
|
||||
|
||||
def test_tie_prefers_atom_over_json(self):
|
||||
"""On quality tie, prefer ATOM over JSON"""
|
||||
result = negotiate_feed_format(
|
||||
'application/atom+xml;q=0.9, application/feed+json;q=0.9',
|
||||
['atom', 'json']
|
||||
)
|
||||
assert result == 'atom'
|
||||
|
||||
def test_no_acceptable_format_raises(self):
|
||||
"""No acceptable format raises ValueError"""
|
||||
with pytest.raises(ValueError, match="No acceptable format found"):
|
||||
negotiate_feed_format('text/html', ['rss', 'atom', 'json'])
|
||||
|
||||
def test_only_rss_available(self):
|
||||
"""Negotiate when only RSS is available"""
|
||||
result = negotiate_feed_format('application/rss+xml', ['rss'])
|
||||
assert result == 'rss'
|
||||
|
||||
def test_wildcard_with_limited_formats(self):
|
||||
"""Wildcard picks RSS even if not first in list"""
|
||||
result = negotiate_feed_format('*/*', ['atom', 'json', 'rss'])
|
||||
assert result == 'rss'
|
||||
|
||||
def test_complex_accept_header(self):
|
||||
"""Complex Accept header with multiple types and qualities"""
|
||||
result = negotiate_feed_format(
|
||||
'text/html, application/xhtml+xml, application/xml;q=0.9, */*;q=0.8',
|
||||
['rss', 'atom', 'json']
|
||||
)
|
||||
# application/xml doesn't match, so falls back to */* which gives RSS
|
||||
assert result == 'rss'
|
||||
|
||||
def test_browser_like_accept(self):
|
||||
"""Browser-like Accept header defaults to RSS"""
|
||||
result = negotiate_feed_format(
|
||||
'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||||
['rss', 'atom', 'json']
|
||||
)
|
||||
assert result == 'rss'
|
||||
|
||||
def test_feed_reader_accept(self):
|
||||
"""Feed reader requesting ATOM"""
|
||||
result = negotiate_feed_format(
|
||||
'application/atom+xml, application/rss+xml;q=0.9',
|
||||
['rss', 'atom', 'json']
|
||||
)
|
||||
assert result == 'atom'
|
||||
|
||||
def test_json_api_client(self):
|
||||
"""JSON API client requesting JSON"""
|
||||
result = negotiate_feed_format(
|
||||
'application/json, */*;q=0.1',
|
||||
['rss', 'atom', 'json']
|
||||
)
|
||||
assert result == 'json'
|
||||
|
||||
def test_type_wildcard_application(self):
|
||||
"""application/* matches all feed formats, prefers RSS"""
|
||||
result = negotiate_feed_format(
|
||||
'application/*',
|
||||
['rss', 'atom', 'json']
|
||||
)
|
||||
assert result == 'rss'
|
||||
|
||||
def test_empty_accept_header(self):
|
||||
"""Empty Accept header raises ValueError"""
|
||||
with pytest.raises(ValueError, match="No acceptable format found"):
|
||||
negotiate_feed_format('', ['rss', 'atom', 'json'])
|
||||
|
||||
|
||||
class TestGetMimeType:
|
||||
"""Tests for get_mime_type helper"""
|
||||
|
||||
def test_rss_mime_type(self):
|
||||
"""Get MIME type for RSS"""
|
||||
assert get_mime_type('rss') == 'application/rss+xml'
|
||||
|
||||
def test_atom_mime_type(self):
|
||||
"""Get MIME type for ATOM"""
|
||||
assert get_mime_type('atom') == 'application/atom+xml'
|
||||
|
||||
def test_json_mime_type(self):
|
||||
"""Get MIME type for JSON Feed"""
|
||||
assert get_mime_type('json') == 'application/feed+json'
|
||||
|
||||
def test_invalid_format(self):
|
||||
"""Invalid format raises ValueError"""
|
||||
with pytest.raises(ValueError, match="Unknown format"):
|
||||
get_mime_type('invalid')
|
||||
|
||||
|
||||
class TestMimeTypeConstants:
|
||||
"""Tests for MIME type constant mappings"""
|
||||
|
||||
def test_mime_types_defined(self):
|
||||
"""All expected MIME types are defined"""
|
||||
assert 'rss' in MIME_TYPES
|
||||
assert 'atom' in MIME_TYPES
|
||||
assert 'json' in MIME_TYPES
|
||||
|
||||
def test_mime_type_values(self):
|
||||
"""MIME type values are correct"""
|
||||
assert MIME_TYPES['rss'] == 'application/rss+xml'
|
||||
assert MIME_TYPES['atom'] == 'application/atom+xml'
|
||||
assert MIME_TYPES['json'] == 'application/feed+json'
|
||||
255
tests/test_routes_feeds.py
Normal file
255
tests/test_routes_feeds.py
Normal file
@@ -0,0 +1,255 @@
|
||||
"""
|
||||
Integration tests for feed route endpoints
|
||||
|
||||
Tests the /feed, /feed.rss, /feed.atom, /feed.json, and /feed.xml endpoints
|
||||
including content negotiation.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from starpunk import create_app
|
||||
from starpunk.notes import create_note
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def app(tmp_path):
|
||||
"""Create and configure a test app instance"""
|
||||
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 Site",
|
||||
"SITE_DESCRIPTION": "Test Description",
|
||||
"AUTHOR_NAME": "Test Author",
|
||||
"DEV_MODE": False,
|
||||
"FEED_CACHE_SECONDS": 0, # Disable caching for tests
|
||||
"FEED_MAX_ITEMS": 50,
|
||||
}
|
||||
|
||||
app = create_app(config=test_config)
|
||||
|
||||
# Create test notes
|
||||
with app.app_context():
|
||||
create_note(content='Test content 1', published=True, custom_slug='test-note-1')
|
||||
create_note(content='Test content 2', published=True, custom_slug='test-note-2')
|
||||
|
||||
yield app
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client(app):
|
||||
"""Test client for making requests"""
|
||||
return app.test_client()
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def clear_feed_cache():
|
||||
"""Clear feed cache before each test"""
|
||||
from starpunk.routes import public
|
||||
public._feed_cache["notes"] = None
|
||||
public._feed_cache["timestamp"] = None
|
||||
yield
|
||||
# Clear again after test
|
||||
public._feed_cache["notes"] = None
|
||||
public._feed_cache["timestamp"] = None
|
||||
|
||||
|
||||
class TestExplicitEndpoints:
|
||||
"""Tests for explicit format endpoints"""
|
||||
|
||||
def test_feed_rss_endpoint(self, client):
|
||||
"""GET /feed.rss returns RSS feed"""
|
||||
response = client.get('/feed.rss')
|
||||
assert response.status_code == 200
|
||||
assert response.headers['Content-Type'] == 'application/rss+xml; charset=utf-8'
|
||||
assert b'<?xml version="1.0" encoding="UTF-8"?>' in response.data
|
||||
assert b'<rss version="2.0"' in response.data
|
||||
|
||||
def test_feed_atom_endpoint(self, client):
|
||||
"""GET /feed.atom returns ATOM feed"""
|
||||
response = client.get('/feed.atom')
|
||||
assert response.status_code == 200
|
||||
assert response.headers['Content-Type'] == 'application/atom+xml; charset=utf-8'
|
||||
# Check for XML declaration (encoding may be utf-8 or UTF-8)
|
||||
assert b'<?xml version="1.0"' in response.data
|
||||
assert b'<feed xmlns="http://www.w3.org/2005/Atom"' in response.data
|
||||
|
||||
def test_feed_json_endpoint(self, client):
|
||||
"""GET /feed.json returns JSON Feed"""
|
||||
response = client.get('/feed.json')
|
||||
assert response.status_code == 200
|
||||
assert response.headers['Content-Type'] == 'application/feed+json; charset=utf-8'
|
||||
# JSON Feed is streamed, so we need to collect all chunks
|
||||
data = b''.join(response.response)
|
||||
assert b'"version": "https://jsonfeed.org/version/1.1"' in data
|
||||
assert b'"title":' in data
|
||||
|
||||
def test_feed_xml_legacy_endpoint(self, client):
|
||||
"""GET /feed.xml returns RSS feed (backward compatibility)"""
|
||||
response = client.get('/feed.xml')
|
||||
assert response.status_code == 200
|
||||
assert response.headers['Content-Type'] == 'application/rss+xml; charset=utf-8'
|
||||
assert b'<?xml version="1.0" encoding="UTF-8"?>' in response.data
|
||||
assert b'<rss version="2.0"' in response.data
|
||||
|
||||
|
||||
class TestContentNegotiation:
|
||||
"""Tests for /feed content negotiation endpoint"""
|
||||
|
||||
def test_accept_rss(self, client):
|
||||
"""Accept: application/rss+xml returns RSS"""
|
||||
response = client.get('/feed', headers={'Accept': 'application/rss+xml'})
|
||||
assert response.status_code == 200
|
||||
assert response.headers['Content-Type'] == 'application/rss+xml; charset=utf-8'
|
||||
assert b'<rss version="2.0"' in response.data
|
||||
|
||||
def test_accept_atom(self, client):
|
||||
"""Accept: application/atom+xml returns ATOM"""
|
||||
response = client.get('/feed', headers={'Accept': 'application/atom+xml'})
|
||||
assert response.status_code == 200
|
||||
assert response.headers['Content-Type'] == 'application/atom+xml; charset=utf-8'
|
||||
assert b'<feed xmlns="http://www.w3.org/2005/Atom"' in response.data
|
||||
|
||||
def test_accept_json_feed(self, client):
|
||||
"""Accept: application/feed+json returns JSON Feed"""
|
||||
response = client.get('/feed', headers={'Accept': 'application/feed+json'})
|
||||
assert response.status_code == 200
|
||||
assert response.headers['Content-Type'] == 'application/feed+json; charset=utf-8'
|
||||
data = b''.join(response.response)
|
||||
assert b'"version": "https://jsonfeed.org/version/1.1"' in data
|
||||
|
||||
def test_accept_json_generic(self, client):
|
||||
"""Accept: application/json returns JSON Feed"""
|
||||
response = client.get('/feed', headers={'Accept': 'application/json'})
|
||||
assert response.status_code == 200
|
||||
assert response.headers['Content-Type'] == 'application/feed+json; charset=utf-8'
|
||||
data = b''.join(response.response)
|
||||
assert b'"version": "https://jsonfeed.org/version/1.1"' in data
|
||||
|
||||
def test_accept_wildcard(self, client):
|
||||
"""Accept: */* returns RSS (default)"""
|
||||
response = client.get('/feed', headers={'Accept': '*/*'})
|
||||
assert response.status_code == 200
|
||||
assert response.headers['Content-Type'] == 'application/rss+xml; charset=utf-8'
|
||||
assert b'<rss version="2.0"' in response.data
|
||||
|
||||
def test_no_accept_header(self, client):
|
||||
"""No Accept header defaults to RSS"""
|
||||
response = client.get('/feed')
|
||||
assert response.status_code == 200
|
||||
assert response.headers['Content-Type'] == 'application/rss+xml; charset=utf-8'
|
||||
assert b'<rss version="2.0"' in response.data
|
||||
|
||||
def test_quality_factor_atom_wins(self, client):
|
||||
"""Higher quality factor wins"""
|
||||
response = client.get('/feed', headers={
|
||||
'Accept': 'application/atom+xml;q=0.9, application/rss+xml;q=0.5'
|
||||
})
|
||||
assert response.status_code == 200
|
||||
assert response.headers['Content-Type'] == 'application/atom+xml; charset=utf-8'
|
||||
|
||||
def test_quality_factor_json_wins(self, client):
|
||||
"""JSON with highest quality wins"""
|
||||
response = client.get('/feed', headers={
|
||||
'Accept': 'application/json;q=1.0, application/atom+xml;q=0.8'
|
||||
})
|
||||
assert response.status_code == 200
|
||||
assert response.headers['Content-Type'] == 'application/feed+json; charset=utf-8'
|
||||
|
||||
def test_browser_accept_header(self, client):
|
||||
"""Browser-like Accept header returns RSS"""
|
||||
response = client.get('/feed', headers={
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
|
||||
})
|
||||
assert response.status_code == 200
|
||||
assert response.headers['Content-Type'] == 'application/rss+xml; charset=utf-8'
|
||||
|
||||
def test_no_acceptable_format(self, client):
|
||||
"""No acceptable format returns 406"""
|
||||
response = client.get('/feed', headers={'Accept': 'text/html'})
|
||||
assert response.status_code == 406
|
||||
assert response.headers['Content-Type'] == 'text/plain; charset=utf-8'
|
||||
assert 'X-Available-Formats' in response.headers
|
||||
assert 'application/rss+xml' in response.headers['X-Available-Formats']
|
||||
assert 'application/atom+xml' in response.headers['X-Available-Formats']
|
||||
assert 'application/feed+json' in response.headers['X-Available-Formats']
|
||||
assert b'Not Acceptable' in response.data
|
||||
|
||||
|
||||
class TestCacheHeaders:
|
||||
"""Tests for cache control headers"""
|
||||
|
||||
def test_rss_cache_header(self, client):
|
||||
"""RSS feed includes Cache-Control header"""
|
||||
response = client.get('/feed.rss')
|
||||
assert 'Cache-Control' in response.headers
|
||||
# FEED_CACHE_SECONDS is 0 in test config
|
||||
assert 'max-age=0' in response.headers['Cache-Control']
|
||||
|
||||
def test_atom_cache_header(self, client):
|
||||
"""ATOM feed includes Cache-Control header"""
|
||||
response = client.get('/feed.atom')
|
||||
assert 'Cache-Control' in response.headers
|
||||
assert 'max-age=0' in response.headers['Cache-Control']
|
||||
|
||||
def test_json_cache_header(self, client):
|
||||
"""JSON Feed includes Cache-Control header"""
|
||||
response = client.get('/feed.json')
|
||||
assert 'Cache-Control' in response.headers
|
||||
assert 'max-age=0' in response.headers['Cache-Control']
|
||||
|
||||
|
||||
class TestFeedContent:
|
||||
"""Tests for feed content correctness"""
|
||||
|
||||
def test_rss_contains_notes(self, client):
|
||||
"""RSS feed contains test notes"""
|
||||
response = client.get('/feed.rss')
|
||||
assert b'test-note-1' in response.data
|
||||
assert b'test-note-2' in response.data
|
||||
assert b'Test content 1' in response.data
|
||||
assert b'Test content 2' in response.data
|
||||
|
||||
def test_atom_contains_notes(self, client):
|
||||
"""ATOM feed contains test notes"""
|
||||
response = client.get('/feed.atom')
|
||||
assert b'test-note-1' in response.data
|
||||
assert b'test-note-2' in response.data
|
||||
assert b'Test content 1' in response.data
|
||||
assert b'Test content 2' in response.data
|
||||
|
||||
def test_json_contains_notes(self, client):
|
||||
"""JSON Feed contains test notes"""
|
||||
response = client.get('/feed.json')
|
||||
data = b''.join(response.response)
|
||||
assert b'test-note-1' in data
|
||||
assert b'test-note-2' in data
|
||||
assert b'Test content 1' in data
|
||||
assert b'Test content 2' in data
|
||||
|
||||
|
||||
class TestBackwardCompatibility:
|
||||
"""Tests for backward compatibility"""
|
||||
|
||||
def test_feed_xml_same_as_feed_rss(self, client):
|
||||
"""GET /feed.xml returns same content as /feed.rss"""
|
||||
rss_response = client.get('/feed.rss')
|
||||
xml_response = client.get('/feed.xml')
|
||||
|
||||
assert rss_response.status_code == xml_response.status_code
|
||||
assert rss_response.headers['Content-Type'] == xml_response.headers['Content-Type']
|
||||
# Content should be identical
|
||||
assert rss_response.data == xml_response.data
|
||||
|
||||
def test_feed_xml_contains_rss(self, client):
|
||||
"""GET /feed.xml contains RSS XML"""
|
||||
response = client.get('/feed.xml')
|
||||
assert b'<?xml version="1.0" encoding="UTF-8"?>' in response.data
|
||||
assert b'<rss version="2.0"' in response.data
|
||||
assert b'</rss>' in response.data
|
||||
Reference in New Issue
Block a user