Ruby on Rails | Screencasts | Download | Documentation | Weblog | Community | Source

root/trunk/actionpack/test/controller/routing_test.rb

Revision 9115, 83.7 kB (checked in by david, 8 months ago)

Added support for regexp flags like ignoring case in the :requirements part of routes declarations (closes #11421) [NeilW]

Line 
1 require 'abstract_unit'
2 require 'controller/fake_controllers'
3 require 'action_controller/routing'
4
5 class MilestonesController < ActionController::Base
6   def index() head :ok end
7   alias_method :show, :index
8   def rescue_action(e) raise e end
9 end
10
11 RunTimeTests = ARGV.include? 'time'
12 ROUTING = ActionController::Routing
13
14 class ROUTING::RouteBuilder
15   attr_reader :warn_output
16
17   def warn(msg)
18     (@warn_output ||= []) << msg
19   end
20 end
21
22 # See RFC 3986, section 3.3 for allowed path characters.
23 class UriReservedCharactersRoutingTest < Test::Unit::TestCase
24   def setup
25     ActionController::Routing.use_controllers! ['controller']
26     @set = ActionController::Routing::RouteSet.new
27     @set.draw do |map|
28       map.connect ':controller/:action/:variable'
29     end
30
31     safe, unsafe = %w(: @ & = + $ , ;), %w(^ / ? # [ ])
32     hex = unsafe.map { |char| '%' + char.unpack('H2').first.upcase }
33
34     @segment = "#{safe.join}#{unsafe.join}".freeze
35     @escaped = "#{safe.join}#{hex.join}".freeze
36   end
37
38   def test_route_generation_escapes_unsafe_path_characters
39     assert_equal "/contr#{@segment}oller/act#{@escaped}ion/var#{@escaped}iable",
40       @set.generate(:controller => "contr#{@segment}oller",
41                     :action => "act#{@segment}ion",
42                     :variable => "var#{@segment}iable")
43   end
44
45   def test_route_recognition_unescapes_path_components
46     options = { :controller => "controller",
47                 :action => "act#{@segment}ion",
48                 :variable => "var#{@segment}iable" }
49     assert_equal options, @set.recognize_path("/controller/act#{@escaped}ion/var#{@escaped}iable")
50   end
51 end
52
53 class LegacyRouteSetTests < Test::Unit::TestCase
54   attr_reader :rs
55   def setup
56     # These tests assume optimisation is on, so re-enable it.
57     ActionController::Base.optimise_named_routes = true
58
59     @rs = ::ActionController::Routing::RouteSet.new
60     @rs.draw {|m| m.connect ':controller/:action/:id' }
61
62     ActionController::Routing.use_controllers! %w(content admin/user admin/news_feed)
63   end
64
65   def test_default_setup
66     assert_equal({:controller => "content", :action => 'index'}, rs.recognize_path("/content"))
67     assert_equal({:controller => "content", :action => 'list'}, rs.recognize_path("/content/list"))
68     assert_equal({:controller => "content", :action => 'show', :id => '10'}, rs.recognize_path("/content/show/10"))
69    
70     assert_equal({:controller => "admin/user", :action => 'show', :id => '10'}, rs.recognize_path("/admin/user/show/10"))
71    
72     assert_equal '/admin/user/show/10', rs.generate(:controller => 'admin/user', :action => 'show', :id => 10)
73    
74     assert_equal '/admin/user/show', rs.generate({:action => 'show'}, {:controller => 'admin/user', :action => 'list', :id => '10'})
75     assert_equal '/admin/user/list/10', rs.generate({}, {:controller => 'admin/user', :action => 'list', :id => '10'})
76
77     assert_equal '/admin/stuff', rs.generate({:controller => 'stuff'}, {:controller => 'admin/user', :action => 'list', :id => '10'})
78     assert_equal '/stuff', rs.generate({:controller => '/stuff'}, {:controller => 'admin/user', :action => 'list', :id => '10'})
79   end
80  
81   def test_ignores_leading_slash
82     @rs.draw {|m| m.connect '/:controller/:action/:id'}
83     test_default_setup
84   end
85  
86   def test_time_recognition
87     # We create many routes to make situation more realistic
88     @rs = ::ActionController::Routing::RouteSet.new
89     @rs.draw { |map|
90       map.frontpage '', :controller => 'search', :action => 'new'
91       map.resources :videos do |video|
92         video.resources :comments
93         video.resource  :file,      :controller => 'video_file'
94         video.resource  :share,     :controller => 'video_shares'
95         video.resource  :abuse,     :controller => 'video_abuses'
96       end
97       map.resources :abuses, :controller => 'video_abuses'
98       map.resources :video_uploads
99       map.resources :video_visits
100
101       map.resources :users do |user|
102         user.resource  :settings
103         user.resources :videos
104       end
105       map.resources :channels do |channel|
106         channel.resources :videos, :controller => 'channel_videos'
107       end
108       map.resource  :session
109       map.resource  :lost_password
110       map.search    'search', :controller => 'search'
111       map.resources :pages
112       map.connect ':controller/:action/:id'
113     }
114     n = 1000
115     if RunTimeTests
116       GC.start
117       rectime = Benchmark.realtime do
118         n.times do
119           rs.recognize_path("/videos/1234567", {:method => :get})
120           rs.recognize_path("/videos/1234567/abuse", {:method => :get})
121           rs.recognize_path("/users/1234567/settings", {:method => :get})
122           rs.recognize_path("/channels/1234567", {:method => :get})
123           rs.recognize_path("/session/new", {:method => :get})
124           rs.recognize_path("/admin/user/show/10", {:method => :get})
125         end
126       end
127       puts "\n\nRecognition (#{rs.routes.size} routes):"
128       per_url = rectime / (n * 6)
129       puts "#{per_url * 1000} ms/url"
130       puts "#{1 / per_url} url/s\n\n"
131     end
132   end
133   def test_time_generation
134     n = 5000
135     if RunTimeTests
136       GC.start
137       pairs = [
138         [{:controller => 'content', :action => 'index'}, {:controller => 'content', :action => 'show'}],
139         [{:controller => 'content'}, {:controller => 'content', :action => 'index'}],   
140         [{:controller => 'content', :action => 'list'}, {:controller => 'content', :action => 'index'}],
141         [{:controller => 'content', :action => 'show', :id => '10'}, {:controller => 'content', :action => 'list'}],
142         [{:controller => 'admin/user', :action => 'index'}, {:controller => 'admin/user', :action => 'show'}],
143         [{:controller => 'admin/user'}, {:controller => 'admin/user', :action => 'index'}],
144         [{:controller => 'admin/user', :action => 'list'}, {:controller => 'admin/user', :action => 'index'}],
145         [{:controller => 'admin/user', :action => 'show', :id => '10'}, {:controller => 'admin/user', :action => 'list'}],
146       ]
147       p = nil
148       gentime = Benchmark.realtime do
149         n.times do
150         pairs.each {|(a, b)| rs.generate(a, b)}
151         end
152       end
153      
154       puts "\n\nGeneration (RouteSet): (#{(n * 8)} urls)"
155       per_url = gentime / (n * 8)
156       puts "#{per_url * 1000} ms/url"
157       puts "#{1 / per_url} url/s\n\n"
158     end
159   end
160
161   def test_route_with_colon_first
162     rs.draw do |map|
163       map.connect '/:controller/:action/:id', :action => 'index', :id => nil
164       map.connect ':url', :controller => 'tiny_url', :action => 'translate'
165     end
166   end
167
168   def test_route_with_regexp_for_controller
169     rs.draw do |map|
170       map.connect ':controller/:admintoken/:action/:id', :controller => /admin\/.+/
171       map.connect ':controller/:action/:id'
172     end
173     assert_equal({:controller => "admin/user", :admintoken => "foo", :action => "index"},
174         rs.recognize_path("/admin/user/foo"))
175     assert_equal({:controller => "content", :action => "foo"}, rs.recognize_path("/content/foo"))
176     assert_equal '/admin/user/foo', rs.generate(:controller => "admin/user", :admintoken => "foo", :action => "index")
177     assert_equal '/content/foo', rs.generate(:controller => "content", :action => "foo")
178   end
179
180   def test_route_with_regexp_and_dot
181     rs.draw do |map|
182       map.connect ':controller/:action/:file',
183                         :controller => /admin|user/,
184                         :action => /upload|download/,
185                         :defaults => {:file => nil},
186                         :requirements => {:file => %r{[^/]+(\.[^/]+)?}}
187     end
188     # Without a file extension
189     assert_equal '/user/download/file',
190       rs.generate(:controller => "user", :action => "download", :file => "file")
191     assert_equal(
192       {:controller => "user", :action => "download", :file => "file"},
193       rs.recognize_path("/user/download/file"))
194
195     # Now, let's try a file with an extension, really a dot (.)
196     assert_equal '/user/download/file.jpg',
197       rs.generate(
198         :controller => "user", :action => "download", :file => "file.jpg")
199     assert_equal(
200       {:controller => "user", :action => "download", :file => "file.jpg"},
201       rs.recognize_path("/user/download/file.jpg"))
202   end
203  
204   def test_basic_named_route
205     rs.add_named_route :home, '', :controller => 'content', :action => 'list'
206     x = setup_for_named_route
207     assert_equal("http://named.route.test/",
208                  x.send(:home_url))
209   end
210
211   def test_basic_named_route_with_relative_url_root
212     rs.add_named_route :home, '', :controller => 'content', :action => 'list'
213     x = setup_for_named_route
214     x.relative_url_root="/foo"
215     assert_equal("http://named.route.test/foo/",
216                  x.send(:home_url))
217     assert_equal "/foo/", x.send(:home_path)
218   end
219
220   def test_named_route_with_option
221     rs.add_named_route :page, 'page/:title', :controller => 'content', :action => 'show_page'
222     x = setup_for_named_route
223     assert_equal("http://named.route.test/page/new%20stuff",
224                  x.send(:page_url, :title => 'new stuff'))
225   end
226
227   def test_named_route_with_default
228     rs.add_named_route :page, 'page/:title', :controller => 'content', :action => 'show_page', :title => 'AboutPage'
229     x = setup_for_named_route
230     assert_equal("http://named.route.test/page/AboutRails",
231                  x.send(:page_url, :title => "AboutRails"))
232
233   end
234
235   def test_named_route_with_nested_controller
236     rs.add_named_route :users, 'admin/user', :controller => 'admin/user', :action => 'index'
237     x = setup_for_named_route
238     assert_equal("http://named.route.test/admin/user",
239                  x.send(:users_url))
240   end
241  
242   uses_mocha "named route optimisation" do
243     def test_optimised_named_route_call_never_uses_url_for
244       rs.add_named_route :users, 'admin/user', :controller => '/admin/user', :action => 'index'
245       rs.add_named_route :user, 'admin/user/:id', :controller=>'/admin/user', :action=>'show'
246       x = setup_for_named_route
247       x.expects(:url_for).never
248       x.send(:users_url)
249       x.send(:users_path)
250       x.send(:user_url, 2, :foo=>"bar")
251       x.send(:user_path, 3, :bar=>"foo")
252     end
253    
254     def test_optimised_named_route_with_host
255         rs.add_named_route :pages, 'pages', :controller => 'content', :action => 'show_page', :host => 'foo.com'
256         x = setup_for_named_route
257         x.expects(:url_for).with(:host => 'foo.com', :only_path => false, :controller => 'content', :action => 'show_page', :use_route => :pages).once
258       x.send(:pages_url)
259     end 
260   end
261
262   def setup_for_named_route
263     klass = Class.new(MockController)
264     rs.install_helpers(klass)
265     klass.new(rs)
266   end
267
268   def test_named_route_without_hash
269     rs.draw do |map|
270       map.normal ':controller/:action/:id'
271     end
272   end
273      
274   def test_named_route_root
275     rs.draw do |map|
276       map.root :controller => "hello"
277     end                     
278     x = setup_for_named_route       
279     assert_equal("http://named.route.test/", x.send(:root_url))
280     assert_equal("/", x.send(:root_path))
281   end
282  
283   def test_named_route_with_regexps
284     rs.draw do |map|
285       map.article 'page/:year/:month/:day/:title', :controller => 'page', :action => 'show',
286         :year => /\d+/, :month => /\d+/, :day => /\d+/
287       map.connect ':controller/:action/:id'
288     end
289     x = setup_for_named_route
290     # assert_equal(
291     #   {:controller => 'page', :action => 'show', :title => 'hi', :use_route => :article, :only_path => false},
292     #   x.send(:article_url, :title => 'hi')
293     # )
294     assert_equal(
295       "http://named.route.test/page/2005/6/10/hi",
296       x.send(:article_url, :title => 'hi', :day => 10, :year => 2005, :month => 6)
297     )
298   end
299
300   def test_changing_controller
301     assert_equal '/admin/stuff/show/10', rs.generate(
302       {:controller => 'stuff', :action => 'show', :id => 10},
303       {:controller => 'admin/user', :action => 'index'}
304     )
305   end 
306
307   def test_paths_escaped
308     rs.draw do |map|
309       map.path 'file/*path', :controller => 'content', :action => 'show_file'
310       map.connect ':controller/:action/:id'
311     end
312
313     # No + to space in URI escaping, only for query params.
314     results = rs.recognize_path "/file/hello+world/how+are+you%3F"
315     assert results, "Recognition should have succeeded"
316     assert_equal ['hello+world', 'how+are+you?'], results[:path]
317
318     # Use %20 for space instead.
319     results = rs.recognize_path "/file/hello%20world/how%20are%20you%3F"
320     assert results, "Recognition should have succeeded"
321     assert_equal ['hello world', 'how are you?'], results[:path]
322
323     results = rs.recognize_path "/file"
324     assert results, "Recognition should have succeeded"
325     assert_equal [], results[:path]
326   end
327  
328   def test_paths_slashes_unescaped_with_ordered_parameters
329     rs.add_named_route :path, '/file/*path', :controller => 'content'
330
331     # No / to %2F in URI, only for query params.
332     x = setup_for_named_route
333     assert_equal("/file/hello/world", x.send(:path_path, 'hello/world'))
334   end
335  
336   def test_non_controllers_cannot_be_matched
337     rs.draw do |map|
338       map.connect ':controller/:action/:id'
339     end
340     assert_raises(ActionController::RoutingError) { rs.recognize_path("/not_a/show/10") }
341   end
342
343   def test_paths_do_not_accept_defaults
344     assert_raises(ActionController::RoutingError) do
345       rs.draw do |map|
346         map.path 'file/*path', :controller => 'content', :action => 'show_file', :path => %w(fake default)
347         map.connect ':controller/:action/:id'
348       end
349     end
350    
351     rs.draw do |map|
352       map.path 'file/*path', :controller => 'content', :action => 'show_file', :path => []
353       map.connect ':controller/:action/:id'
354     end
355   end
356  
357   def test_should_list_options_diff_when_routing_requirements_dont_match
358     rs.draw do |map|
359       map.post 'post/:id', :controller=> 'post', :action=> 'show', :requirements => {:id => /\d+/}
360     end
361     exception = assert_raise(ActionController::RoutingError) { rs.generate(:controller => 'post', :action => 'show', :bad_param => "foo", :use_route => "post") }
362     assert_match /^post_url failed to generate/, exception.message
363     from_match = exception.message.match(/from \{[^\}]+\}/).to_s
364     assert_match /:bad_param=>"foo"/,   from_match
365     assert_match /:action=>"show"/,     from_match
366     assert_match /:controller=>"post"/, from_match
367    
368     expected_match = exception.message.match(/expected: \{[^\}]+\}/).to_s
369     assert_no_match /:bad_param=>"foo"/,   expected_match
370     assert_match    /:action=>"show"/,     expected_match
371     assert_match    /:controller=>"post"/, expected_match
372
373     diff_match = exception.message.match(/diff: \{[^\}]+\}/).to_s
374     assert_match    /:bad_param=>"foo"/,   diff_match
375     assert_no_match /:action=>"show"/,     diff_match
376     assert_no_match /:controller=>"post"/, diff_match
377   end
378
379   # this specifies the case where your formerly would get a very confusing error message with an empty diff
380   def test_should_have_better_error_message_when_options_diff_is_empty
381     rs.draw do |map|
382       map.content '/content/:query', :controller => 'content', :action => 'show'
383     end
384
385     exception = assert_raise(ActionController::RoutingError) { rs.generate(:controller => 'content', :action => 'show', :use_route => "content") }
386     assert_match %r[:action=>"show"], exception.message
387     assert_match %r[:controller=>"content"], exception.message
388     assert_match %r[you may have ambiguous routes, or you may need to supply additional parameters for this route], exception.message
389     assert_match %r[content_url has the following required parameters: \["content", :query\] - are they all satisfied?], exception.message
390   end
391  
392   def test_dynamic_path_allowed
393     rs.draw do |map|
394       map.connect '*path', :controller => 'content', :action => 'show_file'
395     end
396
397     assert_equal '/pages/boo', rs.generate(:controller => 'content', :action => 'show_file', :path => %w(pages boo))
398   end
399
400   def test_dynamic_recall_paths_allowed
401     rs.draw do |map|
402       map.connect '*path', :controller => 'content', :action => 'show_file'
403     end
404    
405     recall_path = ActionController::Routing::PathSegment::Result.new(%w(pages boo))
406     assert_equal '/pages/boo', rs.generate({}, :controller => 'content', :