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.
- checksums.yaml +4 -4
- data/.github/dependabot.yml +8 -0
- data/.github/workflows/ci.yml +23 -0
- data/CHANGELOG.md +23 -1
- data/Gemfile +2 -1
- data/README.md +134 -151
- data/accept_headers.gemspec +2 -2
- data/lib/accept_headers/acceptable.rb +8 -0
- data/lib/accept_headers/encoding/negotiator.rb +23 -8
- data/lib/accept_headers/encoding.rb +5 -3
- data/lib/accept_headers/invalid_supported_value.rb +3 -0
- data/lib/accept_headers/language/negotiator.rb +7 -2
- data/lib/accept_headers/language.rb +11 -28
- data/lib/accept_headers/media_type/negotiator.rb +32 -19
- data/lib/accept_headers/media_type.rb +35 -14
- data/lib/accept_headers/negotiatable.rb +70 -28
- data/lib/accept_headers/version.rb +1 -1
- data/spec/encoding/negotiator_spec.rb +60 -6
- data/spec/encoding_spec.rb +8 -0
- data/spec/equality_spec.rb +70 -0
- data/spec/language/negotiator_spec.rb +73 -50
- data/spec/language_spec.rb +14 -4
- data/spec/malformed_spec.rb +178 -0
- data/spec/media_type/negotiator_spec.rb +97 -19
- data/spec/media_type_spec.rb +117 -3
- data/spec/middleware_spec.rb +5 -4
- data/spec/readme_spec.rb +67 -0
- data/spec/rejection_spec.rb +62 -0
- data/spec/spec_helper.rb +1 -3
- data/spec/supported_values_spec.rb +158 -0
- metadata +20 -11
- data/.travis.yml +0 -14
|
@@ -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
|
|
8
|
-
it "
|
|
9
|
-
subject.new(
|
|
10
|
-
|
|
11
|
-
|
|
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(
|
|
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
|
|
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 "
|
|
28
|
-
subject.new(
|
|
29
|
-
|
|
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 "
|
|
34
|
-
|
|
35
|
-
|
|
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 "
|
|
40
|
-
subject.new(
|
|
41
|
-
|
|
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 "
|
|
46
|
-
subject.new(
|
|
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 "
|
|
53
|
-
subject.new(
|
|
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 "
|
|
60
|
-
subject.new("en-
|
|
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 "
|
|
66
|
-
subject.new(
|
|
67
|
-
|
|
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 "
|
|
74
|
-
subject.new(
|
|
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 "
|
|
79
|
-
it "returns
|
|
80
|
-
match =
|
|
81
|
-
|
|
82
|
-
|
|
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
|
data/spec/language_spec.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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('
|
|
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 "
|
|
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 "
|
|
58
|
-
subject.new('').list
|
|
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 "
|
|
96
|
-
subject.new('text/html;
|
|
97
|
-
|
|
98
|
-
|
|
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;
|
|
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']).
|
|
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' => '
|
|
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
|
|
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
|
|
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
|