roda 1.1.0 → 1.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/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
|
+
|