roda 1.1.0 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG +70 -0
- data/README.rdoc +261 -302
- data/Rakefile +1 -1
- data/doc/release_notes/1.2.0.txt +406 -0
- data/lib/roda.rb +206 -124
- data/lib/roda/plugins/all_verbs.rb +11 -10
- data/lib/roda/plugins/assets.rb +5 -5
- data/lib/roda/plugins/backtracking_array.rb +12 -5
- data/lib/roda/plugins/caching.rb +10 -8
- data/lib/roda/plugins/class_level_routing.rb +94 -0
- data/lib/roda/plugins/content_for.rb +6 -0
- data/lib/roda/plugins/default_headers.rb +4 -11
- data/lib/roda/plugins/delay_build.rb +42 -0
- data/lib/roda/plugins/delegate.rb +64 -0
- data/lib/roda/plugins/drop_body.rb +33 -0
- data/lib/roda/plugins/empty_root.rb +48 -0
- data/lib/roda/plugins/environments.rb +68 -0
- data/lib/roda/plugins/error_email.rb +1 -2
- data/lib/roda/plugins/error_handler.rb +1 -1
- data/lib/roda/plugins/halt.rb +7 -5
- data/lib/roda/plugins/head.rb +4 -2
- data/lib/roda/plugins/header_matchers.rb +17 -9
- data/lib/roda/plugins/hooks.rb +16 -32
- data/lib/roda/plugins/json.rb +4 -10
- data/lib/roda/plugins/mailer.rb +233 -0
- data/lib/roda/plugins/match_affix.rb +48 -0
- data/lib/roda/plugins/multi_route.rb +9 -11
- data/lib/roda/plugins/multi_run.rb +81 -0
- data/lib/roda/plugins/named_templates.rb +93 -0
- data/lib/roda/plugins/not_allowed.rb +43 -48
- data/lib/roda/plugins/path.rb +63 -2
- data/lib/roda/plugins/render.rb +79 -48
- data/lib/roda/plugins/render_each.rb +6 -0
- data/lib/roda/plugins/sinatra_helpers.rb +523 -0
- data/lib/roda/plugins/slash_path_empty.rb +25 -0
- data/lib/roda/plugins/static_path_info.rb +64 -0
- data/lib/roda/plugins/streaming.rb +1 -1
- data/lib/roda/plugins/view_subdirs.rb +12 -8
- data/lib/roda/version.rb +1 -1
- data/spec/integration_spec.rb +33 -0
- data/spec/plugin/backtracking_array_spec.rb +24 -18
- data/spec/plugin/class_level_routing_spec.rb +138 -0
- data/spec/plugin/delay_build_spec.rb +23 -0
- data/spec/plugin/delegate_spec.rb +20 -0
- data/spec/plugin/drop_body_spec.rb +20 -0
- data/spec/plugin/empty_root_spec.rb +14 -0
- data/spec/plugin/environments_spec.rb +31 -0
- data/spec/plugin/h_spec.rb +1 -3
- data/spec/plugin/header_matchers_spec.rb +14 -0
- data/spec/plugin/hooks_spec.rb +3 -5
- data/spec/plugin/mailer_spec.rb +191 -0
- data/spec/plugin/match_affix_spec.rb +22 -0
- data/spec/plugin/multi_run_spec.rb +31 -0
- data/spec/plugin/named_templates_spec.rb +65 -0
- data/spec/plugin/path_spec.rb +66 -2
- data/spec/plugin/render_spec.rb +46 -1
- data/spec/plugin/sinatra_helpers_spec.rb +534 -0
- data/spec/plugin/slash_path_empty_spec.rb +22 -0
- data/spec/plugin/static_path_info_spec.rb +50 -0
- data/spec/request_spec.rb +23 -0
- data/spec/response_spec.rb +12 -1
- metadata +48 -6
data/spec/plugin/render_spec.rb
CHANGED
@@ -59,9 +59,14 @@ describe "render plugin" do
|
|
59
59
|
end
|
60
60
|
|
61
61
|
it "custom default layout support" do
|
62
|
-
app.
|
62
|
+
app.plugin :render, :layout => "layout-alternative"
|
63
63
|
body("/home").strip.should == "<title>Alternative Layout: Home</title>\n<h1>Home</h1>\n<p>Hello Agent Smith</p>"
|
64
64
|
end
|
65
|
+
|
66
|
+
it "using hash for :layout" do
|
67
|
+
app.plugin :render, :layout => {:inline=> 'a<%= yield %>b'}
|
68
|
+
body("/home").strip.should == "a<h1>Home</h1>\n<p>Hello Agent Smith</p>\nb"
|
69
|
+
end
|
65
70
|
end
|
66
71
|
|
67
72
|
describe "render plugin" do
|
@@ -119,6 +124,46 @@ describe "render plugin" do
|
|
119
124
|
body.strip.should == '<%= bar %>'
|
120
125
|
end
|
121
126
|
|
127
|
+
it "template renders with :template opts" do
|
128
|
+
app(:render) do
|
129
|
+
render_opts[:views] = "./spec/views"
|
130
|
+
render(:template=>"about", :locals=>{:title => "About Roda"})
|
131
|
+
end
|
132
|
+
body.strip.should == "<h1>About Roda</h1>"
|
133
|
+
end
|
134
|
+
|
135
|
+
it "template renders with :template_class opts" do
|
136
|
+
app(:render) do
|
137
|
+
@a = 1
|
138
|
+
render(:inline=>'i#{@a}', :template_class=>::Tilt[:str])
|
139
|
+
end
|
140
|
+
body.should == "i1"
|
141
|
+
end
|
142
|
+
|
143
|
+
it "template cache respects :opts" do
|
144
|
+
c = Class.new do
|
145
|
+
def initialize(path, _, opts)
|
146
|
+
@path = path
|
147
|
+
@opts = opts
|
148
|
+
end
|
149
|
+
def render(*)
|
150
|
+
"#{@path}-#{@opts[:foo]}"
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
app(:render) do |r|
|
155
|
+
r.is "a" do
|
156
|
+
render(:inline=>"i", :template_class=>c, :opts=>{:foo=>'a'})
|
157
|
+
end
|
158
|
+
r.is "b" do
|
159
|
+
render(:inline=>"i", :template_class=>c, :opts=>{:foo=>'b'})
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
body('/a').should == "i-a"
|
164
|
+
body('/b').should == "i-b"
|
165
|
+
end
|
166
|
+
|
122
167
|
it "render_opts inheritance" do
|
123
168
|
c = Class.new(Roda)
|
124
169
|
c.plugin :render
|
@@ -0,0 +1,534 @@
|
|
1
|
+
require File.expand_path("spec_helper", File.dirname(File.dirname(__FILE__)))
|
2
|
+
|
3
|
+
require 'uri'
|
4
|
+
|
5
|
+
describe "sinatra_helpers plugin" do
|
6
|
+
def sin_app(&block)
|
7
|
+
app(:sinatra_helpers, &block)
|
8
|
+
end
|
9
|
+
|
10
|
+
def status_app(code, &block)
|
11
|
+
#code += 2 if [204, 205, 304].include? code
|
12
|
+
block ||= proc{}
|
13
|
+
sin_app do |r|
|
14
|
+
status code
|
15
|
+
instance_eval(&block).inspect
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'status returns the response status code if not given an argument' do
|
20
|
+
status_app(207){status}
|
21
|
+
body.should == "207"
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'status sets the response status code if given an argument' do
|
25
|
+
status_app 207
|
26
|
+
status.should == 207
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'not_found? is true only if status == 404' do
|
30
|
+
status_app(404){not_found?}
|
31
|
+
body.should == 'true'
|
32
|
+
status_app(405){not_found?}
|
33
|
+
body.should == 'false'
|
34
|
+
status_app(403){not_found?}
|
35
|
+
body.should == 'false'
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'informational? is true only for 1xx status' do
|
39
|
+
status_app(100 + rand(100)){informational?}
|
40
|
+
body.should == 'true'
|
41
|
+
status_app(200 + rand(400)){informational?}
|
42
|
+
body.should == 'false'
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'success? is true only for 2xx status' do
|
46
|
+
status_app(200 + rand(100)){success?}
|
47
|
+
body.should == 'true'
|
48
|
+
status_app(100 + rand(100)){success?}
|
49
|
+
body.should == 'false'
|
50
|
+
status_app(300 + rand(300)){success?}
|
51
|
+
body.should == 'false'
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'redirect? is true only for 3xx status' do
|
55
|
+
status_app(300 + rand(100)){redirect?}
|
56
|
+
body.should == 'true'
|
57
|
+
status_app(200 + rand(100)){redirect?}
|
58
|
+
body.should == 'false'
|
59
|
+
status_app(400 + rand(200)){redirect?}
|
60
|
+
body.should == 'false'
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'client_error? is true only for 4xx status' do
|
64
|
+
status_app(400 + rand(100)){client_error?}
|
65
|
+
body.should == 'true'
|
66
|
+
status_app(200 + rand(200)){client_error?}
|
67
|
+
body.should == 'false'
|
68
|
+
status_app(500 + rand(100)){client_error?}
|
69
|
+
body.should == 'false'
|
70
|
+
end
|
71
|
+
|
72
|
+
it 'server_error? is true only for 5xx status' do
|
73
|
+
status_app(500 + rand(100)){server_error?}
|
74
|
+
body.should == 'true'
|
75
|
+
status_app(200 + rand(300)){server_error?}
|
76
|
+
body.should == 'false'
|
77
|
+
end
|
78
|
+
|
79
|
+
describe 'body' do
|
80
|
+
it 'takes a block for deferred body generation' do
|
81
|
+
sin_app{body{'Hello World'}; nil}
|
82
|
+
body.should == 'Hello World'
|
83
|
+
header('Content-Length').should == '11'
|
84
|
+
end
|
85
|
+
|
86
|
+
it 'takes a String, Array, or other object responding to #each' do
|
87
|
+
sin_app{body 'Hello World'; nil}
|
88
|
+
body.should == 'Hello World'
|
89
|
+
header('Content-Length').should == '11'
|
90
|
+
|
91
|
+
sin_app{body ['Hello ', 'World']; nil}
|
92
|
+
body.should == 'Hello World'
|
93
|
+
header('Content-Length').should == '11'
|
94
|
+
|
95
|
+
o = Object.new
|
96
|
+
def o.each; yield 'Hello World' end
|
97
|
+
sin_app{body o; nil}
|
98
|
+
body.should == 'Hello World'
|
99
|
+
header('Content-Length').should == '11'
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
describe 'redirect' do
|
104
|
+
it 'uses a 302 when only a path is given' do
|
105
|
+
sin_app do
|
106
|
+
redirect '/foo'
|
107
|
+
fail 'redirect should halt'
|
108
|
+
end
|
109
|
+
|
110
|
+
status.should == 302
|
111
|
+
body.should == ''
|
112
|
+
header('Location').should == '/foo'
|
113
|
+
end
|
114
|
+
|
115
|
+
it 'adds script_name if given a path' do
|
116
|
+
sin_app{redirect "/foo"}
|
117
|
+
header('Location', '/bar', 'SCRIPT_NAME'=>'/foo').should == '/foo'
|
118
|
+
end
|
119
|
+
|
120
|
+
it 'does not adds script_name if not given a path' do
|
121
|
+
sin_app{redirect}
|
122
|
+
header('Location', '/bar', 'SCRIPT_NAME'=>'/foo', 'REQUEST_METHOD'=>'POST').should == '/foo/bar'
|
123
|
+
end
|
124
|
+
|
125
|
+
it 'respects :absolute_redirects option' do
|
126
|
+
sin_app{redirect}
|
127
|
+
app.opts[:absolute_redirects] = true
|
128
|
+
header('Location', '/bar', 'HTTP_HOST'=>'example.org', 'SCRIPT_NAME'=>'/foo', 'REQUEST_METHOD'=>'POST').should == 'http://example.org/foo/bar'
|
129
|
+
end
|
130
|
+
|
131
|
+
it 'respects :prefixed_redirects option' do
|
132
|
+
sin_app{redirect "/bar"}
|
133
|
+
app.opts[:prefixed_redirects] = true
|
134
|
+
header('Location', 'SCRIPT_NAME'=>'/foo').should == '/foo/bar'
|
135
|
+
end
|
136
|
+
|
137
|
+
it 'ignores :prefix_redirects option if not given a path' do
|
138
|
+
sin_app{redirect}
|
139
|
+
app.opts[:prefix_redirects] = true
|
140
|
+
header('Location', "/bar", 'SCRIPT_NAME'=>'/foo', 'REQUEST_METHOD'=>'POST').should == '/foo/bar'
|
141
|
+
end
|
142
|
+
|
143
|
+
it 'uses the code given when specified' do
|
144
|
+
sin_app{redirect '/foo', 301}
|
145
|
+
status.should == 301
|
146
|
+
end
|
147
|
+
|
148
|
+
it 'redirects back to request.referer when passed back' do
|
149
|
+
sin_app{redirect back}
|
150
|
+
header('Location', 'HTTP_REFERER' => '/foo').should == '/foo'
|
151
|
+
end
|
152
|
+
|
153
|
+
it 'uses 303 for post requests if request is HTTP 1.1, 302 for 1.0' do
|
154
|
+
sin_app{redirect '/foo'}
|
155
|
+
status('HTTP_VERSION' => 'HTTP/1.1', 'REQUEST_METHOD'=>'POST').should == 303
|
156
|
+
status('HTTP_VERSION' => 'HTTP/1.0', 'REQUEST_METHOD'=>'POST').should == 302
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
describe 'error' do
|
161
|
+
it 'sets a status code and halts' do
|
162
|
+
sin_app do
|
163
|
+
error
|
164
|
+
fail 'error should halt'
|
165
|
+
end
|
166
|
+
|
167
|
+
status.should == 500
|
168
|
+
body.should == ''
|
169
|
+
end
|
170
|
+
|
171
|
+
it 'accepts status code' do
|
172
|
+
sin_app{error 501}
|
173
|
+
status.should == 501
|
174
|
+
body.should == ''
|
175
|
+
end
|
176
|
+
|
177
|
+
it 'accepts body' do
|
178
|
+
sin_app{error '501'}
|
179
|
+
status.should == 500
|
180
|
+
body.should == '501'
|
181
|
+
end
|
182
|
+
|
183
|
+
it 'accepts status code and body' do
|
184
|
+
sin_app{error 502, '501'}
|
185
|
+
status.should == 502
|
186
|
+
body.should == '501'
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
describe 'not_found' do
|
191
|
+
it 'halts with a 404 status' do
|
192
|
+
sin_app do
|
193
|
+
not_found
|
194
|
+
fail 'not_found should halt'
|
195
|
+
end
|
196
|
+
|
197
|
+
status.should == 404
|
198
|
+
body.should == ''
|
199
|
+
end
|
200
|
+
|
201
|
+
it 'accepts optional body' do
|
202
|
+
sin_app{not_found 'nf'}
|
203
|
+
status.should == 404
|
204
|
+
body.should == 'nf'
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
describe 'headers' do
|
209
|
+
it 'sets headers on the response object when given a Hash' do
|
210
|
+
sin_app do
|
211
|
+
headers 'X-Foo' => 'bar'
|
212
|
+
'kthx'
|
213
|
+
end
|
214
|
+
|
215
|
+
header('X-Foo').should == 'bar'
|
216
|
+
body.should == 'kthx'
|
217
|
+
end
|
218
|
+
|
219
|
+
it 'returns the response headers hash when no hash provided' do
|
220
|
+
sin_app{headers['X-Foo'] = 'bar'}
|
221
|
+
header('X-Foo').should == 'bar'
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
describe 'mime_type' do
|
226
|
+
before do
|
227
|
+
sin_app{|r| mime_type(r.path).to_s}
|
228
|
+
end
|
229
|
+
|
230
|
+
it "looks up mime types in Rack's MIME registry" do
|
231
|
+
Rack::Mime::MIME_TYPES['.foo'] = 'application/foo'
|
232
|
+
body('foo').should == 'application/foo'
|
233
|
+
body(:foo).should == 'application/foo'
|
234
|
+
body('.foo').should == 'application/foo'
|
235
|
+
end
|
236
|
+
|
237
|
+
it 'returns nil when given nil' do
|
238
|
+
body('PATH_INFO'=>nil).should == ''
|
239
|
+
end
|
240
|
+
|
241
|
+
it 'returns nil when media type not registered' do
|
242
|
+
body('bizzle').should == ''
|
243
|
+
end
|
244
|
+
|
245
|
+
it 'returns the argument when given a media type string' do
|
246
|
+
body('text/plain').should == 'text/plain'
|
247
|
+
end
|
248
|
+
|
249
|
+
it 'supports mime types registered at the class level' do
|
250
|
+
app.mime_type :foo, 'application/foo'
|
251
|
+
body(:foo).should == 'application/foo'
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
describe 'content_type' do
|
256
|
+
it 'sets the Content-Type header' do
|
257
|
+
sin_app do
|
258
|
+
content_type 'text/plain'
|
259
|
+
'Hello World'
|
260
|
+
end
|
261
|
+
|
262
|
+
header('Content-Type').should == 'text/plain'
|
263
|
+
body.should == 'Hello World'
|
264
|
+
end
|
265
|
+
|
266
|
+
it 'takes media type parameters (like charset=)' do
|
267
|
+
sin_app{content_type 'text/html', :charset => 'latin1'}
|
268
|
+
header('Content-Type').should == 'text/html;charset=latin1'
|
269
|
+
end
|
270
|
+
|
271
|
+
it "looks up symbols in Rack's mime types dictionary" do
|
272
|
+
sin_app{content_type :foo}
|
273
|
+
Rack::Mime::MIME_TYPES['.foo'] = 'application/foo'
|
274
|
+
header('Content-Type').should == 'application/foo'
|
275
|
+
end
|
276
|
+
|
277
|
+
it 'fails when no mime type is registered for the argument provided' do
|
278
|
+
sin_app{content_type :bizzle}
|
279
|
+
proc{body}.should raise_error(Roda::RodaError)
|
280
|
+
end
|
281
|
+
|
282
|
+
it 'handles already present params' do
|
283
|
+
sin_app{content_type 'foo/bar;level=1', :charset => 'utf-8'}
|
284
|
+
header('Content-Type').should == 'foo/bar;level=1, charset=utf-8'
|
285
|
+
end
|
286
|
+
|
287
|
+
it 'does not add charset if present' do
|
288
|
+
sin_app{content_type 'text/plain;charset=utf-16', :charset => 'utf-8'}
|
289
|
+
header('Content-Type').should == 'text/plain;charset=utf-16'
|
290
|
+
end
|
291
|
+
|
292
|
+
it 'properly encodes parameters with delimiter characters' do
|
293
|
+
sin_app{|r| content_type 'image/png', :comment => r.path }
|
294
|
+
header('Content-Type', 'Hello, world!').should == 'image/png;comment="Hello, world!"'
|
295
|
+
header('Content-Type', 'semi;colon').should == 'image/png;comment="semi;colon"'
|
296
|
+
header('Content-Type', '"Whatever."').should == 'image/png;comment="\"Whatever.\""'
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
describe 'attachment' do
|
301
|
+
before do
|
302
|
+
sin_app{|r| attachment r.path; 'b'}
|
303
|
+
end
|
304
|
+
|
305
|
+
it 'sets the Content-Disposition header' do
|
306
|
+
header('Content-Disposition', '/foo/test.xml').should == 'attachment; filename="test.xml"'
|
307
|
+
body.should == 'b'
|
308
|
+
end
|
309
|
+
|
310
|
+
it 'sets the Content-Disposition header even when a filename is not given' do
|
311
|
+
sin_app{attachment}
|
312
|
+
header('Content-Disposition', '/foo/test.xml').should == 'attachment'
|
313
|
+
end
|
314
|
+
|
315
|
+
it 'sets the Content-Type header' do
|
316
|
+
header('Content-Type', 'test.xml').should == 'application/xml'
|
317
|
+
end
|
318
|
+
|
319
|
+
it 'does not modify the default Content-Type without a file extension' do
|
320
|
+
header('Content-Type', 'README').should == 'text/html'
|
321
|
+
end
|
322
|
+
|
323
|
+
it 'should not modify the Content-Type if it is already set' do
|
324
|
+
sin_app do
|
325
|
+
content_type :atom
|
326
|
+
attachment 'test.xml'
|
327
|
+
end
|
328
|
+
|
329
|
+
header('Content-Type', 'README').should == 'application/atom+xml'
|
330
|
+
end
|
331
|
+
end
|
332
|
+
|
333
|
+
describe 'send_file' do
|
334
|
+
before(:all) do
|
335
|
+
file = @file = 'spec/assets/css/raw.css'
|
336
|
+
@content = File.read(@file)
|
337
|
+
sin_app{send_file file, env['OPTS'] || {}}
|
338
|
+
end
|
339
|
+
|
340
|
+
it "sends the contents of the file" do
|
341
|
+
status.should == 200
|
342
|
+
body.should == @content
|
343
|
+
end
|
344
|
+
|
345
|
+
it 'sets the Content-Type response header if a mime-type can be located' do
|
346
|
+
header('Content-Type').should == 'text/css'
|
347
|
+
end
|
348
|
+
|
349
|
+
it 'sets the Content-Type response header if type option is set to a file extension' do
|
350
|
+
header('Content-Type', 'OPTS'=>{:type => 'html'}).should == 'text/html'
|
351
|
+
end
|
352
|
+
|
353
|
+
it 'sets the Content-Type response header if type option is set to a mime type' do
|
354
|
+
header('Content-Type', 'OPTS'=>{:type => 'application/octet-stream'}).should == 'application/octet-stream'
|
355
|
+
end
|
356
|
+
|
357
|
+
it 'sets the Content-Length response header' do
|
358
|
+
header('Content-Length').should == @content.length.to_s
|
359
|
+
end
|
360
|
+
|
361
|
+
it 'sets the Last-Modified response header' do
|
362
|
+
header('Last-Modified').should == File.mtime(@file).httpdate
|
363
|
+
end
|
364
|
+
|
365
|
+
it 'allows passing in a different Last-Modified response header with :last_modified' do
|
366
|
+
time = Time.now
|
367
|
+
@app.plugin :caching
|
368
|
+
header('Last-Modified', 'OPTS'=>{:last_modified => time}).should == time.httpdate
|
369
|
+
end
|
370
|
+
|
371
|
+
it "returns a 404 when not found" do
|
372
|
+
sin_app{send_file 'this-file-does-not-exist.txt'}
|
373
|
+
status.should == 404
|
374
|
+
end
|
375
|
+
|
376
|
+
it "does not set the Content-Disposition header by default" do
|
377
|
+
header('Content-Disposition').should == nil
|
378
|
+
end
|
379
|
+
|
380
|
+
it "sets the Content-Disposition header when :disposition set to 'attachment'" do
|
381
|
+
header('Content-Disposition', 'OPTS'=>{:disposition => 'attachment'}).should == 'attachment; filename="raw.css"'
|
382
|
+
end
|
383
|
+
|
384
|
+
it "does not set add a file name if filename is false" do
|
385
|
+
header('Content-Disposition', 'OPTS'=>{:disposition => 'inline', :filename=>false}).should == 'inline'
|
386
|
+
end
|
387
|
+
|
388
|
+
it "sets the Content-Disposition header when :disposition set to 'inline'" do
|
389
|
+
header('Content-Disposition', 'OPTS'=>{:disposition => 'inline'}).should == 'inline; filename="raw.css"'
|
390
|
+
end
|
391
|
+
|
392
|
+
it "sets the Content-Disposition header when :filename provided" do
|
393
|
+
header('Content-Disposition', 'OPTS'=>{:filename => 'foo.txt'}).should == 'attachment; filename="foo.txt"'
|
394
|
+
end
|
395
|
+
|
396
|
+
it 'allows setting a custom status code' do
|
397
|
+
status('OPTS'=>{:status=>201}).should == 201
|
398
|
+
end
|
399
|
+
|
400
|
+
it "is able to send files with unknown mime type" do
|
401
|
+
header('Content-Type', 'OPTS'=>{:type => '.foobar'}).should == 'application/octet-stream'
|
402
|
+
end
|
403
|
+
|
404
|
+
it "does not override Content-Type if already set and no explicit type is given" do
|
405
|
+
file = @file
|
406
|
+
sin_app do
|
407
|
+
content_type :png
|
408
|
+
send_file file
|
409
|
+
end
|
410
|
+
header('Content-Type').should == 'image/png'
|
411
|
+
end
|
412
|
+
|
413
|
+
it "does override Content-Type even if already set, if explicit type is given" do
|
414
|
+
file = @file
|
415
|
+
sin_app do
|
416
|
+
content_type :png
|
417
|
+
send_file file, :type => :gif
|
418
|
+
end
|
419
|
+
header('Content-Type').should == 'image/gif'
|
420
|
+
end
|
421
|
+
end
|
422
|
+
|
423
|
+
describe 'uri' do
|
424
|
+
describe "without arguments" do
|
425
|
+
before do
|
426
|
+
sin_app{uri}
|
427
|
+
end
|
428
|
+
|
429
|
+
it 'generates absolute urls' do
|
430
|
+
body('HTTP_HOST'=>'example.org').should == 'http://example.org/'
|
431
|
+
end
|
432
|
+
|
433
|
+
it 'includes path_info' do
|
434
|
+
body('/foo', 'HTTP_HOST'=>'example.org').should == 'http://example.org/foo'
|
435
|
+
end
|
436
|
+
|
437
|
+
it 'includes script_name' do
|
438
|
+
body('/bar', 'HTTP_HOST'=>'example.org', "SCRIPT_NAME" => '/foo').should == 'http://example.org/foo/bar'
|
439
|
+
end
|
440
|
+
|
441
|
+
it 'handles standard HTTP and HTTPS ports' do
|
442
|
+
body('SERVER_NAME'=>'example.org', 'SERVER_PORT' => '80').should == 'http://example.org/'
|
443
|
+
body('SERVER_NAME'=>'example.org', 'SERVER_PORT' => '443', 'HTTPS'=>'on').should == 'https://example.org/'
|
444
|
+
end
|
445
|
+
|
446
|
+
it 'handles non-standard HTTP port' do
|
447
|
+
body('SERVER_NAME'=>'example.org', 'SERVER_PORT' => '81').should == 'http://example.org:81/'
|
448
|
+
body('SERVER_NAME'=>'example.org', 'SERVER_PORT' => '443').should == 'http://example.org:443/'
|
449
|
+
end
|
450
|
+
|
451
|
+
it 'handles non-standard HTTPS port' do
|
452
|
+
body('SERVER_NAME'=>'example.org', 'SERVER_PORT' => '444', 'HTTPS'=>'on').should == 'https://example.org:444/'
|
453
|
+
body('SERVER_NAME'=>'example.org', 'SERVER_PORT' => '80', 'HTTPS'=>'on').should == 'https://example.org:80/'
|
454
|
+
end
|
455
|
+
|
456
|
+
it 'handles reverse proxy' do
|
457
|
+
body('SERVER_NAME'=>'example.org', 'HTTP_X_FORWARDED_HOST' => 'example.com', 'SERVER_PORT' => '8080').should == 'http://example.com/'
|
458
|
+
end
|
459
|
+
end
|
460
|
+
|
461
|
+
it 'allows passing an alternative to path_info' do
|
462
|
+
sin_app{uri '/bar'}
|
463
|
+
body('HTTP_HOST'=>'example.org').should == 'http://example.org/bar'
|
464
|
+
body('HTTP_HOST'=>'example.org', "SCRIPT_NAME" => '/foo').should == 'http://example.org/foo/bar'
|
465
|
+
end
|
466
|
+
|
467
|
+
it 'handles absolute URIs' do
|
468
|
+
sin_app{uri 'http://google.com'}
|
469
|
+
body('HTTP_HOST'=>'example.org').should == 'http://google.com'
|
470
|
+
end
|
471
|
+
|
472
|
+
it 'handles different protocols' do
|
473
|
+
sin_app{uri 'mailto:jsmith@example.com'}
|
474
|
+
body('HTTP_HOST'=>'example.org').should == 'mailto:jsmith@example.com'
|
475
|
+
end
|
476
|
+
|
477
|
+
it 'allows turning off host' do
|
478
|
+
sin_app{uri '/foo', false}
|
479
|
+
body('HTTP_HOST'=>'example.org').should == '/foo'
|
480
|
+
body('HTTP_HOST'=>'example.org', "SCRIPT_NAME" => '/bar').should == '/bar/foo'
|
481
|
+
end
|
482
|
+
|
483
|
+
it 'allows turning off script_name' do
|
484
|
+
sin_app{uri '/foo', true, false}
|
485
|
+
body('HTTP_HOST'=>'example.org').should == 'http://example.org/foo'
|
486
|
+
body('HTTP_HOST'=>'example.org', "SCRIPT_NAME" => '/bar').should == 'http://example.org/foo'
|
487
|
+
end
|
488
|
+
|
489
|
+
it 'is aliased to #url' do
|
490
|
+
sin_app{url}
|
491
|
+
body('HTTP_HOST'=>'example.org').should == 'http://example.org/'
|
492
|
+
end
|
493
|
+
|
494
|
+
it 'is aliased to #to' do
|
495
|
+
sin_app{to}
|
496
|
+
body('HTTP_HOST'=>'example.org').should == 'http://example.org/'
|
497
|
+
end
|
498
|
+
|
499
|
+
it 'accepts a URI object instead of a String' do
|
500
|
+
sin_app{uri URI.parse('http://roda.jeremyevans.net')}
|
501
|
+
body.should == 'http://roda.jeremyevans.net'
|
502
|
+
end
|
503
|
+
end
|
504
|
+
|
505
|
+
it 'logger logs to rack.logger' do
|
506
|
+
sin_app{logger.info "foo"}
|
507
|
+
o = Object.new
|
508
|
+
def o.method_missing(*a)
|
509
|
+
(@a ||= []) << a
|
510
|
+
end
|
511
|
+
def o.logs
|
512
|
+
@a
|
513
|
+
end
|
514
|
+
|
515
|
+
status('rack.logger'=>o).should == 404
|
516
|
+
o.logs.should == [[:info, 'foo']]
|
517
|
+
end
|
518
|
+
|
519
|
+
it 'supports disabling delegation if :delegate=>false option is provided' do
|
520
|
+
app(:bare) do
|
521
|
+
plugin :sinatra_helpers, :delegate=>false
|
522
|
+
route do |r|
|
523
|
+
r.root{content_type}
|
524
|
+
r.is("req"){r.ssl?.to_s}
|
525
|
+
r.is("res"){response.not_found?.inspect}
|
526
|
+
end
|
527
|
+
end
|
528
|
+
|
529
|
+
proc{body}.should raise_error(NameError)
|
530
|
+
body('/req').should == 'false'
|
531
|
+
body('/res').should == 'nil'
|
532
|
+
end
|
533
|
+
end
|
534
|
+
|