xenon 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,54 @@
1
+ require 'xenon/headers/accept'
2
+
3
+ describe Xenon::Headers::Accept do
4
+
5
+ context '::parse' do
6
+ it 'can parse a basic accept range' do
7
+ header = Xenon::Headers::Accept.parse('text/plain; q=0.5')
8
+ expect(header.media_ranges.size).to eq(1)
9
+ expect(header.media_ranges[0].to_s).to eq('text/plain; q=0.5')
10
+ end
11
+
12
+ it 'can parse the first example from RFC 7231 § 5.3.2 with the right precedence' do
13
+ header = Xenon::Headers::Accept.parse('text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c')
14
+ expect(header.media_ranges.size).to eq(4)
15
+ expect(header.media_ranges[0].to_s).to eq('text/html')
16
+ expect(header.media_ranges[1].to_s).to eq('text/x-c')
17
+ expect(header.media_ranges[2].to_s).to eq('text/x-dvi; q=0.8')
18
+ expect(header.media_ranges[3].to_s).to eq('text/plain; q=0.5')
19
+ end
20
+
21
+ it 'can parse the second example from RFC 7231 § 5.3.2 with the right precedence' do
22
+ header = Xenon::Headers::Accept.parse('text/*, text/plain, text/plain;format=flowed, */*')
23
+ expect(header.media_ranges.size).to eq(4)
24
+ expect(header.media_ranges[0].to_s).to eq('text/plain; format=flowed')
25
+ expect(header.media_ranges[1].to_s).to eq('text/plain')
26
+ expect(header.media_ranges[2].to_s).to eq('text/*')
27
+ expect(header.media_ranges[3].to_s).to eq('*/*')
28
+ end
29
+
30
+ it 'can parse the third example from RFC 7231 § 5.3.2 with the right precedence' do
31
+ header = Xenon::Headers::Accept.parse('text/*;q=0.3, text/html;q=0.7, text/html;level=1, text/html;level=2;q=0.4, */*;q=0.5')
32
+ expect(header.media_ranges.size).to eq(5)
33
+ expect(header.media_ranges[0].to_s).to eq('text/html; level=1')
34
+ expect(header.media_ranges[1].to_s).to eq('text/html; level=2; q=0.4')
35
+ expect(header.media_ranges[2].to_s).to eq('text/html; q=0.7')
36
+ expect(header.media_ranges[3].to_s).to eq('text/*; q=0.3')
37
+ expect(header.media_ranges[4].to_s).to eq('*/*; q=0.5')
38
+ end
39
+ end
40
+
41
+ context '#merge' do
42
+ it 'can merge two headers with the right precedence' do
43
+ h1 = Xenon::Headers::Accept.parse('text/plain; q=0.5, text/html')
44
+ h2 = Xenon::Headers::Accept.parse('text/x-c, text/x-dvi; q=0.8')
45
+ header = h1.merge(h2)
46
+ expect(header.media_ranges.size).to eq(4)
47
+ expect(header.media_ranges[0].to_s).to eq('text/html')
48
+ expect(header.media_ranges[1].to_s).to eq('text/x-c')
49
+ expect(header.media_ranges[2].to_s).to eq('text/x-dvi; q=0.8')
50
+ expect(header.media_ranges[3].to_s).to eq('text/plain; q=0.5')
51
+ end
52
+ end
53
+
54
+ end
@@ -0,0 +1,47 @@
1
+ require 'xenon/headers/authorization'
2
+
3
+ describe Xenon::Headers::Authorization do
4
+
5
+ context '::parse' do
6
+ it 'can parse Basic credentials' do
7
+ header = Xenon::Headers::Authorization.parse('Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==')
8
+ expect(header.credentials.class).to eq(Xenon::BasicCredentials)
9
+ expect(header.credentials.username).to eq('Aladdin')
10
+ expect(header.credentials.password).to eq('open sesame')
11
+ end
12
+
13
+ it 'can parse Digest credentials' do
14
+ header = Xenon::Headers::Authorization.parse('Digest username="Mufasa"' +
15
+ ', realm="testrealm@host.com"' +
16
+ ', nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093"' +
17
+ ', uri="/dir/index.html"' +
18
+ ', qop=auth' +
19
+ ', nc=00000001' +
20
+ ', cnonce="0a4f113b"' +
21
+ ', response="6629fae49393a05397450978507c4ef1"' +
22
+ ', opaque="5ccc069c403ebaf9f0171e9517f40e41"')
23
+ expect(header.credentials.class).to eq(Xenon::GenericCredentials)
24
+ expect(header.credentials.scheme).to eq('Digest')
25
+ expect(header.credentials.token).to eq(nil)
26
+ expect(header.credentials.params).to eq(
27
+ 'username' => 'Mufasa',
28
+ 'realm' => 'testrealm@host.com',
29
+ 'nonce' => 'dcd98b7102dd2f0e8b11d0f600bfb0c093',
30
+ 'uri' => '/dir/index.html',
31
+ 'qop' => 'auth',
32
+ 'nc' => '00000001',
33
+ 'cnonce' => '0a4f113b',
34
+ 'response' => '6629fae49393a05397450978507c4ef1',
35
+ 'opaque' => '5ccc069c403ebaf9f0171e9517f40e41')
36
+ end
37
+
38
+ it 'can parse Bearer credentials' do
39
+ header = Xenon::Headers::Authorization.parse('Bearer eyJhbGciOiJub25lIn0.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.')
40
+ expect(header.credentials.class).to eq(Xenon::GenericCredentials)
41
+ expect(header.credentials.scheme).to eq('Bearer')
42
+ expect(header.credentials.token).to eq('eyJhbGciOiJub25lIn0.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.')
43
+ expect(header.credentials.params).to eq({})
44
+ end
45
+ end
46
+
47
+ end
@@ -0,0 +1,64 @@
1
+ require 'xenon/headers/cache_control'
2
+
3
+ describe Xenon::Headers::CacheControl do
4
+
5
+ context '::parse' do
6
+ ['max-age', 'max-stale', 'min-fresh', 's-maxage'].each do |dir|
7
+ it "can parse the #{dir} directive" do
8
+ header = Xenon::Headers::CacheControl.parse("#{dir}=5")
9
+ expect(header.directives.size).to eq(1)
10
+ expect(header.directives[0].to_s).to eq("#{dir}=5")
11
+ end
12
+
13
+ it "can parse the #{dir} directive with a quoted value" do # should not be sent by clients but is permitted
14
+ header = Xenon::Headers::CacheControl.parse("#{dir}=\"5\"")
15
+ expect(header.directives.size).to eq(1)
16
+ expect(header.directives[0].to_s).to eq("#{dir}=5")
17
+ end
18
+ end
19
+
20
+ ['no-cache', 'no-store', 'no-transform', 'only-if-cached', 'must-revalidate', 'public', 'proxy-revalidate'].each do |dir|
21
+ it "can parse the #{dir} directive" do
22
+ header = Xenon::Headers::CacheControl.parse("#{dir}")
23
+ expect(header.directives.size).to eq(1)
24
+ expect(header.directives[0].to_s).to eq("#{dir}")
25
+ end
26
+ end
27
+
28
+ it "can parse the private directive with no field names" do
29
+ header = Xenon::Headers::CacheControl.parse('private')
30
+ expect(header.directives.size).to eq(1)
31
+ expect(header.directives[0].to_s).to eq('private')
32
+ end
33
+
34
+ # TODO: private directive with field names
35
+
36
+ it 'can parse extension directives with quoted string values' do
37
+ header = Xenon::Headers::CacheControl.parse('ext="hello \"world\""')
38
+ expect(header.directives.size).to eq(1)
39
+ expect(header.directives[0].name).to eq('ext')
40
+ expect(header.directives[0].value).to eq("hello \"world\"")
41
+ end
42
+
43
+ it 'can parse more complex directives' do
44
+ header = Xenon::Headers::CacheControl.parse('public, max-age=3600, must-revalidate')
45
+ expect(header.directives.size).to eq(3)
46
+ expect(header.directives[0].to_s).to eq('public')
47
+ expect(header.directives[1].to_s).to eq('max-age=3600')
48
+ expect(header.directives[2].to_s).to eq('must-revalidate')
49
+ end
50
+ end
51
+
52
+ context '#merge' do
53
+ it 'can merge two headers and maintain directive order' do
54
+ h1 = Xenon::Headers::CacheControl.parse('public, max-age=3600')
55
+ h2 = Xenon::Headers::CacheControl.parse('must-revalidate')
56
+ header = h1.merge(h2)
57
+ expect(header.directives.size).to eq(3)
58
+ expect(header.directives[0].to_s).to eq('public')
59
+ expect(header.directives[1].to_s).to eq('max-age=3600')
60
+ expect(header.directives[2].to_s).to eq('must-revalidate')
61
+ end
62
+ end
63
+
64
+ end
@@ -0,0 +1,267 @@
1
+ require 'xenon/media_type'
2
+
3
+ describe Xenon::MediaType do
4
+
5
+ context '::parse' do
6
+ it 'can parse basic media types' do
7
+ mt = Xenon::MediaType.parse('application/json')
8
+ expect(mt.type).to eq('application')
9
+ expect(mt.subtype).to eq('json')
10
+ end
11
+ it 'can parse media types with a subtype suffix' do
12
+ mt = Xenon::MediaType.parse('application/rss+xml')
13
+ expect(mt.type).to eq('application')
14
+ expect(mt.subtype).to eq('rss+xml')
15
+ end
16
+
17
+ it 'can parse media types with parameters' do
18
+ mt = Xenon::MediaType.parse('text/plain; format=flowed; paged')
19
+ expect(mt.type).to eq('text')
20
+ expect(mt.subtype).to eq('plain')
21
+ expect(mt.params).to eq({ 'format' => 'flowed', 'paged' => nil })
22
+ end
23
+
24
+ it 'strips whitespace around separators' do
25
+ mt = Xenon::MediaType.parse('text/plain ; format = flowed ; paged')
26
+ expect(mt.type).to eq('text')
27
+ expect(mt.subtype).to eq('plain')
28
+ expect(mt.params).to eq({ 'format' => 'flowed', 'paged' => nil })
29
+ end
30
+
31
+ it 'raises an error when the media type contains wildcards' do
32
+ expect { Xenon::MediaType.parse('*/*') }.to raise_error(Xenon::ParseError)
33
+ expect { Xenon::MediaType.parse('application/*') }.to raise_error(Xenon::ParseError)
34
+ end
35
+
36
+ it 'raises an error when the media type is invalid' do
37
+ expect { Xenon::MediaType.parse('application') }.to raise_error(Xenon::ParseError)
38
+ expect { Xenon::MediaType.parse('application; foo=bar') }.to raise_error(Xenon::ParseError)
39
+ expect { Xenon::MediaType.parse('/json') }.to raise_error(Xenon::ParseError)
40
+ expect { Xenon::MediaType.parse('/json; foo=bar') }.to raise_error(Xenon::ParseError)
41
+ end
42
+ end
43
+
44
+ %w(application audio image message multipart text video).each do |type|
45
+ context "#{type}?" do
46
+ it "returns true when the root type is '#{type}'" do
47
+ mt = Xenon::MediaType.new(type, 'dummy')
48
+ expect(mt.send("#{type}?")).to eq(true)
49
+ end
50
+
51
+ it "returns false when the root type is not '#{type}'" do
52
+ mt = Xenon::MediaType.new('dummy', 'dummy')
53
+ expect(mt.send("#{type}?")).to eq(false)
54
+ end
55
+ end
56
+ end
57
+
58
+ { experimental?: 'x', personal?: 'prs', vendor?: 'vnd' }.each do |method, prefix|
59
+ context method do
60
+ it "returns true when the subtype starts with '#{prefix}.'" do
61
+ mt = Xenon::MediaType.new('application', "#{prefix}.dummy")
62
+ expect(mt.send(method)).to eq(true)
63
+ end
64
+
65
+ it "returns false when the subtype does not start with '#{prefix}.'" do
66
+ mt = Xenon::MediaType.new('application', "dummy.dummy")
67
+ expect(mt.send(method)).to eq(false)
68
+ end
69
+ end
70
+ end
71
+
72
+ %w(ber der fastinfoset json wbxml xml zip).each do |format|
73
+ context "#{format}?" do
74
+ it "returns true when the subtype is '#{format}'" do
75
+ mt = Xenon::MediaType.new('application', format)
76
+ expect(mt.send("#{format}?")).to eq(true)
77
+ end
78
+
79
+ it "returns true when the subtype ends with '+#{format}'" do
80
+ mt = Xenon::MediaType.new('application', "dummy+#{format}")
81
+ expect(mt.send("#{format}?")).to eq(true)
82
+ end
83
+
84
+ it "returns false when the subtype is not '#{format}' and does not end with '+#{format}'" do
85
+ mt = Xenon::MediaType.new('dummy', 'dummy+dummy')
86
+ expect(mt.send("#{format}?")).to eq(false)
87
+ end
88
+ end
89
+ end
90
+
91
+ context '#to_s' do
92
+ it 'returns the string representation of a media type' do
93
+ mt = Xenon::MediaType.new('text', 'plain', 'format' => 'flowed', 'paged' => nil)
94
+ expect(mt.to_s).to eq('text/plain; format=flowed; paged')
95
+ end
96
+ end
97
+
98
+ end
99
+
100
+ describe Xenon::MediaRange do
101
+
102
+ context '::parse' do
103
+ it 'can parse basic media ranges' do
104
+ mt = Xenon::MediaRange.parse('application/json')
105
+ expect(mt.type).to eq('application')
106
+ expect(mt.subtype).to eq('json')
107
+ end
108
+
109
+ it 'can parse media ranges with parameters' do
110
+ mt = Xenon::MediaRange.parse('text/plain; format=flowed; paged')
111
+ expect(mt.type).to eq('text')
112
+ expect(mt.subtype).to eq('plain')
113
+ expect(mt.params).to eq({ 'format' => 'flowed', 'paged' => nil })
114
+ end
115
+
116
+ it 'strips whitespace around separators' do
117
+ mt = Xenon::MediaRange.parse('text/plain ; format = flowed ; paged')
118
+ expect(mt.type).to eq('text')
119
+ expect(mt.subtype).to eq('plain')
120
+ expect(mt.params).to eq({ 'format' => 'flowed', 'paged' => nil })
121
+ end
122
+
123
+ it 'can parse media ranges with subtype wildcards' do
124
+ mt = Xenon::MediaRange.parse('application/*')
125
+ expect(mt.type).to eq('application')
126
+ expect(mt.subtype).to eq('*')
127
+ end
128
+
129
+ it 'can parse media ranges with type and subtype wildcards' do
130
+ mt = Xenon::MediaRange.parse('*/*')
131
+ expect(mt.type).to eq('*')
132
+ expect(mt.subtype).to eq('*')
133
+ end
134
+
135
+ it 'extracts q from the parameters' do
136
+ mt = Xenon::MediaRange.parse('text/plain; q=0.8; format=flowed; paged')
137
+ expect(mt.type).to eq('text')
138
+ expect(mt.subtype).to eq('plain')
139
+ expect(mt.q).to eq(0.8)
140
+ expect(mt.params).to eq({ 'format' => 'flowed', 'paged' => nil })
141
+ end
142
+
143
+ it 'uses the default value for q if the value is not numeric' do
144
+ mt = Xenon::MediaRange.parse('application/json; q=foo')
145
+ expect(mt.type).to eq('application')
146
+ expect(mt.subtype).to eq('json')
147
+ expect(mt.q).to eq(Xenon::MediaRange::DEFAULT_Q)
148
+ end
149
+
150
+ it 'raises an error when the media range is invalid' do
151
+ expect { Xenon::MediaRange.parse('application') }.to raise_error(Xenon::ParseError)
152
+ expect { Xenon::MediaRange.parse('application; foo=bar') }.to raise_error(Xenon::ParseError)
153
+ expect { Xenon::MediaRange.parse('*/json') }.to raise_error(Xenon::ParseError)
154
+ expect { Xenon::MediaRange.parse('/json') }.to raise_error(Xenon::ParseError)
155
+ expect { Xenon::MediaRange.parse('/json; foo=bar') }.to raise_error(Xenon::ParseError)
156
+ end
157
+ end
158
+
159
+ context '#<=>' do
160
+ it 'considers a wildcard type less than a regular type' do
161
+ mr1 = Xenon::MediaRange.new('*', '*')
162
+ mr2 = Xenon::MediaRange.new('text', '*')
163
+ expect(mr1 <=> mr2).to eq(-1)
164
+ end
165
+
166
+ it 'considers a wildcard subtype less than a regular subtype' do
167
+ mr1 = Xenon::MediaRange.new('application', '*')
168
+ mr2 = Xenon::MediaRange.new('text', 'plain')
169
+ expect(mr1 <=> mr2).to eq(-1)
170
+ end
171
+
172
+ it 'considers media ranges with type and subtype equal' do
173
+ mr1 = Xenon::MediaRange.new('application', 'json')
174
+ mr2 = Xenon::MediaRange.new('text', 'plain')
175
+ expect(mr1 <=> mr2).to eq(0)
176
+ end
177
+
178
+ it 'considers a media range with parameters greater than one without' do
179
+ mr1 = Xenon::MediaRange.new('text', 'plain', 'format' => 'flowed')
180
+ mr2 = Xenon::MediaRange.new('text', 'plain')
181
+ expect(mr1 <=> mr2).to eq(1)
182
+ end
183
+
184
+ it 'does not consider the quality when one media range is more specific' do
185
+ mr1 = Xenon::MediaRange.new('application', '*', 'q' => '0.3')
186
+ mr2 = Xenon::MediaRange.new('*', '*', 'q' => '0.5')
187
+ expect(mr1 <=> mr2).to eq(1)
188
+ end
189
+
190
+ it 'considers the quality when media ranges are equally specific' do
191
+ mr1 = Xenon::MediaRange.new('application', 'json', 'q' => '0.8')
192
+ mr2 = Xenon::MediaRange.new('application', 'xml')
193
+ expect(mr1 <=> mr2).to eq(-1)
194
+ end
195
+ end
196
+
197
+ %i(=~ ===).each do |name|
198
+ context "##{name}" do
199
+ it 'returns true when the type and subtype are wildcards' do
200
+ mr = Xenon::MediaRange.new('*', '*')
201
+ mt = Xenon::MediaType.new('application', 'json')
202
+ expect(mr.send(name, mt)).to eq(true)
203
+ end
204
+
205
+ it 'returns true when the type matches and subtype is a wildcard' do
206
+ mr = Xenon::MediaRange.new('application', '*')
207
+ mt = Xenon::MediaType.new('application', 'json')
208
+ expect(mr.send(name, mt)).to eq(true)
209
+ end
210
+
211
+ it 'returns true when the type and subtype match exactly' do
212
+ mr = Xenon::MediaRange.new('application', 'json')
213
+ mt = Xenon::MediaType.new('application', 'json')
214
+ expect(mr.send(name, mt)).to eq(true)
215
+ end
216
+
217
+ it 'returns true when the type, subtype and parameters match exactly' do
218
+ mr = Xenon::MediaRange.new('text', 'plain', 'format' => 'flowed')
219
+ mt = Xenon::MediaType.new('text', 'plain', 'format' => 'flowed', 'paged' => nil)
220
+ expect(mr.send(name, mt)).to eq(true)
221
+ end
222
+
223
+ it 'returns true when the the media type has more specific parameters' do
224
+ mr = Xenon::MediaRange.new('text', 'plain')
225
+ mt = Xenon::MediaType.new('text', 'plain', 'format' => 'flowed', 'paged' => nil)
226
+ expect(mr.send(name, mt)).to eq(true)
227
+ end
228
+
229
+ it 'returns false when the type is different' do
230
+ mr = Xenon::MediaRange.new('text', 'json')
231
+ mt = Xenon::MediaType.new('application', 'json')
232
+ expect(mr.send(name, mt)).to eq(false)
233
+ end
234
+
235
+ it 'returns false when the type matches but subtype is different' do
236
+ mr = Xenon::MediaRange.new('application', 'xml')
237
+ mt = Xenon::MediaType.new('application', 'json')
238
+ expect(mr.send(name, mt)).to eq(false)
239
+ end
240
+
241
+ it 'returns false when the media range has more specific parameters' do
242
+ mr = Xenon::MediaRange.new('text', 'plain', 'format' => 'flowed', 'paged' => nil)
243
+ mt = Xenon::MediaType.new('text', 'plain')
244
+ expect(mr.send(name, mt)).to eq(false)
245
+ end
246
+
247
+ it 'returns false when the media range has a different parameter value' do
248
+ mr = Xenon::MediaRange.new('text', 'plain', 'format' => 'flowed')
249
+ mt = Xenon::MediaType.new('text', 'plain', 'format' => 'linear')
250
+ expect(mr.send(name, mt)).to eq(false)
251
+ end
252
+ end
253
+ end
254
+
255
+ context '#to_s' do
256
+ it 'returns the string representation of a media range' do
257
+ mt = Xenon::MediaRange.new('text', 'plain', 'q' => 0.8, 'format' => 'flowed', 'paged' => nil)
258
+ expect(mt.to_s).to eq('text/plain; format=flowed; paged; q=0.8')
259
+ end
260
+
261
+ it 'omits the q parameter when it is 1.0' do
262
+ mt = Xenon::MediaRange.new('application', 'json', 'q' => 1.0)
263
+ expect(mt.to_s).to eq('application/json')
264
+ end
265
+ end
266
+
267
+ end
data/xenon.gemspec CHANGED
@@ -18,7 +18,12 @@ Gem::Specification.new do |spec|
18
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
19
  spec.require_paths = ['lib']
20
20
 
21
+ spec.required_ruby_version = '>= 2.2.0'
22
+
23
+ spec.add_runtime_dependency 'activesupport', '~> 4.0'
24
+ spec.add_runtime_dependency 'parslet', '~> 1.7'
25
+
21
26
  spec.add_development_dependency 'bundler', '~> 1.6'
22
- spec.add_development_dependency 'rake'
27
+ spec.add_development_dependency 'rake', '~> 10.0'
23
28
  spec.add_development_dependency 'rspec', '~> 3.0'
24
29
  end