roda 3.17.0 → 3.18.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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +48 -0
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +22 -4
  5. data/doc/release_notes/3.18.0.txt +170 -0
  6. data/lib/roda.rb +249 -26
  7. data/lib/roda/plugins/_after_hook.rb +4 -26
  8. data/lib/roda/plugins/_before_hook.rb +30 -2
  9. data/lib/roda/plugins/branch_locals.rb +2 -2
  10. data/lib/roda/plugins/class_level_routing.rb +9 -7
  11. data/lib/roda/plugins/default_headers.rb +15 -1
  12. data/lib/roda/plugins/default_status.rb +9 -10
  13. data/lib/roda/plugins/direct_call.rb +38 -0
  14. data/lib/roda/plugins/error_email.rb +1 -1
  15. data/lib/roda/plugins/error_handler.rb +37 -11
  16. data/lib/roda/plugins/hooks.rb +28 -30
  17. data/lib/roda/plugins/mail_processor.rb +16 -11
  18. data/lib/roda/plugins/mailer.rb +1 -1
  19. data/lib/roda/plugins/middleware.rb +13 -3
  20. data/lib/roda/plugins/multi_route.rb +3 -3
  21. data/lib/roda/plugins/named_templates.rb +4 -4
  22. data/lib/roda/plugins/path.rb +13 -8
  23. data/lib/roda/plugins/render.rb +2 -2
  24. data/lib/roda/plugins/route_block_args.rb +4 -3
  25. data/lib/roda/plugins/route_csrf.rb +9 -4
  26. data/lib/roda/plugins/sessions.rb +2 -1
  27. data/lib/roda/plugins/shared_vars.rb +1 -1
  28. data/lib/roda/plugins/static_routing.rb +7 -17
  29. data/lib/roda/plugins/status_handler.rb +5 -3
  30. data/lib/roda/plugins/view_options.rb +2 -2
  31. data/lib/roda/version.rb +1 -1
  32. data/spec/define_roda_method_spec.rb +257 -0
  33. data/spec/plugin/class_level_routing_spec.rb +0 -27
  34. data/spec/plugin/default_headers_spec.rb +7 -0
  35. data/spec/plugin/default_status_spec.rb +31 -1
  36. data/spec/plugin/direct_call_spec.rb +28 -0
  37. data/spec/plugin/error_handler_spec.rb +27 -0
  38. data/spec/plugin/hooks_spec.rb +21 -0
  39. data/spec/plugin/middleware_spec.rb +108 -36
  40. data/spec/plugin/multi_route_spec.rb +12 -0
  41. data/spec/plugin/route_csrf_spec.rb +27 -0
  42. data/spec/plugin/sessions_spec.rb +26 -1
  43. data/spec/plugin/static_routing_spec.rb +25 -3
  44. data/spec/plugin/status_handler_spec.rb +17 -0
  45. data/spec/route_spec.rb +39 -0
  46. data/spec/spec_helper.rb +2 -2
  47. metadata +9 -3
@@ -161,31 +161,4 @@ describe "class_level_routing plugin" do
161
161
 
162
162
  proc{app.on{}}.must_raise
163
163
  end
164
-
165
- it 'works with route_block_args plugin' do
166
- app(:bare) do
167
- plugin :class_level_routing
168
- plugin :route_block_args do
169
- [request.path]
170
- end
171
-
172
- root do |path|
173
- "root-#{path}"
174
- end
175
-
176
- get do |path|
177
- "GET-#{path}"
178
- end
179
-
180
- route do |path|
181
- request.get('foo') do
182
- path
183
- end
184
- end
185
- end
186
-
187
- body.must_equal 'root-/'
188
- body('/a').must_equal 'GET-/a'
189
- body('/foo').must_equal '/foo'
190
- end
191
164
  end
@@ -72,4 +72,11 @@ describe "default_headers plugin" do
72
72
 
73
73
  req[1].must_equal h
74
74
  end
75
+
76
+ it "should offer default_headers method on class and response instance" do
77
+ h = {'Content-Type'=>'text/json', 'Foo'=>'bar'}
78
+ app.plugin :default_headers, h
79
+ app.default_headers.must_equal h
80
+ app::RodaResponse.new.default_headers.must_equal h
81
+ end
75
82
  end
@@ -14,7 +14,7 @@ describe "default_status plugin" do
14
14
  status.must_equal 201
15
15
  end
16
16
 
17
- it "should instance_exec the plugin block" do
17
+ it "should exec the plugin block in the context of the instance" do
18
18
  app(:bare) do
19
19
  plugin :default_status do
20
20
  200 + @body[0].length
@@ -62,4 +62,34 @@ describe "default_status plugin" do
62
62
  it "should raise if not given a block" do
63
63
  proc{app(:default_status)}.must_raise Roda::RodaError
64
64
  end
65
+
66
+ [true, false].each do |warn_arity|
67
+ send(warn_arity ? :deprecated : :it, "works with blocks with invalid arity") do
68
+ app(:bare) do
69
+ opts[:check_arity] = :warn if warn_arity
70
+ plugin :default_status do |r|
71
+ 201
72
+ end
73
+ route do |r|
74
+ r.halt response.finish_with_body([])
75
+ end
76
+ end
77
+
78
+ status.must_equal 201
79
+ end
80
+ end
81
+
82
+ it "does not work with blocks with invalid arity if :check_arity app option is false" do
83
+ app(:bare) do
84
+ opts[:check_arity] = false
85
+ plugin :default_status do |r|
86
+ 201
87
+ end
88
+ route do |r|
89
+ r.halt response.finish_with_body([])
90
+ end
91
+ end
92
+
93
+ proc{status}.must_raise ArgumentError
94
+ end
65
95
  end
@@ -0,0 +1,28 @@
1
+ require_relative "../spec_helper"
2
+
3
+ describe "direct_call plugin" do
4
+ it "should have .call skip middleware" do
5
+ app{'123'}
6
+ app.use(Class.new do
7
+ def initialize(_) end
8
+ def call(env) [200, {}, ['321']] end
9
+ end)
10
+ body.must_equal '321'
11
+ app.plugin :direct_call
12
+ body.must_equal '123'
13
+ end
14
+
15
+ deprecated "should work when #call is overridden" do
16
+ app.class_eval do
17
+ def call; super end
18
+ route{'123'}
19
+ end
20
+ app.use(Class.new do
21
+ def initialize(_) end
22
+ def call(env) [200, {}, ['321']] end
23
+ end)
24
+ body.must_equal '321'
25
+ app.plugin :direct_call
26
+ body.must_equal '123'
27
+ end
28
+ end
@@ -24,6 +24,33 @@ describe "error_handler plugin" do
24
24
  status.must_equal 500
25
25
  end
26
26
 
27
+ deprecated "works if #call is overridden" do
28
+ app(:bare) do
29
+ plugin :error_handler
30
+
31
+ def call
32
+ super
33
+ end
34
+
35
+ error do |e|
36
+ e.message
37
+ end
38
+
39
+ route do |r|
40
+ r.on "a" do
41
+ "found"
42
+ end
43
+
44
+ raise ArgumentError, "bad idea"
45
+ end
46
+ end
47
+
48
+ body("/a").must_equal 'found'
49
+ status("/a").must_equal 200
50
+ body.must_equal 'bad idea'
51
+ status.must_equal 500
52
+ end
53
+
27
54
  it "executes on SyntaxError exceptions" do
28
55
  app(:bare) do
29
56
  plugin :error_handler
@@ -44,6 +44,15 @@ describe "hooks plugin" do
44
44
  @a.must_equal [[200, 'baz', ['bar']]]
45
45
  end
46
46
 
47
+ it "works when freezing the app" do
48
+ app.freeze
49
+ s, h, b = req
50
+ s.must_equal 201
51
+ h['foo'].must_equal 'baz'
52
+ b.join.must_equal 'bar'
53
+ @a.must_equal [[200, 'baz', ['bar']]]
54
+ end
55
+
47
56
  it "after hooks are still called if an exception is raised" do
48
57
  a = @a
49
58
  @app.before do
@@ -128,4 +137,16 @@ describe "hooks plugin" do
128
137
  body('/a').must_equal "error"
129
138
  body('/b').must_equal "error"
130
139
  end
140
+
141
+ deprecated "should work if #call is overridden" do
142
+ app.class_eval do
143
+ def call; super end
144
+ end
145
+ app.route(&app.route_block)
146
+ s, h, b = req
147
+ s.must_equal 201
148
+ h['foo'].must_equal 'baz'
149
+ b.join.must_equal 'bar'
150
+ @a.must_equal [[200, 'baz', ['bar']]]
151
+ end
131
152
  end
@@ -1,53 +1,62 @@
1
1
  require_relative "../spec_helper"
2
2
 
3
3
  describe "middleware plugin" do
4
- it "turns Roda app into middlware" do
5
- a2 = app(:bare) do
6
- plugin :middleware
7
-
8
- route do |r|
9
- r.is "a" do
10
- "a2"
4
+ [false, true].each do |def_call|
5
+ meth = def_call ? :deprecated : :it
6
+ send meth, "turns Roda app into middlware" do
7
+ a2 = app(:bare) do
8
+ plugin :middleware
9
+
10
+ if def_call
11
+ def call
12
+ super
13
+ end
11
14
  end
12
- r.post "b" do
13
- "b2"
15
+
16
+ route do |r|
17
+ r.is "a" do
18
+ "a2"
19
+ end
20
+ r.post "b" do
21
+ "b2"
22
+ end
14
23
  end
15
24
  end
16
- end
17
25
 
18
- a3 = app(:bare) do
19
- plugin :middleware
26
+ a3 = app(:bare) do
27
+ plugin :middleware
20
28
 
21
- route do |r|
22
- r.get "a" do
23
- "a3"
24
- end
25
- r.get "b" do
26
- "b3"
29
+ route do |r|
30
+ r.get "a" do
31
+ "a3"
32
+ end
33
+ r.get "b" do
34
+ "b3"
35
+ end
27
36
  end
28
37
  end
29
- end
30
-
31
- app(:bare) do
32
- use a3
33
- use a2
34
38
 
35
- route do |r|
36
- r.is "a" do
37
- "a1"
38
- end
39
- r.is "b" do
40
- "b1"
39
+ app(:bare) do
40
+ use a3
41
+ use a2
42
+
43
+ route do |r|
44
+ r.is "a" do
45
+ "a1"
46
+ end
47
+ r.is "b" do
48
+ "b1"
49
+ end
41
50
  end
42
51
  end
43
- end
44
52
 
45
- body('/a').must_equal 'a3'
46
- body('/b').must_equal 'b3'
47
- body('/a', 'REQUEST_METHOD'=>'POST').must_equal 'a2'
48
- body('/b', 'REQUEST_METHOD'=>'POST').must_equal 'b2'
49
- body('/a', 'REQUEST_METHOD'=>'PATCH').must_equal 'a2'
50
- body('/b', 'REQUEST_METHOD'=>'PATCH').must_equal 'b1'
53
+ body('/a').must_equal 'a3'
54
+ body('/b').must_equal 'b3'
55
+ body('/a', 'REQUEST_METHOD'=>'POST').must_equal 'a2'
56
+ body('/b', 'REQUEST_METHOD'=>'POST').must_equal 'b2'
57
+ body('/a', 'REQUEST_METHOD'=>'PATCH').must_equal 'a2'
58
+ body('/b', 'REQUEST_METHOD'=>'PATCH').must_equal 'b1'
59
+ end
51
60
  end
52
61
 
53
62
  it "makes it still possible to use the Roda app normally" do
@@ -57,6 +66,21 @@ describe "middleware plugin" do
57
66
  body.must_equal 'a'
58
67
  end
59
68
 
69
+ deprecated "makes it still possible to use the Roda app normally when #call is overwritten" do
70
+ app(:bare) do
71
+ plugin :middleware
72
+ def call
73
+ super
74
+ end
75
+
76
+ route do
77
+ "a"
78
+ end
79
+ end
80
+ app
81
+ body.must_equal 'a'
82
+ end
83
+
60
84
  it "makes middleware always use a subclass of the app" do
61
85
  app(:middleware) do |r|
62
86
  r.get{opts[:a]}
@@ -161,4 +185,52 @@ describe "middleware plugin" do
161
185
  end
162
186
  body.must_equal 'bazbar'
163
187
  end
188
+
189
+ it "works with the route_block_args block when loaded before" do
190
+ app(:bare) do
191
+ plugin :middleware
192
+ plugin :route_block_args do
193
+ [request.path, response]
194
+ end
195
+
196
+ route do |path, res|
197
+ request.get 'a' do
198
+ res.write(path + '2')
199
+ end
200
+ end
201
+ end
202
+ a = app
203
+
204
+ app(:bare) do
205
+ use a
206
+ route{|r| 'b'}
207
+ end
208
+
209
+ body('/a').must_equal '/a2'
210
+ body('/x').must_equal 'b'
211
+ end
212
+
213
+ it "works with the route_block_args block when loaded after" do
214
+ app(:bare) do
215
+ plugin :route_block_args do
216
+ [request.path, response]
217
+ end
218
+ plugin :middleware
219
+
220
+ route do |path, res|
221
+ request.get 'a' do
222
+ res.write(path + '2')
223
+ end
224
+ end
225
+ end
226
+ a = app
227
+
228
+ app(:bare) do
229
+ use a
230
+ route{|r| 'b'}
231
+ end
232
+
233
+ body('/a').must_equal '/a2'
234
+ body('/x').must_equal 'b'
235
+ end
164
236
  end
@@ -170,6 +170,18 @@ describe "multi_route plugin" do
170
170
  end
171
171
  end
172
172
 
173
+ describe "multi_route plugin" do
174
+ it "r.multi_route handles loading the same route more than once" do
175
+ app(:multi_route) do |r|
176
+ r.multi_route
177
+ end
178
+ app.route('foo'){'bar'}
179
+ body('/foo').must_equal 'bar'
180
+ app.route('foo'){'baz'}
181
+ body('/foo').must_equal 'baz'
182
+ end
183
+ end
184
+
173
185
  describe "multi_route plugin" do
174
186
  it "r.multi_route raises error for invalid namespace" do
175
187
  app(:multi_route) do |r|
@@ -109,6 +109,25 @@ describe "route_csrf plugin" do
109
109
  body("/foo", "REQUEST_METHOD"=>'POST', 'rack.input'=>StringIO.new).must_equal '/foo2'
110
110
  end
111
111
 
112
+ it "allows plugin block to integrate with route_block_args plugin" do
113
+ app(:bare) do
114
+ send(*DEFAULT_SESSION_ARGS)
115
+ plugin :route_block_args do
116
+ [request, request.path, response]
117
+ end
118
+ plugin(:route_csrf){|r, path, res| res.write(path); res.write('2')}
119
+ route do |r|
120
+ check_csrf!
121
+ r.post('foo'){'f'}
122
+ r.get "token", String do |s|
123
+ csrf_token("/#{s}")
124
+ end
125
+ end
126
+ end
127
+ body("/foo", "REQUEST_METHOD"=>'POST', 'rack.input'=>StringIO.new("_csrf=#{Rack::Utils.escape(body("/token/foo"))}")).must_equal 'f'
128
+ body("/foo", "REQUEST_METHOD"=>'POST', 'rack.input'=>StringIO.new).must_equal '/foo2'
129
+ end
130
+
112
131
  it "raises Error if configuring plugin with invalid :csrf_failure option" do
113
132
  route_csrf_app(:csrf_failure=>:foo)
114
133
  proc{body("/foo", "REQUEST_METHOD"=>'POST', 'rack.input'=>StringIO.new)}.must_raise Roda::RodaError
@@ -118,6 +137,14 @@ describe "route_csrf plugin" do
118
137
  proc{route_csrf_app(:block=>proc{|r| r.path + '2'}, :csrf_failure=>:raise)}.must_raise Roda::RodaError
119
138
  end
120
139
 
140
+ deprecated "supports check_csrf! :csrf_failure option as a Proc" do
141
+ pr = proc{env['BAD'] == '1' ? 't' : 'f'}
142
+ route_csrf_app{check_csrf!(:csrf_failure=>pr); ''}
143
+ token = body("/token/foo")
144
+ body("SKIP"=>"1", "BAD"=>'1', "REQUEST_METHOD"=>'POST', 'rack.input'=>StringIO.new("_csrf=#{Rack::Utils.escape(token)}")).must_equal 't'
145
+ body("SKIP"=>"1", "BAD"=>'0', "REQUEST_METHOD"=>'POST', 'rack.input'=>StringIO.new("_csrf=#{Rack::Utils.escape(token)}")).must_equal 'f'
146
+ end
147
+
121
148
  it "supports valid_csrf? method" do
122
149
  route_csrf_app{valid_csrf?.to_s}
123
150
  body("/a", "REQUEST_METHOD"=>'POST', 'rack.input'=>StringIO.new("_csrf=#{Rack::Utils.escape(body("/token/a"))}")).must_equal 'true'
@@ -87,7 +87,32 @@ describe "sessions plugin" do
87
87
  body('/g/foo').must_equal 'bar'
88
88
 
89
89
  _, h, b = req('/sc')
90
- h['Set-Cookie'].must_include "roda.session=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00"
90
+
91
+ # Parameters can come in any order, and only the final parameter may omit the ;
92
+ ['roda.session=', 'max-age=0', 'path=/'].each do |param|
93
+ h['Set-Cookie'].must_match /#{Regexp.escape(param)}(;|\z)/
94
+ end
95
+ h['Set-Cookie'].must_match /expires=Thu, 01 Jan 1970 00:00:00 (-0000|GMT)(;|\z)/
96
+
97
+ b.must_equal ['c']
98
+
99
+ errors.must_equal []
100
+ end
101
+
102
+ it "removes session cookie even when max-age and expires are in cookie options" do
103
+ app.plugin :sessions, :cookie_options=>{:max_age=>'1000', :expires=>Time.now+1000}
104
+ body('/s/foo/bar').must_equal 'bar'
105
+ sct = body('/sct').to_i
106
+ body('/g/foo').must_equal 'bar'
107
+
108
+ _, h, b = req('/sc')
109
+
110
+ # Parameters can come in any order, and only the final parameter may omit the ;
111
+ ['roda.session=', 'max-age=0', 'path=/'].each do |param|
112
+ h['Set-Cookie'].must_match /#{Regexp.escape(param)}(;|\z)/
113
+ end
114
+ h['Set-Cookie'].must_match /expires=Thu, 01 Jan 1970 00:00:00 (-0000|GMT)(;|\z)/
115
+
91
116
  b.must_equal ['c']
92
117
 
93
118
  errors.must_equal []