accept_headers 0.1.0 → 0.2.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 +13 -1
- data/Gemfile +2 -1
- data/README.md +115 -155
- data/accept_headers.gemspec +2 -2
- data/lib/accept_headers/acceptable.rb +8 -0
- data/lib/accept_headers/encoding/negotiator.rb +18 -8
- data/lib/accept_headers/encoding.rb +5 -3
- data/lib/accept_headers/language/negotiator.rb +3 -2
- data/lib/accept_headers/language.rb +11 -28
- data/lib/accept_headers/media_type/negotiator.rb +18 -19
- data/lib/accept_headers/media_type.rb +28 -15
- data/lib/accept_headers/negotiatable.rb +48 -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 +72 -50
- data/spec/language_spec.rb +14 -4
- data/spec/malformed_spec.rb +121 -0
- data/spec/media_type/negotiator_spec.rb +97 -19
- data/spec/media_type_spec.rb +39 -3
- data/spec/middleware_spec.rb +5 -4
- data/spec/readme_spec.rb +66 -0
- data/spec/rejection_spec.rb +62 -0
- data/spec/spec_helper.rb +1 -3
- metadata +17 -11
- data/.travis.yml +0 -14
|
@@ -0,0 +1,121 @@
|
|
|
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_invalid_media_ranges_do_not_match_partial_lines_or_non_token_characters
|
|
57
|
+
['*/json', 'text/html/extra', 'text/ht{ml}', 'text/ht\\ml', "text/html\nnonsense", 'text /html'].each do |range|
|
|
58
|
+
assert_empty AcceptHeaders::MediaType::Negotiator.new(range).list, range
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def test_media_ranges_accept_http_token_characters_and_supported_wildcards
|
|
63
|
+
['application/x_example+json', 'application/problem+json', 'image/avif', 'text/*', '*/*', '*'].each do |range|
|
|
64
|
+
assert_equal 1, AcceptHeaders::MediaType::Negotiator.new(range).list.size, range
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def test_quality_uses_the_complete_rfc_9110_qvalue_grammar
|
|
69
|
+
values = {
|
|
70
|
+
'0' => 0.0, '0.' => 0.0, '0.000' => 0.0,
|
|
71
|
+
'0.123' => 0.123, '1' => 1.0, '1.' => 1.0, '1.000' => 1.0
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
quality_negotiators.each do |negotiator, value|
|
|
75
|
+
values.each do |qvalue, expected|
|
|
76
|
+
assert_equal expected, negotiator.new("#{value};q=#{qvalue}").list.first.q, "#{value};q=#{qvalue}"
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def test_invalid_quality_preserves_the_documented_recovery_value
|
|
82
|
+
['', '.5', '+0.5', '01', '0.1234', '1.0000', '0.1e0', '"0.1"', '1.001', 'NaN', "0\nx"].each do |qvalue|
|
|
83
|
+
quality_negotiators.each do |negotiator, value|
|
|
84
|
+
assert_equal 0.001, negotiator.new("#{value};q=#{qvalue}").list.first.q, "#{value};q=#{qvalue}"
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def test_quality_parameter_name_is_case_insensitive_and_exact
|
|
90
|
+
quality_negotiators.each do |negotiator, value|
|
|
91
|
+
assert_equal 0.4, negotiator.new("#{value};Q = 0.4").list.first.q
|
|
92
|
+
assert_equal 1.0, negotiator.new("#{value};qs=0").list.first.q
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def test_quoted_parameter_contents_are_not_quality_parameters
|
|
97
|
+
header = 'text/html;note="a;q=0;b"'
|
|
98
|
+
|
|
99
|
+
assert_equal 1.0, AcceptHeaders::MediaType::Negotiator.new(header).list.first.q
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def test_quality_after_escaped_quotes_is_still_parsed
|
|
103
|
+
header = 'text/html;note="a\";q=0";q=0.8'
|
|
104
|
+
|
|
105
|
+
assert_equal 0.8, AcceptHeaders::MediaType::Negotiator.new(header).list.first.q
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def test_unterminated_quoted_parameters_are_skipped
|
|
109
|
+
assert_empty AcceptHeaders::MediaType::Negotiator.new('text/html;note="a;q=0').list
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
private
|
|
113
|
+
|
|
114
|
+
def quality_negotiators
|
|
115
|
+
{
|
|
116
|
+
AcceptHeaders::MediaType::Negotiator => 'text/html',
|
|
117
|
+
AcceptHeaders::Encoding::Negotiator => 'gzip',
|
|
118
|
+
AcceptHeaders::Language::Negotiator => 'en'
|
|
119
|
+
}
|
|
120
|
+
end
|
|
121
|
+
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
|
data/spec/media_type_spec.rb
CHANGED
|
@@ -25,8 +25,8 @@ describe AcceptHeaders::MediaType do
|
|
|
25
25
|
subject.new('*', nil).subtype.must_equal '*'
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
-
it "strips
|
|
29
|
-
subject.new('*', '*', extensions: { "\s\nLEVEL\r\t" => "\t\nX\s\n"}).extensions['
|
|
28
|
+
it "strips and downcases parameter names while preserving values" do
|
|
29
|
+
subject.new('*', '*', extensions: { "\s\nLEVEL\r\t" => "\t\nX\s\n"}).extensions['level'].must_equal "\t\nX\s\n"
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
it "optionally supports a q argument" do
|
|
@@ -92,7 +92,12 @@ describe AcceptHeaders::MediaType do
|
|
|
92
92
|
|
|
93
93
|
it "converts to string" do
|
|
94
94
|
s = subject.new('text', 'html', q: 0.9, extensions: { 'level' => '1' }).to_s
|
|
95
|
-
s.must_equal "text/html;q=0.9
|
|
95
|
+
s.must_equal "text/html;level=1;q=0.9"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
it "quotes parameter delimiters and escapes quotes and backslashes" do
|
|
99
|
+
value = subject.new('text', 'html', extensions: { 'note' => 'a,b;c"d\\e', 'empty' => '' })
|
|
100
|
+
value.to_s.must_equal 'text/html;note="a,b;c\\"d\\\\e";empty="";q=1'
|
|
96
101
|
end
|
|
97
102
|
|
|
98
103
|
it "outputs the media range" do
|
|
@@ -100,6 +105,37 @@ describe AcceptHeaders::MediaType do
|
|
|
100
105
|
end
|
|
101
106
|
|
|
102
107
|
describe "#accept?" do
|
|
108
|
+
it "matches type and subtype without case sensitivity" do
|
|
109
|
+
subject.new('text', 'html').accept?('TEXT/HTML').must_equal true
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
it "requires every media range parameter to match the supported representation" do
|
|
113
|
+
range = subject.new('application', 'json', extensions: { 'version' => '2' })
|
|
114
|
+
range.accept?('application/json').must_equal false
|
|
115
|
+
range.accept?('application/json;version=1').must_equal false
|
|
116
|
+
range.accept?('application/json;VERSION=2').must_equal true
|
|
117
|
+
range.accept?('application/json;version=2;profile=public').must_equal true
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
it "matches charset values without case sensitivity" do
|
|
121
|
+
range = subject.new('text', 'plain', extensions: { 'charset' => 'UTF-8' })
|
|
122
|
+
range.accept?('text/plain;charset=utf-8').must_equal true
|
|
123
|
+
range.accept?('text/plain;charset=iso-8859-1').must_equal false
|
|
124
|
+
range.accept?('text/plain').must_equal false
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
it "preserves case sensitivity for other parameter values" do
|
|
128
|
+
range = subject.new('application', 'json', extensions: { 'profile' => 'Public' })
|
|
129
|
+
range.accept?('application/json;profile=Public').must_equal true
|
|
130
|
+
range.accept?('application/json;profile=public').must_equal false
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
it "does not match an empty candidate or a list of candidates" do
|
|
134
|
+
range = subject.new('*')
|
|
135
|
+
range.accept?('').must_equal false
|
|
136
|
+
range.accept?('text/html,application/json').must_equal false
|
|
137
|
+
end
|
|
138
|
+
|
|
103
139
|
it "accepted if the type and subtype are the same" do
|
|
104
140
|
a = subject.new('text', 'html')
|
|
105
141
|
a.accept?('text/html').must_equal true
|
data/spec/middleware_spec.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
require_relative "spec_helper"
|
|
2
2
|
require "accept_headers/middleware"
|
|
3
|
-
require "rack/
|
|
3
|
+
require "rack/builder"
|
|
4
|
+
require "rack/mock"
|
|
4
5
|
|
|
5
6
|
describe AcceptHeaders::Middleware do
|
|
6
7
|
subject do
|
|
@@ -19,9 +20,9 @@ describe AcceptHeaders::Middleware do
|
|
|
19
20
|
matched_encoding = env["accept_headers.encodings"].negotiate(supported_encodings)
|
|
20
21
|
matched_language = env["accept_headers.languages"].negotiate(supported_languages)
|
|
21
22
|
headers = {
|
|
22
|
-
'
|
|
23
|
-
'
|
|
24
|
-
'
|
|
23
|
+
'content-type' => matched_media_type ? matched_media_type.media_range : '',
|
|
24
|
+
'content-encoding' => matched_encoding ? matched_encoding.encoding : '',
|
|
25
|
+
'content-language' => matched_language ? matched_language.language_tag : '',
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
[ 200, headers, [ '' ] ]
|
data/spec/readme_spec.rb
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
require_relative "spec_helper"
|
|
2
|
+
require "rack/builder"
|
|
3
|
+
require "rack/lint"
|
|
4
|
+
require "rack/mock"
|
|
5
|
+
|
|
6
|
+
class ReadmeSpec < Minitest::Test
|
|
7
|
+
def test_documented_negotiation_results
|
|
8
|
+
context = binding
|
|
9
|
+
examples.each do |example|
|
|
10
|
+
next if example.start_with?("gem ") || example.include?('use AcceptHeaders::Middleware')
|
|
11
|
+
source = +""
|
|
12
|
+
example.each_line do |line|
|
|
13
|
+
if (expectation = /\A# => (.*)/.match(line))
|
|
14
|
+
expected = eval(expectation[1], context, readme_path)
|
|
15
|
+
actual = eval(source, context, readme_path)
|
|
16
|
+
if expected.nil?
|
|
17
|
+
assert_nil actual, source
|
|
18
|
+
else
|
|
19
|
+
assert_equal expected, actual, source
|
|
20
|
+
end
|
|
21
|
+
source.clear
|
|
22
|
+
else
|
|
23
|
+
source << line
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
eval(source, context, readme_path) unless source.strip.empty?
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def test_rack_example_selects_supported_responses_and_honors_rejections
|
|
31
|
+
source = examples.find { |example| example.include?('use AcceptHeaders::Middleware') }
|
|
32
|
+
refute_nil source
|
|
33
|
+
builder = Rack::Builder.new
|
|
34
|
+
builder.instance_eval(source, readme_path)
|
|
35
|
+
request = Rack::MockRequest.new(Rack::Lint.new(builder.to_app))
|
|
36
|
+
|
|
37
|
+
response = request.get('/', 'HTTP_ACCEPT' => 'application/json', 'HTTP_ACCEPT_LANGUAGE' => 'en-GB')
|
|
38
|
+
assert_equal 200, response.status
|
|
39
|
+
assert_equal 'application/json', response['content-type']
|
|
40
|
+
assert_equal 'en-gb', response['content-language']
|
|
41
|
+
assert_equal '{"message":"Hello"}', response.body
|
|
42
|
+
assert_nil response['content-encoding']
|
|
43
|
+
assert_equal 'Accept, Accept-Encoding, Accept-Language', response['vary']
|
|
44
|
+
|
|
45
|
+
[
|
|
46
|
+
{ 'HTTP_ACCEPT' => 'application/xml' },
|
|
47
|
+
{ 'HTTP_ACCEPT_ENCODING' => 'identity;q=0' },
|
|
48
|
+
{ 'HTTP_ACCEPT_LANGUAGE' => 'fr' }
|
|
49
|
+
].each do |headers|
|
|
50
|
+
rejected = request.get('/', headers)
|
|
51
|
+
assert_equal 406, rejected.status, headers.inspect
|
|
52
|
+
assert_equal 'Not acceptable', rejected.body
|
|
53
|
+
assert_equal 'Accept, Accept-Encoding, Accept-Language', rejected['vary']
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def readme_path
|
|
60
|
+
File.expand_path('../README.md', __dir__)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def examples
|
|
64
|
+
File.read(readme_path).scan(/```ruby\n(.*?)```/m).flatten
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
require_relative "spec_helper"
|
|
2
|
+
|
|
3
|
+
describe "Negotiation exclusions" do
|
|
4
|
+
it "rejects an explicitly excluded media type despite an acceptable wildcard" do
|
|
5
|
+
negotiator = AcceptHeaders::MediaType::Negotiator.new('text/html;q=0, */*;q=1')
|
|
6
|
+
|
|
7
|
+
negotiator.negotiate('text/html').must_be_nil
|
|
8
|
+
negotiator.accept?('text/html').must_equal false
|
|
9
|
+
negotiator.negotiate(['text/html', 'application/json']).media_range.must_equal 'application/json'
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
it "allows an explicit media type to override a wildcard exclusion" do
|
|
13
|
+
negotiator = AcceptHeaders::MediaType::Negotiator.new('*/*;q=0, text/html;q=1')
|
|
14
|
+
|
|
15
|
+
negotiator.negotiate('text/html').media_range.must_equal 'text/html'
|
|
16
|
+
negotiator.accept?('application/json').must_equal false
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it "rejects an explicitly excluded encoding despite an acceptable wildcard" do
|
|
20
|
+
negotiator = AcceptHeaders::Encoding::Negotiator.new('gzip;q=0, *;q=1')
|
|
21
|
+
|
|
22
|
+
negotiator.negotiate('gzip').must_be_nil
|
|
23
|
+
negotiator.accept?('gzip').must_equal false
|
|
24
|
+
negotiator.negotiate(['gzip', 'br']).encoding.must_equal 'br'
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it "allows an explicit encoding to override a wildcard exclusion" do
|
|
28
|
+
negotiator = AcceptHeaders::Encoding::Negotiator.new('*;q=0, gzip;q=1')
|
|
29
|
+
|
|
30
|
+
negotiator.negotiate('gzip').encoding.must_equal 'gzip'
|
|
31
|
+
negotiator.accept?('br').must_equal false
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it "rejects an explicitly excluded language despite an acceptable wildcard" do
|
|
35
|
+
negotiator = AcceptHeaders::Language::Negotiator.new('en-us;q=0, *;q=1')
|
|
36
|
+
|
|
37
|
+
negotiator.negotiate('en-us').must_be_nil
|
|
38
|
+
negotiator.accept?('en-us').must_equal false
|
|
39
|
+
negotiator.negotiate(['en-us', 'fr-ca']).language_tag.must_equal 'fr-ca'
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it "allows an explicit language to override a wildcard exclusion" do
|
|
43
|
+
negotiator = AcceptHeaders::Language::Negotiator.new('*;q=0, en-us;q=1')
|
|
44
|
+
|
|
45
|
+
negotiator.negotiate('en-us').language_tag.must_equal 'en-us'
|
|
46
|
+
negotiator.accept?('fr-ca').must_equal false
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it "allows a regional language to override a broader language exclusion" do
|
|
50
|
+
negotiator = AcceptHeaders::Language::Negotiator.new('en;q=0, en-us;q=1')
|
|
51
|
+
|
|
52
|
+
negotiator.negotiate('en-us').language_tag.must_equal 'en-us'
|
|
53
|
+
negotiator.accept?('en-gb').must_equal false
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it "rejects a regional language despite an acceptable broader language" do
|
|
57
|
+
negotiator = AcceptHeaders::Language::Negotiator.new('en-us;q=0, en;q=1')
|
|
58
|
+
|
|
59
|
+
negotiator.negotiate('en-us').must_be_nil
|
|
60
|
+
negotiator.negotiate(['en-us', 'en-gb']).language_tag.must_equal 'en-gb'
|
|
61
|
+
end
|
|
62
|
+
end
|
data/spec/spec_helper.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: accept_headers
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jack Chu
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: rake
|
|
@@ -16,14 +15,14 @@ dependencies:
|
|
|
16
15
|
requirements:
|
|
17
16
|
- - "~>"
|
|
18
17
|
- !ruby/object:Gem::Version
|
|
19
|
-
version: '
|
|
18
|
+
version: '13.0'
|
|
20
19
|
type: :development
|
|
21
20
|
prerelease: false
|
|
22
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
22
|
requirements:
|
|
24
23
|
- - "~>"
|
|
25
24
|
- !ruby/object:Gem::Version
|
|
26
|
-
version: '
|
|
25
|
+
version: '13.0'
|
|
27
26
|
- !ruby/object:Gem::Dependency
|
|
28
27
|
name: minitest
|
|
29
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -66,16 +65,17 @@ dependencies:
|
|
|
66
65
|
- - ">="
|
|
67
66
|
- !ruby/object:Gem::Version
|
|
68
67
|
version: '0'
|
|
69
|
-
description: A ruby library that
|
|
70
|
-
|
|
68
|
+
description: A ruby library that parses HTTP accept headers and negotiates media types,
|
|
69
|
+
content codings, and languages using RFC 9110 and RFC 4647 Basic Filtering.
|
|
71
70
|
email:
|
|
72
71
|
- kamuigt@gmail.com
|
|
73
72
|
executables: []
|
|
74
73
|
extensions: []
|
|
75
74
|
extra_rdoc_files: []
|
|
76
75
|
files:
|
|
76
|
+
- ".github/dependabot.yml"
|
|
77
|
+
- ".github/workflows/ci.yml"
|
|
77
78
|
- ".gitignore"
|
|
78
|
-
- ".travis.yml"
|
|
79
79
|
- CHANGELOG.md
|
|
80
80
|
- Gemfile
|
|
81
81
|
- Guardfile
|
|
@@ -96,11 +96,15 @@ files:
|
|
|
96
96
|
- lib/accept_headers/version.rb
|
|
97
97
|
- spec/encoding/negotiator_spec.rb
|
|
98
98
|
- spec/encoding_spec.rb
|
|
99
|
+
- spec/equality_spec.rb
|
|
99
100
|
- spec/language/negotiator_spec.rb
|
|
100
101
|
- spec/language_spec.rb
|
|
102
|
+
- spec/malformed_spec.rb
|
|
101
103
|
- spec/media_type/negotiator_spec.rb
|
|
102
104
|
- spec/media_type_spec.rb
|
|
103
105
|
- spec/middleware_spec.rb
|
|
106
|
+
- spec/readme_spec.rb
|
|
107
|
+
- spec/rejection_spec.rb
|
|
104
108
|
- spec/spec_helper.rb
|
|
105
109
|
- spec/support/encodings/content-coding.csv
|
|
106
110
|
- spec/support/media_types/application.csv
|
|
@@ -115,7 +119,6 @@ homepage: ''
|
|
|
115
119
|
licenses:
|
|
116
120
|
- MIT
|
|
117
121
|
metadata: {}
|
|
118
|
-
post_install_message:
|
|
119
122
|
rdoc_options: []
|
|
120
123
|
require_paths:
|
|
121
124
|
- lib
|
|
@@ -130,19 +133,22 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
130
133
|
- !ruby/object:Gem::Version
|
|
131
134
|
version: '0'
|
|
132
135
|
requirements: []
|
|
133
|
-
rubygems_version:
|
|
134
|
-
signing_key:
|
|
136
|
+
rubygems_version: 4.0.16
|
|
135
137
|
specification_version: 4
|
|
136
138
|
summary: A ruby library that does content negotiation and parses and sorts http accept
|
|
137
139
|
headers.
|
|
138
140
|
test_files:
|
|
139
141
|
- spec/encoding/negotiator_spec.rb
|
|
140
142
|
- spec/encoding_spec.rb
|
|
143
|
+
- spec/equality_spec.rb
|
|
141
144
|
- spec/language/negotiator_spec.rb
|
|
142
145
|
- spec/language_spec.rb
|
|
146
|
+
- spec/malformed_spec.rb
|
|
143
147
|
- spec/media_type/negotiator_spec.rb
|
|
144
148
|
- spec/media_type_spec.rb
|
|
145
149
|
- spec/middleware_spec.rb
|
|
150
|
+
- spec/readme_spec.rb
|
|
151
|
+
- spec/rejection_spec.rb
|
|
146
152
|
- spec/spec_helper.rb
|
|
147
153
|
- spec/support/encodings/content-coding.csv
|
|
148
154
|
- spec/support/media_types/application.csv
|