accept_headers 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,84 +2,107 @@ require_relative "../spec_helper"
2
2
 
3
3
  describe AcceptHeaders::Language::Negotiator do
4
4
  subject { AcceptHeaders::Language::Negotiator }
5
- let(:language) { AcceptHeaders::Language }
6
5
 
7
- describe "parsing an accept header" do
8
- it "returns a sorted array of media primary tags" do
9
- subject.new("en-*;q=0.2, en-us").list.must_equal [
10
- language.new('en', 'us'),
11
- language.new('en', '*', q: 0.2)
6
+ describe "parsing an Accept-Language header" do
7
+ it "sorts ranges by quality and specificity" do
8
+ subject.new('en;q=0.2, en-us').list.map(&:to_h).must_equal [
9
+ {primary_tag: 'en', subtag: 'us', q: 1.0},
10
+ {primary_tag: 'en', subtag: '*', q: 0.2}
12
11
  ]
13
12
 
14
- subject.new("en-*, en-us, *;q=0.8").list.must_equal [
15
- language.new('en', 'us'),
16
- language.new('en', '*'),
17
- language.new('*', '*', q: 0.8)
18
- ]
13
+ subject.new('en, en-us, *;q=0.8').list.map(&:language_tag).must_equal ['en-us', 'en', '*']
19
14
  end
20
15
 
21
- it "ignores the 'Accept-Language:' prefix" do
22
- subject.new('Accept-Language: en-us').list.must_equal [
23
- language.new('en', 'us')
24
- ]
16
+ it "ignores the Accept-Language prefix" do
17
+ subject.new('Accept-Language: en-us').list.map(&:language_tag).must_equal ['en-us']
25
18
  end
26
19
 
27
- it "sets media primary tag to */* when the accept header is empty" do
28
- subject.new('').list.must_equal [
29
- language.new('*', '*')
30
- ]
20
+ it "accepts any language when the header is missing" do
21
+ subject.new(nil).list.map(&:language_tag).must_equal ['*']
22
+ subject.new(nil).negotiate('en-US').language_tag.must_equal 'en-us'
31
23
  end
32
24
 
33
- it "sets media primary tag to */* when the primary tag is only *" do
34
- subject.new('*').list.must_equal [
35
- language.new('*', '*')
36
- ]
25
+ it "accepts no languages when the header is present and empty" do
26
+ ['', " \t", 'Accept-Language:'].each do |header|
27
+ assert_empty subject.new(header).list
28
+ assert_nil subject.new(header).negotiate('en-US')
29
+ end
37
30
  end
38
31
 
39
- it "defaults q to 1 if it's not explicitly specified" do
40
- subject.new("en-us").list.must_equal [
41
- language.new('en', 'plain', q: 1.0)
32
+ it "parses a wildcard range" do
33
+ subject.new('*').list.map(&:to_h).must_equal [
34
+ {primary_tag: '*', subtag: '*', q: 1.0}
42
35
  ]
43
36
  end
44
37
 
45
- it "strips whitespace from between media primary tags" do
46
- subject.new("\ten-us\r,\nen-gb\s").list.must_equal [
47
- language.new('en', 'us'),
48
- language.new('en', 'gb')
49
- ]
38
+ it "defaults quality to one" do
39
+ subject.new('en-us').list.first.to_h.must_equal({primary_tag: 'en', subtag: 'us', q: 1.0})
50
40
  end
51
41
 
52
- it "strips whitespace around q" do
53
- subject.new("en-us;\tq\r=\n1, en-gb").list.must_equal [
54
- language.new('en', 'us'),
55
- language.new('en', 'gb')
56
- ]
42
+ it "uses the documented fallback quality for an invalid weight" do
43
+ subject.new('en-us;q=x').list.first.to_h.must_equal({primary_tag: 'en', subtag: 'us', q: 0.001})
57
44
  end
58
45
 
59
- it "has a q value of 0.001 when parsed q is invalid" do
60
- subject.new("en-us;q=x").list.must_equal [
61
- language.new('en', 'plain', q: 0.001)
62
- ]
46
+ it "allows optional whitespace around ranges" do
47
+ subject.new("\ten-us , en-gb\t").list.map(&:language_tag).must_equal ['en-us', 'en-gb']
63
48
  end
64
49
 
65
- it "skips invalid media primary tags" do
66
- subject.new("en-us, en-us-omg;q=0.9").list.must_equal [
67
- language.new('en', 'us', q: 1)
50
+ it "retains every subtag and normalizes case" do
51
+ subject.new('ZH-Hant-TW, en-US-omg;q=0.9').list.map(&:to_h).must_equal [
52
+ {primary_tag: 'zh', subtag: 'hant-tw', q: 1.0},
53
+ {primary_tag: 'en', subtag: 'us-omg', q: 0.9}
68
54
  ]
69
55
  end
56
+
57
+ it "skips ranges outside the basic language-range grammar" do
58
+ invalid = ['en-*', '*-US', 'en_US', '123', 'englishxx', 'en-123456789', 'en--US', 'en - US']
59
+ subject.new((invalid + ['fr']).join(',')).list.map(&:language_tag).must_equal ['fr']
60
+ end
70
61
  end
71
62
 
72
63
  describe "#to_s" do
73
- it "returns a string of each language #to_s joined by a comma" do
74
- subject.new("en-us, en-gb;q=0.9").to_s.must_equal "en-us;q=1,en-gb;q=0.9"
64
+ it "serializes basic language ranges without adding wildcards" do
65
+ subject.new('en, zh-Hant-TW;q=0.9').to_s.must_equal 'en;q=1,zh-hant-tw;q=0.9'
75
66
  end
76
67
  end
77
68
 
78
- describe "negotiate supported languages" do
79
- it "returns a best matching language" do
80
- match = language.new('en', 'us')
81
- n = subject.new('en-*, en-us, *;q=0.8')
82
- n.negotiate('en-us').must_equal language.new('en', 'us')
69
+ describe "negotiating supported languages" do
70
+ it "returns the supported language rather than the matching range" do
71
+ match = subject.new('en, *;q=0.8').negotiate('en-US')
72
+ match.to_h.must_equal({primary_tag: 'en', subtag: 'us', q: 1.0})
73
+ end
74
+
75
+ it "returns a plain supported language without a synthetic wildcard" do
76
+ subject.new('*').negotiate('en').language_tag.must_equal 'en'
77
+ end
78
+
79
+ it "matches script and region subtags case insensitively" do
80
+ subject.new('zh-Hant').negotiate(['zh-Hans-TW', 'ZH-hant-TW']).language_tag.must_equal 'zh-hant-tw'
81
+ end
82
+
83
+ it "applies a more specific exclusion before its parent language range" do
84
+ match = subject.new('zh, zh-Hant;q=0').negotiate(['zh-Hant-TW', 'zh-Hans-CN'])
85
+ match.language_tag.must_equal 'zh-hans-cn'
86
+ end
87
+
88
+ it "uses the most specific matching range quality" do
89
+ match = subject.new('en;q=1, en-US;q=0.1, fr;q=0.5').negotiate(['en-US', 'fr'])
90
+ match.language_tag.must_equal 'fr'
91
+ end
92
+
93
+ it "rejects candidates outside a subtag boundary" do
94
+ subject.new('en-US').accept?('en-USA').must_equal false
95
+ end
96
+
97
+ it "requires concrete supported language tags" do
98
+ ['*', 'en-*', 'en_US', 'en;q=1'].each do |support|
99
+ assert_raises(AcceptHeaders::InvalidSupportedValue) { subject.new('*').accept?(support) }
100
+ end
101
+ subject.new('*').accept?(nil).must_equal false
102
+ end
103
+
104
+ it "returns nil when no supported language matches" do
105
+ _(subject.new('fr').negotiate('en')).must_be_nil
83
106
  end
84
107
  end
85
108
  end
@@ -91,6 +91,11 @@ describe AcceptHeaders::Language do
91
91
  subject.new('en', 'us', q: 0.9).language_tag.must_equal "en-us"
92
92
  end
93
93
 
94
+ it "outputs the primary tag alone when no subtag is given" do
95
+ subject.new('en').language_tag.must_equal 'en'
96
+ subject.new('en', '*').language_tag.must_equal 'en'
97
+ end
98
+
94
99
  it "if primary tag is * and subtag is * or nil, outputs *" do
95
100
  subject.new('*', nil).language_tag.must_equal '*'
96
101
  subject.new('*', '*').language_tag.must_equal '*'
@@ -119,6 +124,13 @@ describe AcceptHeaders::Language do
119
124
  b.accept?('en-us').must_equal true
120
125
  end
121
126
 
127
+ it "matches complete subtag prefixes case insensitively" do
128
+ a = subject.new('zh', 'hant')
129
+ a.accept?('ZH-Hant-TW').must_equal true
130
+ a.accept?('zh-Hans-TW').must_equal false
131
+ a.accept?('zh-Hantish').must_equal false
132
+ end
133
+
122
134
  it "not accepted if the primary_tag and subtag don't match" do
123
135
  a = subject.new('en', 'us')
124
136
  a.accept?('en-gb').must_equal false
@@ -156,8 +168,7 @@ describe AcceptHeaders::Language do
156
168
  a.accept?(nil).must_equal false
157
169
  end
158
170
 
159
- # TODO: test *
160
- it "not accepted if..." do
171
+ it "does not accept a wildcard as a concrete supported tag" do
161
172
  a = subject.new('en', 'us')
162
173
  a.accept?('*').must_equal false
163
174
  end
@@ -195,8 +206,7 @@ describe AcceptHeaders::Language do
195
206
  a.reject?('en-gb').must_equal false
196
207
  end
197
208
 
198
- # TODO: test *
199
- it "not rejected if..." do
209
+ it "does not reject a wildcard as a concrete supported tag" do
200
210
  a = subject.new('en', 'us', q: 0)
201
211
  a.reject?('*').must_equal false
202
212
  end
@@ -0,0 +1,178 @@
1
+ require_relative "spec_helper"
2
+
3
+ class MalformedHeaderSpec < Minitest::Test
4
+ def test_absent_accept_header_accepts_any_media_type
5
+ result = AcceptHeaders::MediaType::Negotiator.new(nil).negotiate('text/html')
6
+
7
+ assert_equal 'text/html', result.media_range
8
+ end
9
+
10
+ def test_present_empty_accept_header_accepts_no_media_types
11
+ ['', " \t", 'Accept:'].each do |header|
12
+ negotiator = AcceptHeaders::MediaType::Negotiator.new(header)
13
+
14
+ assert_empty negotiator.list
15
+ assert_nil negotiator.negotiate('text/html')
16
+ end
17
+ end
18
+
19
+ def test_unqualified_media_types_are_skipped_without_raising
20
+ negotiator = AcceptHeaders::MediaType::Negotiator.new('nonsense, text/html')
21
+
22
+ assert_equal [{ type: 'text', subtype: 'html', q: 1.0, extensions: {} }], negotiator.list.map(&:to_h)
23
+ end
24
+
25
+ def test_invalidly_tagged_token_bytes_are_skipped_by_each_negotiator
26
+ quality_negotiators.each do |negotiator, valid_token|
27
+ invalid_token = "\xFF".freeze
28
+ assert_empty negotiator.new(invalid_token).list
29
+
30
+ header = "#{invalid_token},#{valid_token}"
31
+ assert_equal negotiator.new(valid_token).list.map(&:to_h), negotiator.new(header).list.map(&:to_h)
32
+ assert_equal Encoding::UTF_8, invalid_token.encoding
33
+ refute invalid_token.valid_encoding?
34
+ end
35
+ end
36
+
37
+ def test_quoted_obs_text_bytes_are_preserved_and_round_trip
38
+ value = (0x80..0xFF).to_a.pack('C*')
39
+ header = ('text/html;note="'.b + value + '"').force_encoding('UTF-8')
40
+ item = AcceptHeaders::MediaType::Negotiator.new(header).list.first
41
+
42
+ assert_equal value, item.extensions['note']
43
+ assert_equal Encoding::ASCII_8BIT, item.extensions['note'].encoding
44
+ assert_equal item.to_h, AcceptHeaders::MediaType::Negotiator.new(item.to_s).list.first.to_h
45
+ end
46
+
47
+ def test_valid_utf8_quoted_values_retain_their_encoding_and_round_trip
48
+ value = "日本語; café"
49
+ item = AcceptHeaders::MediaType::Negotiator.new("text/html;note=\"#{value}\"").list.first
50
+
51
+ assert_equal value, item.extensions['note']
52
+ assert_equal Encoding::UTF_8, item.extensions['note'].encoding
53
+ assert_equal item.to_h, AcceptHeaders::MediaType::Negotiator.new(item.to_s).list.first.to_h
54
+ end
55
+
56
+ def test_multibyte_headers_decode_quoted_pairs_as_http_bytes
57
+ {
58
+ [0x95, 0x5c, 0x5c] => [0x95, 0x5c],
59
+ [0x95, 0x5c, 0x5c, 0x61] => [0x95, 0x5c, 0x61],
60
+ [0x95, 0x5c, 0x61] => [0x95, 0x61],
61
+ [0x95, 0x5c, 0x22, 0x61] => [0x95, 0x22, 0x61]
62
+ }.each do |bytes, expected|
63
+ wire = 'text/plain;note="'.b + bytes.pack('C*') + '";q=0.8,application/json;q=0.5'
64
+ header = wire.dup.force_encoding('Shift_JIS').freeze
65
+ assert header.valid_encoding?
66
+ binary = AcceptHeaders::MediaType::Negotiator.new(wire)
67
+ tagged = AcceptHeaders::MediaType::Negotiator.new(header)
68
+
69
+ assert_equal binary.list.map(&:media_range), tagged.list.map(&:media_range)
70
+ assert_equal binary.list.first.extensions['note'].bytes, tagged.list.first.extensions['note'].bytes
71
+ assert_equal expected, tagged.list.first.extensions['note'].bytes
72
+ assert_equal [0.8, 0.5], tagged.list.map(&:q)
73
+ [wire.split(';q=').first, 'text/plain;note="other"', 'application/json'].each do |offer|
74
+ assert_equal binary.accept?(offer), tagged.accept?(offer)
75
+ assert_equal [binary.negotiate(offer)&.content_type&.bytes], [tagged.negotiate(offer)&.content_type&.bytes]
76
+ end
77
+ assert_equal wire.bytes, header.bytes
78
+ assert_equal Encoding::Shift_JIS, header.encoding
79
+ end
80
+ end
81
+
82
+ def test_multibyte_headers_reject_unterminated_http_quoted_pairs
83
+ ['"', '",application/json'].each do |suffix|
84
+ wire = 'text/plain;note="'.b + [0x95, 0x5c].pack('C*') + suffix
85
+ header = wire.dup.force_encoding('Shift_JIS').freeze
86
+ assert header.valid_encoding?
87
+
88
+ [wire, header].each do |input|
89
+ matcher = AcceptHeaders::MediaType::Negotiator.new(input)
90
+ assert_empty matcher.list
91
+ refute matcher.accept?('application/json')
92
+ assert_nil matcher.negotiate('text/plain')
93
+ end
94
+ assert_equal wire.bytes, header.bytes
95
+ end
96
+ end
97
+
98
+ def test_utf8_header_quoted_pairs_preserve_values_and_encoding
99
+ header = 'text/plain;note="日本語; café\\"x\\\\y";q=0.8'.freeze
100
+ tagged = AcceptHeaders::MediaType::Negotiator.new(header)
101
+ binary = AcceptHeaders::MediaType::Negotiator.new(header.b)
102
+ item = tagged.list.first
103
+
104
+ assert_equal '日本語; café"x\\y', item.extensions['note']
105
+ assert_equal Encoding::UTF_8, item.extensions['note'].encoding
106
+ assert_equal 0.8, item.q
107
+ assert_equal binary.list.first.extensions['note'].bytes, item.extensions['note'].bytes
108
+ assert_equal item.to_h, AcceptHeaders::MediaType::Negotiator.new(item.to_s).list.first.to_h
109
+ assert_equal item.content_type, tagged.negotiate(item.content_type).content_type
110
+ assert_equal Encoding::UTF_8, header.encoding
111
+ end
112
+
113
+ def test_invalid_media_ranges_do_not_match_partial_lines_or_non_token_characters
114
+ ['*/json', 'text/html/extra', 'text/ht{ml}', 'text/ht\\ml', "text/html\nnonsense", 'text /html'].each do |range|
115
+ assert_empty AcceptHeaders::MediaType::Negotiator.new(range).list, range
116
+ end
117
+ end
118
+
119
+ def test_media_ranges_accept_http_token_characters_and_supported_wildcards
120
+ ['application/x_example+json', 'application/problem+json', 'image/avif', 'text/*', '*/*', '*'].each do |range|
121
+ assert_equal 1, AcceptHeaders::MediaType::Negotiator.new(range).list.size, range
122
+ end
123
+ end
124
+
125
+ def test_quality_uses_the_complete_rfc_9110_qvalue_grammar
126
+ values = {
127
+ '0' => 0.0, '0.' => 0.0, '0.000' => 0.0,
128
+ '0.123' => 0.123, '1' => 1.0, '1.' => 1.0, '1.000' => 1.0
129
+ }
130
+
131
+ quality_negotiators.each do |negotiator, value|
132
+ values.each do |qvalue, expected|
133
+ assert_equal expected, negotiator.new("#{value};q=#{qvalue}").list.first.q, "#{value};q=#{qvalue}"
134
+ end
135
+ end
136
+ end
137
+
138
+ def test_invalid_quality_preserves_the_documented_recovery_value
139
+ ['', '.5', '+0.5', '01', '0.1234', '1.0000', '0.1e0', '"0.1"', '1.001', 'NaN', "0\nx"].each do |qvalue|
140
+ quality_negotiators.each do |negotiator, value|
141
+ assert_equal 0.001, negotiator.new("#{value};q=#{qvalue}").list.first.q, "#{value};q=#{qvalue}"
142
+ end
143
+ end
144
+ end
145
+
146
+ def test_quality_parameter_name_is_case_insensitive_and_exact
147
+ quality_negotiators.each do |negotiator, value|
148
+ assert_equal 0.4, negotiator.new("#{value};Q = 0.4").list.first.q
149
+ assert_equal 1.0, negotiator.new("#{value};qs=0").list.first.q
150
+ end
151
+ end
152
+
153
+ def test_quoted_parameter_contents_are_not_quality_parameters
154
+ header = 'text/html;note="a;q=0;b"'
155
+
156
+ assert_equal 1.0, AcceptHeaders::MediaType::Negotiator.new(header).list.first.q
157
+ end
158
+
159
+ def test_quality_after_escaped_quotes_is_still_parsed
160
+ header = 'text/html;note="a\";q=0";q=0.8'
161
+
162
+ assert_equal 0.8, AcceptHeaders::MediaType::Negotiator.new(header).list.first.q
163
+ end
164
+
165
+ def test_unterminated_quoted_parameters_are_skipped
166
+ assert_empty AcceptHeaders::MediaType::Negotiator.new('text/html;note="a;q=0').list
167
+ end
168
+
169
+ private
170
+
171
+ def quality_negotiators
172
+ {
173
+ AcceptHeaders::MediaType::Negotiator => 'text/html',
174
+ AcceptHeaders::Encoding::Negotiator => 'gzip',
175
+ AcceptHeaders::Language::Negotiator => 'en'
176
+ }
177
+ end
178
+ end
@@ -34,12 +34,12 @@ describe AcceptHeaders::MediaType::Negotiator do
34
34
  end
35
35
 
36
36
  it "ignores the 'Accept:' prefix" do
37
- subject.new('Accept: text/html').list.must_equal [
37
+ subject.new('aCcEpT: text/html').list.must_equal [
38
38
  media_type.new('text', 'html')
39
39
  ]
40
40
  end
41
41
 
42
- it "supports all registered IANA media types" do
42
+ it "parses media types in the bundled IANA snapshot" do
43
43
  require 'csv'
44
44
  # https://www.iana.org/assignments/media-types/media-types.xhtml
45
45
  %w[application audio image message model multipart text video].each do |filename|
@@ -54,10 +54,8 @@ describe AcceptHeaders::MediaType::Negotiator do
54
54
  end
55
55
  end
56
56
 
57
- it "sets media type to */* when the accept header is empty" do
58
- subject.new('').list.must_equal [
59
- media_type.new('*', '*')
60
- ]
57
+ it "accepts no media types when the accept header is present but empty" do
58
+ assert_empty subject.new('').list
61
59
  end
62
60
 
63
61
  it "sets media type to */* when the type is only *" do
@@ -92,11 +90,47 @@ describe AcceptHeaders::MediaType::Negotiator do
92
90
  ]
93
91
  end
94
92
 
95
- it "parses extensions with quoted values" do
96
- subject.new('text/html;q=1;version="2";level="a;b;cc\'cd", text/html;version=\'1\';level=\'\blah;x;1;;\'').list.must_equal [
97
- media_type.new('text', 'html', extensions: { 'version' => '2', 'level' => 'a;b;cc\'cd'}),
98
- media_type.new('text', 'html', extensions: { 'version' => '1', 'level' => '\'\blah;x;1;;\''})
99
- ]
93
+ it "preserves delimiters inside quoted parameter values" do
94
+ parsed = subject.new('text/html;version="2";level="a,b;c", application/json;q=0.8').list
95
+ parsed.map(&:media_range).must_equal ['text/html', 'application/json']
96
+ parsed.first.extensions.must_equal('version' => '2', 'level' => 'a,b;c')
97
+ end
98
+
99
+ it "unescapes quoted pairs without splitting escaped quotes" do
100
+ parsed = subject.new('text/html;note="a\\"b,c;d\\\\e"').list
101
+ parsed.size.must_equal 1
102
+ parsed.first.extensions.must_equal('note' => 'a"b,c;d\\e')
103
+ end
104
+
105
+ it "treats apostrophes as token characters rather than quotes" do
106
+ parsed = subject.new("text/html;version='1', application/json;q=0.8").list
107
+ parsed.map(&:media_range).must_equal ['text/html', 'application/json']
108
+ parsed.first.extensions.must_equal('version' => "'1'")
109
+ end
110
+
111
+ it "does not read quality values inside quoted parameters" do
112
+ parsed = subject.new('text/html;note="example;q=0.2";q=0.8').list.first
113
+ parsed.q.must_equal 0.8
114
+ parsed.extensions.must_equal('note' => 'example;q=0.2')
115
+ end
116
+
117
+ it "does not invent parameters from malformed values" do
118
+ ['text/html;note="unterminated', 'text/html;note="trailing' + "\\",
119
+ 'text/html;note="closed"junk', "text/html;note=\"line\nbreak\"",
120
+ "text/html;note=\"escaped\\\nbreak\"", 'text/html;note=two words'].each do |header|
121
+ subject.new(header).list.must_equal []
122
+ end
123
+ parsed = subject.new('text/html;note="closed"junk, application/json').list
124
+ parsed.map(&:media_range).must_equal ['application/json']
125
+ end
126
+
127
+ it "round trips quoted parameter values and quality" do
128
+ original = media_type.new('text', 'html', q: 0.8, extensions: {
129
+ 'note' => 'a,b;c"d\\e', 'empty' => '', 'version' => "'1'"
130
+ })
131
+ parsed = subject.new(original.to_s).list
132
+ parsed.size.must_equal 1
133
+ parsed.first.to_h.must_equal original.to_h
100
134
  end
101
135
 
102
136
  it "skips invalid media types" do
@@ -108,7 +142,7 @@ describe AcceptHeaders::MediaType::Negotiator do
108
142
 
109
143
  describe "#to_s" do
110
144
  it "returns a string of each media type #to_s joined by a comma" do
111
- subject.new("text/html;level=1, text/plain;q=0.9").to_s.must_equal "text/html;q=1;level=1,text/plain;q=0.9"
145
+ subject.new("text/html;level=1, text/plain;q=0.9").to_s.must_equal "text/html;level=1;q=1,text/plain;q=0.9"
112
146
  end
113
147
  end
114
148
 
@@ -150,13 +184,14 @@ describe AcceptHeaders::MediaType::Negotiator do
150
184
  api = subject.new('application/json;q=1;version=2,application/json;q=0.9;version=1')
151
185
  api.negotiate('text/html').must_be_nil
152
186
  api.negotiate('application/xml').must_be_nil
153
- api.negotiate(['application/xml', 'application/json']).must_equal media_type.new(
187
+ api.negotiate(['application/xml', 'application/json']).must_be_nil
188
+ api.negotiate(['application/json;version=1', 'application/json;version=2']).to_h.must_equal media_type.new(
154
189
  'application',
155
190
  'json',
156
191
  extensions: {
157
- 'version' => '1'
192
+ 'version' => '2'
158
193
  }
159
- )
194
+ ).to_h
160
195
 
161
196
  q0 = subject.new('application/json,application/xml;q=0')
162
197
  q0.negotiate('application/json').must_equal media_type.new(
@@ -170,11 +205,54 @@ describe AcceptHeaders::MediaType::Negotiator do
170
205
  )
171
206
  end
172
207
 
208
+ it "uses the most specific matching range to assign quality before choosing a representation" do
209
+ n = subject.new('text/html;q=0.1,text/*;q=0.9,application/json;q=0.5')
210
+ n.negotiate(['text/html', 'application/json']).media_range.must_equal 'application/json'
211
+ n.negotiate(['text/html', 'text/plain']).media_range.must_equal 'text/plain'
212
+ end
213
+
214
+ it "gives matching parameters precedence over a parameterless range" do
215
+ n = subject.new('text/plain;q=0.7,text/plain;format=fixed;q=0.4,image/jpeg;q=0.5')
216
+ n.negotiate(['text/plain;format=fixed', 'image/jpeg']).media_range.must_equal 'image/jpeg'
217
+ n.negotiate(['text/plain', 'image/jpeg']).media_range.must_equal 'text/plain'
218
+ end
219
+
220
+ it "treats parameters after q as media parameters" do
221
+ n = subject.new('application/json;q=0.9;version=2')
222
+ n.negotiate('application/json').must_be_nil
223
+ n.negotiate('application/json;version=2').extensions.must_equal('version' => '2')
224
+ end
225
+
226
+ it "preserves the parameters of the supported representation" do
227
+ n = subject.new('application/json')
228
+ n.negotiate('application/json;version=2').to_h.must_equal media_type.new(
229
+ 'application', 'json', extensions: { 'version' => '2' }
230
+ ).to_h
231
+ end
232
+
233
+ it "uses supported order to break equally preferred matching representations" do
234
+ n = subject.new('text/*')
235
+ n.negotiate(['text/plain', 'text/html']).media_range.must_equal 'text/plain'
236
+ n.negotiate(['text/html', 'text/plain']).media_range.must_equal 'text/html'
237
+ end
238
+
239
+ it "allows a specific positive quality to override a wildcard exclusion" do
240
+ n = subject.new('text/*;q=0,text/html;q=0.5')
241
+ n.negotiate('text/html').media_range.must_equal 'text/html'
242
+ n.negotiate('text/plain').must_be_nil
243
+ end
244
+
245
+ it "excludes only representations matching the rejected parameters" do
246
+ n = subject.new('text/plain;format=fixed;q=0,text/plain;q=1')
247
+ n.negotiate('text/plain;format=fixed').must_be_nil
248
+ n.negotiate('text/plain;format=flowed').extensions.must_equal('format' => 'flowed')
249
+ end
250
+
173
251
  it "rejects matching q=0 even if it matches media ranges where q > 0" do
174
- n = subject.new('application/xml;q=0;*/*')
252
+ n = subject.new('application/xml;q=0,*/*')
175
253
  n.negotiate('application/xml').must_be_nil
176
254
 
177
- n2 = subject.new('application/xml;q=0;application/xml;q=1')
255
+ n2 = subject.new('application/xml;q=0,application/xml;q=1')
178
256
  n2.negotiate('application/xml').must_be_nil
179
257
  end
180
258
  end
@@ -196,10 +274,10 @@ describe AcceptHeaders::MediaType::Negotiator do
196
274
  end
197
275
 
198
276
  it "returns false when q=0 even if it matches media ranges where q > 0" do
199
- n = subject.new('application/xml;q=0;*/*')
277
+ n = subject.new('application/xml;q=0,*/*')
200
278
  n.accept?('application/xml').must_equal false
201
279
 
202
- n2 = subject.new('application/xml;q=0;application/xml;q=1')
280
+ n2 = subject.new('application/xml;q=0,application/xml;q=1')
203
281
  n2.accept?('application/xml').must_equal false
204
282
  end
205
283
  end