""" 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'