diff options
40 files changed, 1339 insertions, 693 deletions
diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index 85a83ed7d9..aa4872f3ac 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,5 +1,25 @@ ## Rails 4.0.0 (unreleased) ## +* Add option flag to `CacheHelper#cache` to manually bypass automatic template digests: + + <% cache project, skip_digest: true do %> + ... + <% end %> + + *Drew Ulmer* + +* No sort Hash options in #grouped_options_for_select. *Sergey Kojin* + +* Accept symbols as #send_data :disposition value *Elia Schito* + +* Add i18n scope to distance_of_time_in_words. *Steve Klabnik* + +* `assert_template`: + - is no more passing with empty string. + - is now validating option keys. It accepts: `:layout`, `:partial`, `:locals` and `:count`. + + *Roberto Soares* + * Allow setting a symbol as path in scope on routes. This is now allowed: scope :api do diff --git a/actionpack/lib/action_controller/metal/data_streaming.rb b/actionpack/lib/action_controller/metal/data_streaming.rb index 334943818c..75c4d3ef99 100644 --- a/actionpack/lib/action_controller/metal/data_streaming.rb +++ b/actionpack/lib/action_controller/metal/data_streaming.rb @@ -150,6 +150,7 @@ module ActionController #:nodoc: disposition = options.fetch(:disposition, DEFAULT_SEND_FILE_DISPOSITION) unless disposition.nil? + disposition = disposition.to_s disposition += %(; filename="#{options[:filename]}") if options[:filename] headers['Content-Disposition'] = disposition end diff --git a/actionpack/lib/action_controller/metal/params_wrapper.rb b/actionpack/lib/action_controller/metal/params_wrapper.rb index a475d4bdff..c9f1d8dcb4 100644 --- a/actionpack/lib/action_controller/metal/params_wrapper.rb +++ b/actionpack/lib/action_controller/metal/params_wrapper.rb @@ -2,7 +2,7 @@ require 'active_support/core_ext/hash/slice' require 'active_support/core_ext/hash/except' require 'active_support/core_ext/module/anonymous' require 'active_support/core_ext/struct' -require 'action_dispatch/http/mime_types' +require 'action_dispatch/http/mime_type' module ActionController # Wraps the parameters hash into a nested hash. This will allow clients to submit diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb index da640502a2..99a4e312cf 100644 --- a/actionpack/lib/action_controller/metal/strong_parameters.rb +++ b/actionpack/lib/action_controller/metal/strong_parameters.rb @@ -70,8 +70,7 @@ module ActionController # Also, sets the +permitted+ attribute to the default value of # <tt>ActionController::Parameters.permit_all_parameters</tt>. # - # class Person - # include ActiveRecord::Base + # class Person < ActiveRecord::Base # end # # params = ActionController::Parameters.new(name: 'Francesco') diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb index be8055955d..7b48870090 100644 --- a/actionpack/lib/action_controller/test_case.rb +++ b/actionpack/lib/action_controller/test_case.rb @@ -94,7 +94,7 @@ module ActionController matches_template = case options when String - rendered.any? do |t, num| + !options.empty? && rendered.any? do |t, num| options_splited = options.split(File::SEPARATOR) t_splited = t.split(File::SEPARATOR) t_splited.last(options_splited.size) == options_splited @@ -106,6 +106,8 @@ module ActionController end assert matches_template, msg when Hash + options.assert_valid_keys(:layout, :partial, :locals, :count) + if options.key?(:layout) expected_layout = options[:layout] msg = message || sprintf("expecting layout <%s> but action rendered <%s>", diff --git a/actionpack/lib/action_view/helpers/cache_helper.rb b/actionpack/lib/action_view/helpers/cache_helper.rb index ddac87a37d..db920ae7a4 100644 --- a/actionpack/lib/action_view/helpers/cache_helper.rb +++ b/actionpack/lib/action_view/helpers/cache_helper.rb @@ -52,6 +52,13 @@ module ActionView # Additionally, the digestor will automatically look through your template file for # explicit and implicit dependencies, and include those as part of the digest. # + # The digestor can be bypassed by passing skip_digest: true as an option to the cache call: + # + # <% cache project, skip_digest: true do %> + # <b>All the topics on this project</b> + # <%= render project.topics %> + # <% end %> + # # ==== Implicit dependencies # # Most template dependencies can be derived from calls to render in the template itself. @@ -105,7 +112,7 @@ module ActionView # Now all you'll have to do is change that timestamp when the helper method changes. def cache(name = {}, options = nil, &block) if controller.perform_caching - safe_concat(fragment_for(fragment_name_with_digest(name), options, &block)) + safe_concat(fragment_for(cache_fragment_name(name, options), options, &block)) else yield end @@ -113,6 +120,22 @@ module ActionView nil end + # This helper returns the name of a cache key for a given fragment cache + # call. By supplying skip_digest: true to cache, the digestion of cache + # fragments can be manually bypassed. This is useful when cache fragments + # cannot be manually expired unless you know the exact key which is the + # case when using memcached. + def cache_fragment_name(name = {}, options = nil) + skip_digest = options && options[:skip_digest] + + if skip_digest + name + else + fragment_name_with_digest(name) + end + end + + private def fragment_name_with_digest(name) #:nodoc: if @virtual_path [ @@ -124,7 +147,6 @@ module ActionView end end - private # TODO: Create an object that has caching read/write on it def fragment_for(name = {}, options = nil, &block) #:nodoc: if fragment = controller.read_fragment(name, options) diff --git a/actionpack/lib/action_view/helpers/form_helper.rb b/actionpack/lib/action_view/helpers/form_helper.rb index 6abf1e1751..c79d30ea88 100644 --- a/actionpack/lib/action_view/helpers/form_helper.rb +++ b/actionpack/lib/action_view/helpers/form_helper.rb @@ -702,6 +702,11 @@ module ActionView # <% end %> # ... # <% end %> + # + # Note that fields_for will automatically generate a hidden field + # to store the ID of the record. There are circumstances where this + # hidden field is not needed and you can pass <tt>hidden_field_id: false</tt> + # to prevent fields_for from rendering it automatically. def fields_for(record_name, record_object = nil, options = {}, &block) builder = instantiate_builder(record_name, record_object, options) output = capture(builder, &block) diff --git a/actionpack/lib/action_view/helpers/form_options_helper.rb b/actionpack/lib/action_view/helpers/form_options_helper.rb index 46ebe60ec2..9310a90f0f 100644 --- a/actionpack/lib/action_view/helpers/form_options_helper.rb +++ b/actionpack/lib/action_view/helpers/form_options_helper.rb @@ -482,15 +482,15 @@ module ActionView # grouped_options_for_select(grouped_options) # # Possible output: + # <optgroup label="North America"> + # <option value="US">United States</option> + # <option value="Canada">Canada</option> + # </optgroup> # <optgroup label="Europe"> # <option value="Denmark">Denmark</option> # <option value="Germany">Germany</option> # <option value="France">France</option> # </optgroup> - # <optgroup label="North America"> - # <option value="US">United States</option> - # <option value="Canada">Canada</option> - # </optgroup> # # Sample usage (divider): # grouped_options = [ @@ -530,8 +530,6 @@ module ActionView body.safe_concat content_tag(:option, prompt_text(prompt), :value => "") end - grouped_options = grouped_options.sort if grouped_options.is_a?(Hash) - grouped_options.each do |container| if divider label = divider diff --git a/actionpack/lib/action_view/vendor/html-scanner.rb b/actionpack/lib/action_view/vendor/html-scanner.rb index 879b31e60e..775b827529 100644 --- a/actionpack/lib/action_view/vendor/html-scanner.rb +++ b/actionpack/lib/action_view/vendor/html-scanner.rb @@ -1,4 +1,4 @@ -$LOAD_PATH << "#{File.dirname(__FILE__)}/html-scanner" +$LOAD_PATH.unshift "#{File.dirname(__FILE__)}/html-scanner" module HTML extend ActiveSupport::Autoload diff --git a/actionpack/test/controller/action_pack_assertions_test.rb b/actionpack/test/controller/action_pack_assertions_test.rb index ca542eb7e2..b94f45bfe7 100644 --- a/actionpack/test/controller/action_pack_assertions_test.rb +++ b/actionpack/test/controller/action_pack_assertions_test.rb @@ -430,6 +430,12 @@ end class AssertTemplateTest < ActionController::TestCase tests ActionPackAssertionsController + def test_with_invalid_hash_keys_raises_argument_error + assert_raise(ArgumentError) do + assert_template foo: "bar" + end + end + def test_with_partial get :partial assert_template :partial => '_partial' @@ -447,6 +453,20 @@ class AssertTemplateTest < ActionController::TestCase end end + def test_with_empty_string_fails_when_template_rendered + get :hello_world + assert_raise(ActiveSupport::TestCase::Assertion) do + assert_template "" + end + end + + def test_with_empty_string_fails_when_no_template_rendered + get :nothing + assert_raise(ActiveSupport::TestCase::Assertion) do + assert_template "" + end + end + def test_passes_with_correct_string get :hello_world assert_template 'hello_world' diff --git a/actionpack/test/controller/caching_test.rb b/actionpack/test/controller/caching_test.rb index 65c18dfb64..2428cd7433 100644 --- a/actionpack/test/controller/caching_test.rb +++ b/actionpack/test/controller/caching_test.rb @@ -164,6 +164,9 @@ class FunctionalCachingController < CachingController format.xml end end + + def fragment_cached_without_digest + end end class FunctionalFragmentCachingTest < ActionController::TestCase @@ -200,6 +203,15 @@ CACHED @store.read("views/test.host/functional_caching/html_fragment_cached_with_partial/#{template_digest("functional_caching/_partial", "html")}")) end + def test_skipping_fragment_cache_digesting + get :fragment_cached_without_digest, :format => "html" + assert_response :success + expected_body = "<body>\n<p>ERB</p>\n</body>\n" + + assert_equal expected_body, @response.body + assert_equal "<p>ERB</p>", @store.read("views/nodigest") + end + def test_render_inline_before_fragment_caching get :inline_fragment_cached assert_response :success diff --git a/actionpack/test/controller/send_file_test.rb b/actionpack/test/controller/send_file_test.rb index 8bf3096888..5e9053da5b 100644 --- a/actionpack/test/controller/send_file_test.rb +++ b/actionpack/test/controller/send_file_test.rb @@ -114,6 +114,18 @@ class SendFileTest < ActionController::TestCase assert_equal 'private', h['Cache-Control'] end + def test_send_file_headers_with_disposition_as_a_symbol + options = { + :type => Mime::PNG, + :disposition => :disposition, + :filename => 'filename' + } + + @controller.headers = {} + @controller.send(:send_file_headers!, options) + assert_equal 'disposition; filename="filename"', @controller.headers['Content-Disposition'] + end + def test_send_file_headers_with_mime_lookup_with_symbol options = { :type => :png diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb index 0a59d3cf9e..4f5d8fdb7c 100644 --- a/actionpack/test/dispatch/routing_test.rb +++ b/actionpack/test/dispatch/routing_test.rb @@ -20,592 +20,13 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end end - stub_controllers do |routes| - Routes = routes - Routes.draw do - default_url_options :host => "rubyonrails.org" - resources_path_names :correlation_indexes => "info_about_correlation_indexes" - + def test_logout + draw do controller :sessions do - get 'login' => :new - post 'login' => :create delete 'logout' => :destroy end - - resource :session do - get :create - post :reset - - resource :info - - member do - get :crush - end - end - - scope "bookmark", :controller => "bookmarks", :as => :bookmark do - get :new, :path => "build" - post :create, :path => "create", :as => "" - put :update - get :remove, :action => :destroy, :as => :remove - end - - scope "pagemark", :controller => "pagemarks", :as => :pagemark do - get "new", :path => "build" - post "create", :as => "" - put "update" - get "remove", :action => :destroy, :as => :remove - end - - get 'account/logout' => redirect("/logout"), :as => :logout_redirect - get 'account/login', :to => redirect("/login") - get 'secure', :to => redirect("/secure/login") - - get 'mobile', :to => redirect(:subdomain => 'mobile') - get 'documentation', :to => redirect(:domain => 'example-documentation.com', :path => '') - get 'new_documentation', :to => redirect(:path => '/documentation/new') - get 'super_new_documentation', :to => redirect(:host => 'super-docs.com') - - get 'stores/:name', :to => redirect(:subdomain => 'stores', :path => '/%{name}') - get 'stores/:name(*rest)', :to => redirect(:subdomain => 'stores', :path => '/%{name}%{rest}') - - get 'youtube_favorites/:youtube_id/:name', :to => redirect(YoutubeFavoritesRedirector) - - constraints(lambda { |req| true }) do - get 'account/overview' - end - - get '/account/nested/overview' - get 'sign_in' => "sessions#new" - - get 'account/modulo/:name', :to => redirect("/%{name}s") - get 'account/proc/:name', :to => redirect {|params, req| "/#{params[:name].pluralize}" } - get 'account/proc_req' => redirect {|params, req| "/#{req.method}" } - - get 'account/google' => redirect('http://www.google.com/', :status => 302) - - match 'openid/login', :via => [:get, :post], :to => "openid#login" - - controller(:global) do - get 'global/hide_notice' - get 'global/export', :to => :export, :as => :export_request - get '/export/:id/:file', :to => :export, :as => :export_download, :constraints => { :file => /.*/ } - get 'global/:action' - end - - get "/local/:action", :controller => "local" - - get "/projects/status(.:format)" - get "/404", :to => lambda { |env| [404, {"Content-Type" => "text/plain"}, ["NOT FOUND"]] } - - constraints(:ip => /192\.168\.1\.\d\d\d/) do - get 'admin' => "queenbee#index" - end - - constraints ::TestRoutingMapper::IpRestrictor do - get 'admin/accounts' => "queenbee#accounts" - end - - get 'admin/passwords' => "queenbee#passwords", :constraints => ::TestRoutingMapper::IpRestrictor - - scope 'pt', :as => 'pt' do - resources :projects, :path_names => { :edit => 'editar', :new => 'novo' }, :path => 'projetos' do - post :preview, :on => :new - put :close, :on => :member, :path => 'fechar' - get :open, :on => :new, :path => 'abrir' - end - resource :admin, :path_names => { :new => 'novo', :activate => 'ativar' }, :path => 'administrador' do - post :preview, :on => :new - put :activate, :on => :member - end - resources :products, :path_names => { :new => 'novo' } do - new do - post :preview - end - end - end - - resources :projects, :controller => :project do - resources :involvements, :attachments - get :correlation_indexes, :on => :collection - - resources :participants do - put :update_all, :on => :collection - end - - resources :companies do - resources :people - resource :avatar, :controller => :avatar - end - - resources :images, :as => :funny_images do - post :revise, :on => :member - end - - resource :manager, :as => :super_manager do - post :fire - end - - resources :people do - nested do - scope "/:access_token" do - resource :avatar - end - end - - member do - get 'some_path_with_name' - put :accessible_projects - post :resend, :generate_new_password - end - end - - resources :posts do - get :archive, :toggle_view, :on => :collection - post :preview, :on => :member - - resource :subscription - - resources :comments do - post :preview, :on => :collection - end - end - - post 'new', :action => 'new', :on => :collection, :as => :new - end - - resources :replies do - collection do - get 'page/:page' => 'replies#index', :page => %r{\d+} - get ':page' => 'replies#index', :page => %r{\d+} - end - - new do - post :preview - end - - member do - put :answer, :to => :mark_as_answer - delete :answer, :to => :unmark_as_answer - end - end - - resources :posts, :only => [:index, :show] do - namespace :admin do - root :to => "index#index" - end - resources :comments, :except => :destroy do - get "views" => "comments#views", :as => :views - end - end - - resource :past, :only => :destroy - resource :present, :only => :update - resource :future, :only => :create - resources :relationships, :only => [:create, :destroy] - resources :friendships, :only => [:update] - - shallow do - namespace :api do - resources :teams do - resources :players - resource :captain - end - end - end - - scope '/hello' do - shallow do - resources :notes do - resources :trackbacks - end - end - end - - resources :threads, :shallow => true do - resource :owner - resources :messages do - resources :comments do - member do - post :preview - end - end - end - end - - resources :sheep do - get "_it", :on => :member - end - - resources :clients do - namespace :google do - resource :account do - namespace :secret do - resource :info - end - end - end - end - - resources :customers do - get :recent, :on => :collection - get "profile", :on => :member - get "secret/profile" => "customers#secret", :on => :member - post "preview" => "customers#preview", :as => :another_preview, :on => :new - resource :avatar do - get "thumbnail" => "avatars#thumbnail", :as => :thumbnail, :on => :member - end - resources :invoices do - get "outstanding" => "invoices#outstanding", :on => :collection - get "overdue", :to => :overdue, :on => :collection - get "print" => "invoices#print", :as => :print, :on => :member - post "preview" => "invoices#preview", :as => :preview, :on => :new - get "aged/:months", :on => :collection, :action => :aged, :as => :aged - end - resources :notes, :shallow => true do - get "preview" => "notes#preview", :as => :preview, :on => :new - get "print" => "notes#print", :as => :print, :on => :member - end - get "inactive", :on => :collection - post "deactivate", :on => :member - get "old", :on => :collection, :as => :stale - get "export" - end - - namespace :api do - resources :customers do - get "recent" => "customers#recent", :as => :recent, :on => :collection - get "profile" => "customers#profile", :as => :profile, :on => :member - post "preview" => "customers#preview", :as => :preview, :on => :new - end - scope(':version', :version => /.+/) do - resources :users, :id => /.+?/, :format => /json|xml/ - end - - get "products/list" - end - - get 'sprockets.js' => ::TestRoutingMapper::SprocketsApp - - get 'people/:id/update', :to => 'people#update', :as => :update_person - get '/projects/:project_id/people/:id/update', :to => 'people#update', :as => :update_project_person - - # misc - get 'articles/:year/:month/:day/:title', :to => "articles#show", :as => :article - - # default params - get 'inline_pages/(:id)', :to => 'pages#show', :id => 'home' - get 'default_pages/(:id)', :to => 'pages#show', :defaults => { :id => 'home' } - defaults :id => 'home' do - get 'scoped_pages/(:id)', :to => 'pages#show' - end - - namespace :account do - get 'shorthand' - get 'description', :to => :description, :as => "description" - get ':action/callback', :action => /twitter|github/, :to => "callbacks", :as => :callback - resource :subscription, :credit, :credit_card - - root :to => "account#index" - - namespace :admin do - resource :subscription - end - end - - namespace :forum do - resources :products, :path => '' do - resources :questions - end - end - - namespace :users, :path => 'usuarios' do - root :to => 'home#index' - end - - controller :articles do - scope '/articles', :as => 'article' do - scope :path => '/:title', :title => /[a-z]+/, :as => :with_title do - get '/:id', :to => :with_id, :as => "" - end - end - end - - scope ':access_token', :constraints => { :access_token => /\w{5,5}/ } do - resources :rooms - end - - get '/info' => 'projects#info', :as => 'info' - - namespace :admin do - scope '(:locale)', :locale => /en|pl/ do - resources :descriptions - end - end - - scope '(:locale)', :locale => /en|pl/ do - get "registrations/new" - resources :descriptions - root :to => 'projects#index' - end - - scope :only => [:index, :show] do - resources :products, :constraints => { :id => /\d{4}/ } do - root :to => "products#root" - get :favorite, :on => :collection - resources :images - end - resource :account - end - - resource :dashboard, :constraints => { :ip => /192\.168\.1\.\d{1,3}/ } - - resource :token, :module => :api - scope :module => :api do - resources :errors, :shallow => true do - resources :notices - end - end - - scope :path => 'api' do - resource :me - get '/' => 'mes#index' - scope :v2 do - resource :me, as: 'v2_me' - get '/' => 'mes#index' - end - - scope :v3, :admin do - resource :me, as: 'v3_me' - end - end - - get "(/:username)/followers" => "followers#index" - get "/groups(/user/:username)" => "groups#index" - get "(/user/:username)/photos" => "photos#index" - - scope '(groups)' do - scope '(discussions)' do - resources :messages - end - end - - get "whatever/:controller(/:action(/:id))", :id => /\d+/ - - resource :profile do - get :settings - - new do - post :preview - end - end - - resources :content - - namespace :transport do - resources :taxis - end - - namespace :medical do - resource :taxis - end - - scope :constraints => { :id => /\d+/ } do - get '/tickets', :to => 'tickets#index', :as => :tickets - end - - scope :constraints => { :id => /\d{4}/ } do - resources :movies do - resources :reviews - resource :trailer - end - end - - namespace :private do - root :to => redirect('/private/index') - get "index", :to => 'private#index' - end - - scope :only => [:index, :show] do - namespace :only do - resources :clubs do - resources :players - resource :chairman - end - end - end - - scope :except => [:new, :create, :edit, :update, :destroy] do - namespace :except do - resources :clubs do - resources :players - resource :chairman - end - end - end - - namespace :wiki do - resources :articles, :id => /[^\/]+/ do - resources :comments, :only => [:create, :new] - end - end - - resources :wiki_pages, :path => :pages - resource :wiki_account, :path => :my_account - - scope :only => :show do - namespace :only do - resources :sectors, :only => :index do - resources :companies do - scope :only => :index do - resources :divisions - end - scope :except => [:show, :update, :destroy] do - resources :departments - end - end - resource :leader - resources :managers, :except => [:show, :update, :destroy] - end - end - end - - scope :except => :index do - namespace :except do - resources :sectors, :except => [:show, :update, :destroy] do - resources :companies do - scope :except => [:show, :update, :destroy] do - resources :divisions - end - scope :only => :index do - resources :departments - end - end - resource :leader - resources :managers, :only => :index - end - end - end - - resources :sections, :id => /.+/ do - get :preview, :on => :member - end - - resources :profiles, :param => :username, :username => /[a-z]+/ do - get :details, :on => :member - resources :messages - end - - resources :orders do - constraints :download => /[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}/ do - resources :downloads, :param => :download, :shallow => true - end - end - - scope :as => "routes" do - get "/c/:id", :as => :collision, :to => "collision#show" - get "/collision", :to => "collision#show" - get "/no_collision", :to => "collision#show", :as => nil - - get "/fc/:id", :as => :forced_collision, :to => "forced_collision#show" - get "/forced_collision", :as => :forced_collision, :to => "forced_collision#show" - end - - get '/purchases/:token/:filename', - :to => 'purchases#fetch', - :token => /[[:alnum:]]{10}/, - :filename => /(.+)/, - :as => :purchase - - resources :lists, :id => /([A-Za-z0-9]{25})|default/ do - resources :todos, :id => /\d+/ - end - - scope '/countries/:country', :constraints => lambda { |params, req| %w(all France).include?(params[:country]) } do - get '/', :to => 'countries#index' - get '/cities', :to => 'countries#cities' - end - - get '/countries/:country/(*other)', :to => redirect{ |params, req| params[:other] ? "/countries/all/#{params[:other]}" : '/countries/all' } - - get '/:locale/*file.:format', :to => 'files#show', :file => /path\/to\/existing\/file/ - - scope '/italians' do - get '/writers', :to => 'italians#writers', :constraints => ::TestRoutingMapper::IpRestrictor - get '/sculptors', :to => 'italians#sculptors' - get '/painters/:painter', :to => 'italians#painters', :constraints => {:painter => /michelangelo/} - end - end - end - - class TestAltApp < ActionDispatch::IntegrationTest - class AltRequest - def initialize(env) - @env = env - end - - def path_info - "/" - end - - def request_method - "GET" - end - - def ip - "127.0.0.1" - end - - def x_header - @env["HTTP_X_HEADER"] || "" - end - end - - class XHeader - def call(env) - [200, {"Content-Type" => "text/html"}, ["XHeader"]] - end - end - - class AltApp - def call(env) - [200, {"Content-Type" => "text/html"}, ["Alternative App"]] - end - end - - AltRoutes = ActionDispatch::Routing::RouteSet.new(AltRequest) - AltRoutes.draw do - get "/" => TestRoutingMapper::TestAltApp::XHeader.new, :constraints => {:x_header => /HEADER/} - get "/" => TestRoutingMapper::TestAltApp::AltApp.new - end - - def app - AltRoutes end - def test_alt_request_without_header - get "/" - assert_equal "Alternative App", @response.body - end - - def test_alt_request_with_matched_header - get "/", {}, "HTTP_X_HEADER" => "HEADER" - assert_equal "XHeader", @response.body - end - - def test_alt_request_with_unmatched_header - get "/", {}, "HTTP_X_HEADER" => "NON_MATCH" - assert_equal "Alternative App", @response.body - end - end - - def app - Routes - end - - include Routes.url_helpers - - def test_logout delete '/logout' assert_equal 'sessions#destroy', @response.body @@ -614,6 +35,15 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_login + draw do + default_url_options :host => "rubyonrails.org" + + controller :sessions do + get 'login' => :new + post 'login' => :create + end + end + get '/login' assert_equal 'sessions#new', @response.body assert_equal '/login', login_path @@ -624,39 +54,59 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest assert_equal '/login', url_for(:controller => 'sessions', :action => 'create', :only_path => true) assert_equal '/login', url_for(:controller => 'sessions', :action => 'new', :only_path => true) - assert_equal 'http://rubyonrails.org/login', Routes.url_for(:controller => 'sessions', :action => 'create') - assert_equal 'http://rubyonrails.org/login', Routes.url_helpers.login_url + assert_equal 'http://rubyonrails.org/login', url_for(:controller => 'sessions', :action => 'create') + assert_equal 'http://rubyonrails.org/login', login_url end def test_login_redirect + draw do + get 'account/login', :to => redirect("/login") + end + get '/account/login' verify_redirect 'http://www.example.com/login' end def test_logout_redirect_without_to + draw do + get 'account/logout' => redirect("/logout"), :as => :logout_redirect + end + assert_equal '/account/logout', logout_redirect_path get '/account/logout' verify_redirect 'http://www.example.com/logout' end def test_namespace_redirect + draw do + namespace :private do + root :to => redirect('/private/index') + get "index", :to => 'private#index' + end + end + get '/private' verify_redirect 'http://www.example.com/private/index' end def test_namespace_with_controller_segment assert_raise(ArgumentError) do - self.class.stub_controllers do |routes| - routes.draw do - namespace :admin do - get '/:controller(/:action(/:id(.:format)))' - end + draw do + namespace :admin do + get '/:controller(/:action(/:id(.:format)))' end end end end def test_session_singleton_resource + draw do + resource :session do + get :create + post :reset + end + end + get '/session' assert_equal 'sessions#create', @response.body assert_equal '/session', session_path @@ -684,68 +134,126 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_session_info_nested_singleton_resource + draw do + resource :session do + resource :info + end + end + get '/session/info' assert_equal 'infos#show', @response.body assert_equal '/session/info', session_info_path end def test_member_on_resource + draw do + resource :session do + member do + get :crush + end + end + end + get '/session/crush' assert_equal 'sessions#crush', @response.body assert_equal '/session/crush', crush_session_path end def test_redirect_modulo + draw do + get 'account/modulo/:name', :to => redirect("/%{name}s") + end + get '/account/modulo/name' verify_redirect 'http://www.example.com/names' end def test_redirect_proc + draw do + get 'account/proc/:name', :to => redirect {|params, req| "/#{params[:name].pluralize}" } + end + get '/account/proc/person' verify_redirect 'http://www.example.com/people' end def test_redirect_proc_with_request + draw do + get 'account/proc_req' => redirect {|params, req| "/#{req.method}" } + end + get '/account/proc_req' verify_redirect 'http://www.example.com/GET' end def test_redirect_hash_with_subdomain + draw do + get 'mobile', :to => redirect(:subdomain => 'mobile') + end + get '/mobile' verify_redirect 'http://mobile.example.com/mobile' end def test_redirect_hash_with_domain_and_path + draw do + get 'documentation', :to => redirect(:domain => 'example-documentation.com', :path => '') + end + get '/documentation' verify_redirect 'http://www.example-documentation.com' end def test_redirect_hash_with_path + draw do + get 'new_documentation', :to => redirect(:path => '/documentation/new') + end + get '/new_documentation' verify_redirect 'http://www.example.com/documentation/new' end def test_redirect_hash_with_host + draw do + get 'super_new_documentation', :to => redirect(:host => 'super-docs.com') + end + get '/super_new_documentation?section=top' verify_redirect 'http://super-docs.com/super_new_documentation?section=top' end def test_redirect_hash_path_substitution + draw do + get 'stores/:name', :to => redirect(:subdomain => 'stores', :path => '/%{name}') + end + get '/stores/iernest' verify_redirect 'http://stores.example.com/iernest' end def test_redirect_hash_path_substitution_with_catch_all + draw do + get 'stores/:name(*rest)', :to => redirect(:subdomain => 'stores', :path => '/%{name}%{rest}') + end + get '/stores/iernest/products' verify_redirect 'http://stores.example.com/iernest/products' end def test_redirect_class + draw do + get 'youtube_favorites/:youtube_id/:name', :to => redirect(YoutubeFavoritesRedirector) + end + get '/youtube_favorites/oHg5SJYRHA0/rick-rolld' verify_redirect 'http://www.youtube.com/watch?v=oHg5SJYRHA0' end def test_openid + draw do + match 'openid/login', :via => [:get, :post], :to => "openid#login" + end + get '/openid/login' assert_equal 'openid#login', @response.body @@ -754,6 +262,15 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_bookmarks + draw do + scope "bookmark", :controller => "bookmarks", :as => :bookmark do + get :new, :path => "build" + post :create, :path => "create", :as => "" + put :update + get :remove, :action => :destroy, :as => :remove + end + end + get '/bookmark/build' assert_equal 'bookmarks#new', @response.body assert_equal '/bookmark/build', bookmark_new_path @@ -772,6 +289,15 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_pagemarks + draw do + scope "pagemark", :controller => "pagemarks", :as => :pagemark do + get "new", :path => "build" + post "create", :as => "" + put "update" + get "remove", :action => :destroy, :as => :remove + end + end + get '/pagemark/build' assert_equal 'pagemarks#new', @response.body assert_equal '/pagemark/build', pagemark_new_path @@ -790,6 +316,18 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_admin + draw do + constraints(:ip => /192\.168\.1\.\d\d\d/) do + get 'admin' => "queenbee#index" + end + + constraints ::TestRoutingMapper::IpRestrictor do + get 'admin/accounts' => "queenbee#accounts" + end + + get 'admin/passwords' => "queenbee#passwords", :constraints => ::TestRoutingMapper::IpRestrictor + end + get '/admin', {}, {'REMOTE_ADDR' => '192.168.1.100'} assert_equal 'queenbee#index', @response.body @@ -810,6 +348,15 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_global + draw do + controller(:global) do + get 'global/hide_notice' + get 'global/export', :to => :export, :as => :export_request + get '/export/:id/:file', :to => :export, :as => :export_download, :constraints => { :file => /.*/ } + get 'global/:action' + end + end + get '/global/dashboard' assert_equal 'global#dashboard', @response.body @@ -828,12 +375,20 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_local + draw do + get "/local/:action", :controller => "local" + end + get '/local/dashboard' assert_equal 'local#dashboard', @response.body end # tests the use of dup in url_for def test_url_for_with_no_side_effects + draw do + get "/projects/status(.:format)" + end + # without dup, additional (and possibly unwanted) values will be present in the options (eg. :host) original_options = {:controller => 'projects', :action => 'status'} options = original_options.dup @@ -845,6 +400,10 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_url_for_does_not_modify_controller + draw do + get "/projects/status(.:format)" + end + controller = '/projects' options = {:controller => controller, :action => 'status', :only_path => true} url = url_for(options) @@ -855,6 +414,12 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest # tests the arguments modification free version of define_hash_access def test_named_route_with_no_side_effects + draw do + resources :customers do + get "profile", :on => :member + end + end + original_options = { :host => 'test.host' } options = original_options.dup @@ -865,11 +430,19 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_projects_status + draw do + get "/projects/status(.:format)" + end + assert_equal '/projects/status', url_for(:controller => 'projects', :action => 'status', :only_path => true) assert_equal '/projects/status.json', url_for(:controller => 'projects', :action => 'status', :format => 'json', :only_path => true) end def test_projects + draw do + resources :projects, :controller => :project + end + get '/projects' assert_equal 'project#index', @response.body assert_equal '/projects', projects_path @@ -903,12 +476,24 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_projects_with_post_action_and_new_path_on_collection + draw do + resources :projects, :controller => :project do + post 'new', :action => 'new', :on => :collection, :as => :new + end + end + post '/projects/new' assert_equal "project#new", @response.body assert_equal "/projects/new", new_projects_path end def test_projects_involvements + draw do + resources :projects, :controller => :project do + resources :involvements, :attachments + end + end + get '/projects/1/involvements' assert_equal 'involvements#index', @response.body assert_equal '/projects/1/involvements', project_involvements_path(:project_id => '1') @@ -933,12 +518,26 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_projects_attachments + draw do + resources :projects, :controller => :project do + resources :involvements, :attachments + end + end + get '/projects/1/attachments' assert_equal 'attachments#index', @response.body assert_equal '/projects/1/attachments', project_attachments_path(:project_id => '1') end def test_projects_participants + draw do + resources :projects, :controller => :project do + resources :participants do + put :update_all, :on => :collection + end + end + end + get '/projects/1/participants' assert_equal 'participants#index', @response.body assert_equal '/projects/1/participants', project_participants_path(:project_id => '1') @@ -949,6 +548,15 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_projects_companies + draw do + resources :projects, :controller => :project do + resources :companies do + resources :people + resource :avatar, :controller => :avatar + end + end + end + get '/projects/1/companies' assert_equal 'companies#index', @response.body assert_equal '/projects/1/companies', project_companies_path(:project_id => '1') @@ -963,6 +571,14 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_project_manager + draw do + resources :projects do + resource :manager, :as => :super_manager do + post :fire + end + end + end + get '/projects/1/manager' assert_equal 'managers#show', @response.body assert_equal '/projects/1/manager', project_super_manager_path(:project_id => '1') @@ -977,6 +593,14 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_project_images + draw do + resources :projects do + resources :images, :as => :funny_images do + post :revise, :on => :member + end + end + end + get '/projects/1/images' assert_equal 'images#index', @response.body assert_equal '/projects/1/images', project_funny_images_path(:project_id => '1') @@ -991,6 +615,23 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_projects_people + draw do + resources :projects do + resources :people do + nested do + scope "/:access_token" do + resource :avatar + end + end + + member do + put :accessible_projects + post :resend, :generate_new_password + end + end + end + end + get '/projects/1/people' assert_equal 'people#index', @response.body assert_equal '/projects/1/people', project_people_path(:project_id => '1') @@ -1017,12 +658,35 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_projects_with_resources_path_names + draw do + resources_path_names :correlation_indexes => "info_about_correlation_indexes" + + resources :projects do + get :correlation_indexes, :on => :collection + end + end + get '/projects/info_about_correlation_indexes' - assert_equal 'project#correlation_indexes', @response.body + assert_equal 'projects#correlation_indexes', @response.body assert_equal '/projects/info_about_correlation_indexes', correlation_indexes_projects_path end def test_projects_posts + draw do + resources :projects do + resources :posts do + get :archive, :toggle_view, :on => :collection + post :preview, :on => :member + + resource :subscription + + resources :comments do + post :preview, :on => :collection + end + end + end + end + get '/projects/1/posts' assert_equal 'posts#index', @response.body assert_equal '/projects/1/posts', project_posts_path(:project_id => '1') @@ -1053,6 +717,15 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_replies + draw do + resources :replies do + member do + put :answer, :to => :mark_as_answer + delete :answer, :to => :unmark_as_answer + end + end + end + put '/replies/1/answer' assert_equal 'replies#mark_as_answer', @response.body @@ -1061,6 +734,12 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_resource_routes_with_only_and_except + draw do + resources :posts, :only => [:index, :show] do + resources :comments, :except => :destroy + end + end + get '/posts' assert_equal 'posts#index', @response.body assert_equal '/posts', posts_path @@ -1084,6 +763,12 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_resource_routes_only_create_update_destroy + draw do + resource :past, :only => :destroy + resource :present, :only => :update + resource :future, :only => :create + end + delete '/past' assert_equal 'pasts#destroy', @response.body assert_equal '/past', past_path @@ -1102,6 +787,11 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_resources_routes_only_create_update_destroy + draw do + resources :relationships, :only => [:create, :destroy] + resources :friendships, :only => [:update] + end + post '/relationships' assert_equal 'relationships#create', @response.body assert_equal '/relationships', relationships_path @@ -1120,12 +810,22 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_resource_with_slugs_in_ids + draw do + resources :posts + end + get '/posts/rails-rocks' assert_equal 'posts#show', @response.body assert_equal '/posts/rails-rocks', post_path(:id => 'rails-rocks') end def test_resources_for_uncountable_names + draw do + resources :sheep do + get "_it", :on => :member + end + end + assert_equal '/sheep', sheep_index_path assert_equal '/sheep/1', sheep_path(1) assert_equal '/sheep/new', new_sheep_path @@ -1135,25 +835,26 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest def test_resource_does_not_modify_passed_options options = {:id => /.+?/, :format => /json|xml/} - self.class.stub_controllers do |routes| - routes.draw do - resource :user, options - end - end + draw { resource :user, options } assert_equal({:id => /.+?/, :format => /json|xml/}, options) end def test_resources_does_not_modify_passed_options options = {:id => /.+?/, :format => /json|xml/} - self.class.stub_controllers do |routes| - routes.draw do - resources :users, options - end - end + draw { resources :users, options } assert_equal({:id => /.+?/, :format => /json|xml/}, options) end def test_path_names + draw do + scope 'pt', :as => 'pt' do + resources :projects, :path_names => { :edit => 'editar', :new => 'novo' }, :path => 'projetos' + resource :admin, :path_names => { :new => 'novo', :activate => 'ativar' }, :path => 'administrador' do + put :activate, :on => :member + end + end + end + get '/pt/projetos' assert_equal 'projects#index', @response.body assert_equal '/pt/projetos', pt_projects_path @@ -1176,6 +877,15 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_path_option_override + draw do + scope 'pt', :as => 'pt' do + resources :projects, :path_names => { :new => 'novo' }, :path => 'projetos' do + put :close, :on => :member, :path => 'fechar' + get :open, :on => :new, :path => 'abrir' + end + end + end + get '/pt/projetos/novo/abrir' assert_equal 'projects#open', @response.body assert_equal '/pt/projetos/novo/abrir', open_new_pt_project_path @@ -1186,11 +896,19 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_sprockets + draw do + get 'sprockets.js' => ::TestRoutingMapper::SprocketsApp + end + get '/sprockets.js' assert_equal 'javascripts', @response.body end def test_update_person_route + draw do + get 'people/:id/update', :to => 'people#update', :as => :update_person + end + get '/people/1/update' assert_equal 'people#update', @response.body @@ -1198,6 +916,10 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_update_project_person + draw do + get '/projects/:project_id/people/:id/update', :to => 'people#update', :as => :update_project_person + end + get '/projects/1/people/2/update' assert_equal 'people#update', @response.body @@ -1205,6 +927,14 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_forum_products + draw do + namespace :forum do + resources :products, :path => '' do + resources :questions + end + end + end + get '/forum' assert_equal 'forum/products#index', @response.body assert_equal '/forum', forum_products_path @@ -1223,6 +953,10 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_articles_perma + draw do + get 'articles/:year/:month/:day/:title', :to => "articles#show", :as => :article + end + get '/articles/2009/08/18/rails-3' assert_equal 'articles#show', @response.body @@ -1230,6 +964,12 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_account_namespace + draw do + namespace :account do + resource :subscription, :credit, :credit_card + end + end + get '/account/subscription' assert_equal 'account/subscriptions#show', @response.body assert_equal '/account/subscription', account_subscription_path @@ -1244,12 +984,32 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_nested_namespace + draw do + namespace :account do + namespace :admin do + resource :subscription + end + end + end + get '/account/admin/subscription' assert_equal 'account/admin/subscriptions#show', @response.body assert_equal '/account/admin/subscription', account_admin_subscription_path end def test_namespace_nested_in_resources + draw do + resources :clients do + namespace :google do + resource :account do + namespace :secret do + resource :info + end + end + end + end + end + get '/clients/1/google/account' assert_equal '/clients/1/google/account', client_google_account_path(1) assert_equal 'google/accounts#show', @response.body @@ -1260,12 +1020,28 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_namespace_with_options + draw do + namespace :users, :path => 'usuarios' do + root :to => 'home#index' + end + end + get '/usuarios' assert_equal '/usuarios', users_root_path assert_equal 'users/home#index', @response.body end def test_articles_with_id + draw do + controller :articles do + scope '/articles', :as => 'article' do + scope :path => '/:title', :title => /[a-z]+/, :as => :with_title do + get '/:id', :to => :with_id, :as => "" + end + end + end + end + get '/articles/rails/1' assert_equal 'articles#with_id', @response.body @@ -1276,6 +1052,12 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_access_token_rooms + draw do + scope ':access_token', :constraints => { :access_token => /\w{5,5}/ } do + resources :rooms + end + end + get '/12345/rooms' assert_equal 'rooms#index', @response.body @@ -1287,40 +1069,91 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_root + draw do + root :to => 'projects#index' + end + assert_equal '/', root_path get '/' assert_equal 'projects#index', @response.body end + def test_scoped_root + draw do + scope '(:locale)', :locale => /en|pl/ do + root :to => 'projects#index' + end + end + + assert_equal '/en', root_path(:locale => 'en') + get '/en' + assert_equal 'projects#index', @response.body + end + def test_index + draw do + get '/info' => 'projects#info', :as => 'info' + end + assert_equal '/info', info_path get '/info' assert_equal 'projects#info', @response.body end def test_match_shorthand_with_no_scope + draw do + get 'account/overview' + end + assert_equal '/account/overview', account_overview_path get '/account/overview' assert_equal 'account#overview', @response.body end def test_match_shorthand_inside_namespace + draw do + namespace :account do + get 'shorthand' + end + end + assert_equal '/account/shorthand', account_shorthand_path get '/account/shorthand' assert_equal 'account#shorthand', @response.body end def test_match_shorthand_inside_namespace_with_controller + draw do + namespace :api do + get "products/list" + end + end + assert_equal '/api/products/list', api_products_list_path get '/api/products/list' assert_equal 'api/products#list', @response.body end def test_dynamically_generated_helpers_on_collection_do_not_clobber_resources_url_helper + draw do + resources :replies do + collection do + get 'page/:page' => 'replies#index', :page => %r{\d+} + get ':page' => 'replies#index', :page => %r{\d+} + end + end + end + assert_equal '/replies', replies_path end def test_scoped_controller_with_namespace_and_action + draw do + namespace :account do + get ':action/callback', :action => /twitter|github/, :to => "callbacks", :as => :callback + end + end + assert_equal '/account/twitter/callback', account_callback_path("twitter") get '/account/twitter/callback' assert_equal 'account/callbacks#twitter', @response.body @@ -1330,23 +1163,39 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_convention_match_nested_and_with_leading_slash + draw do + get '/account/nested/overview' + end + assert_equal '/account/nested/overview', account_nested_overview_path get '/account/nested/overview' assert_equal 'account/nested#overview', @response.body end def test_convention_with_explicit_end + draw do + get 'sign_in' => "sessions#new" + end + get '/sign_in' assert_equal 'sessions#new', @response.body assert_equal '/sign_in', sign_in_path end def test_redirect_with_complete_url_and_status + draw do + get 'account/google' => redirect('http://www.google.com/', :status => 302) + end + get '/account/google' verify_redirect 'http://www.google.com/', 302 end def test_redirect_with_port + draw do + get 'account/login', :to => redirect("/login") + end + previous_host, self.host = self.host, 'www.example.com:3000' get '/account/login' @@ -1356,6 +1205,12 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_normalize_namespaced_matches + draw do + namespace :account do + get 'description', :to => :description, :as => "description" + end + end + assert_equal '/account/description', account_description_path get '/account/description' @@ -1363,18 +1218,36 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_namespaced_roots + draw do + namespace :account do + root :to => "account#index" + end + end + assert_equal '/account', account_root_path get '/account' assert_equal 'account/account#index', @response.body end def test_optional_scoped_root + draw do + scope '(:locale)', :locale => /en|pl/ do + root :to => 'projects#index' + end + end + assert_equal '/en', root_path("en") get '/en' assert_equal 'projects#index', @response.body end def test_optional_scoped_path + draw do + scope '(:locale)', :locale => /en|pl/ do + resources :descriptions + end + end + assert_equal '/en/descriptions', descriptions_path("en") assert_equal '/descriptions', descriptions_path(nil) assert_equal '/en/descriptions/1', description_path("en", 1) @@ -1394,6 +1267,14 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_nested_optional_scoped_path + draw do + namespace :admin do + scope '(:locale)', :locale => /en|pl/ do + resources :descriptions + end + end + end + assert_equal '/admin/en/descriptions', admin_descriptions_path("en") assert_equal '/admin/descriptions', admin_descriptions_path(nil) assert_equal '/admin/en/descriptions/1', admin_description_path("en", 1) @@ -1413,6 +1294,12 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_nested_optional_path_shorthand + draw do + scope '(:locale)', :locale => /en|pl/ do + get "registrations/new" + end + end + get '/registrations/new' assert_nil @request.params[:locale] @@ -1421,6 +1308,15 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_default_params + draw do + get 'inline_pages/(:id)', :to => 'pages#show', :id => 'home' + get 'default_pages/(:id)', :to => 'pages#show', :defaults => { :id => 'home' } + + defaults :id => 'home' do + get 'scoped_pages/(:id)', :to => 'pages#show' + end + end + get '/inline_pages' assert_equal 'home', @request.params[:id] @@ -1432,6 +1328,16 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_resource_constraints + draw do + resources :products, :constraints => { :id => /\d{4}/ } do + root :to => "products#root" + get :favorite, :on => :collection + resources :images + end + + resource :dashboard, :constraints => { :ip => /192\.168\.1\.\d{1,3}/ } + end + get '/products/1' assert_equal 'pass', @response.headers['X-Cascade'] get '/products' @@ -1455,18 +1361,35 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_root_works_in_the_resources_scope + draw do + resources :products do + root :to => "products#root" + end + end + get '/products' assert_equal 'products#root', @response.body assert_equal '/products', products_root_path end def test_module_scope + draw do + resource :token, :module => :api + end + get '/token' assert_equal 'api/tokens#show', @response.body assert_equal '/token', token_path end def test_path_scope + draw do + scope :path => 'api' do + resource :me + get '/' => 'mes#index' + end + end + get '/api/me' assert_equal 'mes#show', @response.body assert_equal '/api/me', me_path @@ -1476,6 +1399,19 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_symbol_scope + draw do + scope :path => 'api' do + scope :v2 do + resource :me, as: 'v2_me' + get '/' => 'mes#index' + end + + scope :v3, :admin do + resource :me, as: 'v3_me' + end + end + end + get '/api/v2/me' assert_equal 'mes#show', @response.body assert_equal '/api/v2/me', v2_me_path @@ -1488,6 +1424,10 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_url_generator_for_generic_route + draw do + get "whatever/:controller(/:action(/:id))" + end + get 'whatever/foo/bar' assert_equal 'foo#bar', @response.body @@ -1496,6 +1436,10 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_url_generator_for_namespaced_generic_route + draw do + get "whatever/:controller(/:action(/:id))", :id => /\d+/ + end + get 'whatever/foo/bar/show' assert_equal 'foo/bar#show', @response.body @@ -1509,11 +1453,37 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest url_for(:controller => "foo/bar", :action => "show", :id => '1') end - def test_assert_recognizes_account_overview - assert_recognizes({:controller => "account", :action => "overview"}, "/account/overview") - end - def test_resource_new_actions + draw do + resources :replies do + new do + post :preview + end + end + + scope 'pt', :as => 'pt' do + resources :projects, :path_names => { :new => 'novo' }, :path => 'projetos' do + post :preview, :on => :new + end + + resource :admin, :path_names => { :new => 'novo' }, :path => 'administrador' do + post :preview, :on => :new + end + + resources :products, :path_names => { :new => 'novo' } do + new do + post :preview + end + end + end + + resource :profile do + new do + post :preview + end + end + end + assert_equal '/replies/new/preview', preview_new_reply_path assert_equal '/pt/projetos/novo/preview', preview_new_pt_project_path assert_equal '/pt/administrador/novo/preview', preview_new_pt_admin_path @@ -1537,13 +1507,27 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_resource_merges_options_from_scope - assert_raise(NameError) { new_account_path } + draw do + scope :only => :show do + resource :account + end + end + + assert_raise(NoMethodError) { new_account_path } get '/account/new' assert_equal 404, status end def test_resources_merges_options_from_scope + draw do + scope :only => [:index, :show] do + resources :products do + resources :images + end + end + end + assert_raise(NoMethodError) { edit_product_path('1') } get '/products/1/edit' @@ -1556,6 +1540,28 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_shallow_nested_resources + draw do + shallow do + namespace :api do + resources :teams do + resources :players + resource :captain + end + end + end + + resources :threads, :shallow => true do + resource :owner + resources :messages do + resources :comments do + member do + post :preview + end + end + end + end + end + get '/api/teams' assert_equal 'api/teams#index', @response.body assert_equal '/api/teams', api_teams_path @@ -1658,6 +1664,16 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_shallow_nested_resources_within_scope + draw do + scope '/hello' do + shallow do + resources :notes do + resources :trackbacks + end + end + end + end + get '/hello/notes/1/trackbacks' assert_equal 'trackbacks#index', @response.body assert_equal '/hello/notes/1/trackbacks', note_trackbacks_path(:note_id => 1) @@ -1709,6 +1725,36 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_custom_resource_routes_are_scoped + draw do + resources :customers do + get :recent, :on => :collection + get "profile", :on => :member + get "secret/profile" => "customers#secret", :on => :member + post "preview" => "customers#preview", :as => :another_preview, :on => :new + resource :avatar do + get "thumbnail" => "avatars#thumbnail", :as => :thumbnail, :on => :member + end + resources :invoices do + get "outstanding" => "invoices#outstanding", :on => :collection + get "overdue", :to => :overdue, :on => :collection + get "print" => "invoices#print", :as => :print, :on => :member + post "preview" => "invoices#preview", :as => :preview, :on => :new + end + resources :notes, :shallow => true do + get "preview" => "notes#preview", :as => :preview, :on => :new + get "print" => "notes#print", :as => :print, :on => :member + end + end + + namespace :api do + resources :customers do + get "recent" => "customers#recent", :as => :recent, :on => :collection + get "profile" => "customers#profile", :as => :profile, :on => :member + post "preview" => "customers#preview", :as => :preview, :on => :new + end + end + end + assert_equal '/customers/recent', recent_customers_path assert_equal '/customers/1/profile', profile_customer_path(:id => '1') assert_equal '/customers/1/secret/profile', secret_profile_customer_path(:id => '1') @@ -1731,6 +1777,14 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_shallow_nested_routes_ignore_module + draw do + scope :module => :api do + resources :errors, :shallow => true do + resources :notices + end + end + end + get '/errors/1/notices' assert_equal 'api/notices#index', @response.body assert_equal '/errors/1/notices', error_notices_path(:error_id => '1') @@ -1741,6 +1795,14 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_non_greedy_regexp + draw do + namespace :api do + scope(':version', :version => /.+/) do + resources :users, :id => /.+?/, :format => /json|xml/ + end + end + end + get '/api/1.0/users' assert_equal 'api/users#index', @response.body assert_equal '/api/1.0/users', api_users_path(:version => '1.0') @@ -1763,16 +1825,28 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_glob_parameter_accepts_regexp + draw do + get '/:locale/*file.:format', :to => 'files#show', :file => /path\/to\/existing\/file/ + end + get '/en/path/to/existing/file.html' assert_equal 200, @response.status end def test_resources_controller_name_is_not_pluralized + draw do + resources :content + end + get '/content' assert_equal 'content#index', @response.body end def test_url_generator_for_optional_prefix_dynamic_segment + draw do + get "(/:username)/followers" => "followers#index" + end + get '/bob/followers' assert_equal 'followers#index', @response.body assert_equal 'http://www.example.com/bob/followers', @@ -1785,6 +1859,10 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_url_generator_for_optional_suffix_static_and_dynamic_segment + draw do + get "/groups(/user/:username)" => "groups#index" + end + get '/groups/user/bob' assert_equal 'groups#index', @response.body assert_equal 'http://www.example.com/groups/user/bob', @@ -1797,6 +1875,10 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_url_generator_for_optional_prefix_static_and_dynamic_segment + draw do + get "(/user/:username)/photos" => "photos#index" + end + get 'user/bob/photos' assert_equal 'photos#index', @response.body assert_equal 'http://www.example.com/user/bob/photos', @@ -1809,6 +1891,14 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_url_recognition_for_optional_static_segments + draw do + scope '(groups)' do + scope '(discussions)' do + resources :messages + end + end + end + get '/groups/discussions/messages' assert_equal 'messages#index', @response.body @@ -1835,12 +1925,27 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_router_removes_invalid_conditions + draw do + scope :constraints => { :id => /\d+/ } do + get '/tickets', :to => 'tickets#index', :as => :tickets + end + end + get '/tickets' assert_equal 'tickets#index', @response.body assert_equal '/tickets', tickets_path end def test_constraints_are_merged_from_scope + draw do + scope :constraints => { :id => /\d{4}/ } do + resources :movies do + resources :reviews + resource :trailer + end + end + end + get '/movies/0001' assert_equal 'movies#show', @response.body assert_equal '/movies/0001', movie_path(:id => '0001') @@ -1875,6 +1980,17 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_only_should_be_read_from_scope + draw do + scope :only => [:index, :show] do + namespace :only do + resources :clubs do + resources :players + resource :chairman + end + end + end + end + get '/only/clubs' assert_equal 'only/clubs#index', @response.body assert_equal '/only/clubs', only_clubs_path @@ -1901,6 +2017,17 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_except_should_be_read_from_scope + draw do + scope :except => [:new, :create, :edit, :update, :destroy] do + namespace :except do + resources :clubs do + resources :players + resource :chairman + end + end + end + end + get '/except/clubs' assert_equal 'except/clubs#index', @response.body assert_equal '/except/clubs', except_clubs_path @@ -1927,6 +2054,14 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_only_option_should_override_scope + draw do + scope :only => :show do + namespace :only do + resources :sectors, :only => :index + end + end + end + get '/only/sectors' assert_equal 'only/sectors#index', @response.body assert_equal '/only/sectors', only_sectors_path @@ -1937,6 +2072,17 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_only_option_should_not_inherit + draw do + scope :only => :show do + namespace :only do + resources :sectors, :only => :index do + resources :companies + resource :leader + end + end + end + end + get '/only/sectors/1/companies/2' assert_equal 'only/companies#show', @response.body assert_equal '/only/sectors/1/companies/2', only_sector_company_path(:sector_id => '1', :id => '2') @@ -1947,6 +2093,14 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_except_option_should_override_scope + draw do + scope :except => :index do + namespace :except do + resources :sectors, :except => [:show, :update, :destroy] + end + end + end + get '/except/sectors' assert_equal 'except/sectors#index', @response.body assert_equal '/except/sectors', except_sectors_path @@ -1957,6 +2111,17 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_except_option_should_not_inherit + draw do + scope :except => :index do + namespace :except do + resources :sectors, :except => [:show, :update, :destroy] do + resources :companies + resource :leader + end + end + end + end + get '/except/sectors/1/companies/2' assert_equal 'except/companies#show', @response.body assert_equal '/except/sectors/1/companies/2', except_sector_company_path(:sector_id => '1', :id => '2') @@ -1967,6 +2132,16 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_except_option_should_override_scoped_only + draw do + scope :only => :show do + namespace :only do + resources :sectors, :only => :index do + resources :managers, :except => [:show, :update, :destroy] + end + end + end + end + get '/only/sectors/1/managers' assert_equal 'only/managers#index', @response.body assert_equal '/only/sectors/1/managers', only_sector_managers_path(:sector_id => '1') @@ -1977,6 +2152,16 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_only_option_should_override_scoped_except + draw do + scope :except => :index do + namespace :except do + resources :sectors, :except => [:show, :update, :destroy] do + resources :managers, :only => :index + end + end + end + end + get '/except/sectors/1/managers' assert_equal 'except/managers#index', @response.body assert_equal '/except/sectors/1/managers', except_sector_managers_path(:sector_id => '1') @@ -1987,6 +2172,20 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_only_scope_should_override_parent_scope + draw do + scope :only => :show do + namespace :only do + resources :sectors, :only => :index do + resources :companies do + scope :only => :index do + resources :divisions + end + end + end + end + end + end + get '/only/sectors/1/companies/2/divisions' assert_equal 'only/divisions#index', @response.body assert_equal '/only/sectors/1/companies/2/divisions', only_sector_company_divisions_path(:sector_id => '1', :company_id => '2') @@ -1997,6 +2196,20 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_except_scope_should_override_parent_scope + draw do + scope :except => :index do + namespace :except do + resources :sectors, :except => [:show, :update, :destroy] do + resources :companies do + scope :except => [:show, :update, :destroy] do + resources :divisions + end + end + end + end + end + end + get '/except/sectors/1/companies/2/divisions' assert_equal 'except/divisions#index', @response.body assert_equal '/except/sectors/1/companies/2/divisions', except_sector_company_divisions_path(:sector_id => '1', :company_id => '2') @@ -2007,6 +2220,20 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_except_scope_should_override_parent_only_scope + draw do + scope :only => :show do + namespace :only do + resources :sectors, :only => :index do + resources :companies do + scope :except => [:show, :update, :destroy] do + resources :departments + end + end + end + end + end + end + get '/only/sectors/1/companies/2/departments' assert_equal 'only/departments#index', @response.body assert_equal '/only/sectors/1/companies/2/departments', only_sector_company_departments_path(:sector_id => '1', :company_id => '2') @@ -2017,6 +2244,20 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_only_scope_should_override_parent_except_scope + draw do + scope :except => :index do + namespace :except do + resources :sectors, :except => [:show, :update, :destroy] do + resources :companies do + scope :only => :index do + resources :departments + end + end + end + end + end + end + get '/except/sectors/1/companies/2/departments' assert_equal 'except/departments#index', @response.body assert_equal '/except/sectors/1/companies/2/departments', except_sector_company_departments_path(:sector_id => '1', :company_id => '2') @@ -2027,6 +2268,12 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_resources_are_not_pluralized + draw do + namespace :transport do + resources :taxis + end + end + get '/transport/taxis' assert_equal 'transport/taxis#index', @response.body assert_equal '/transport/taxis', transport_taxis_path @@ -2054,6 +2301,12 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_singleton_resources_are_not_singularized + draw do + namespace :medical do + resource :taxis + end + end + get '/medical/taxis/new' assert_equal 'medical/taxis#new', @response.body assert_equal '/medical/taxis/new', new_medical_taxis_path @@ -2077,6 +2330,12 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_greedy_resource_id_regexp_doesnt_match_edit_and_custom_action + draw do + resources :sections, :id => /.+/ do + get :preview, :on => :member + end + end + get '/sections/1/edit' assert_equal 'sections#edit', @response.body assert_equal '/sections/1/edit', edit_section_path(:id => '1') @@ -2087,6 +2346,14 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_resource_constraints_are_pushed_to_scope + draw do + namespace :wiki do + resources :articles, :id => /[^\/]+/ do + resources :comments, :only => [:create, :new] + end + end + end + get '/wiki/articles/Ruby_on_Rails_3.0' assert_equal 'wiki/articles#show', @response.body assert_equal '/wiki/articles/Ruby_on_Rails_3.0', wiki_article_path(:id => 'Ruby_on_Rails_3.0') @@ -2101,6 +2368,11 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_resources_path_can_be_a_symbol + draw do + resources :wiki_pages, :path => :pages + resource :wiki_account, :path => :my_account + end + get '/pages' assert_equal 'wiki_pages#index', @response.body assert_equal '/pages', wiki_pages_path @@ -2115,6 +2387,10 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_redirect_https + draw do + get 'secure', :to => redirect("/secure/login") + end + with_https do get '/secure' verify_redirect 'https://www.example.com/secure/login' @@ -2122,6 +2398,15 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_symbolized_path_parameters_is_not_stale + draw do + scope '/countries/:country', :constraints => lambda { |params, req| %w(all France).include?(params[:country]) } do + get '/', :to => 'countries#index' + get '/cities', :to => 'countries#cities' + end + + get '/countries/:country/(*other)', :to => redirect{ |params, req| params[:other] ? "/countries/all/#{params[:other]}" : '/countries/all' } + end + get '/countries/France' assert_equal 'countries#index', @response.body @@ -2136,6 +2421,14 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_constraints_block_not_carried_to_following_routes + draw do + scope '/italians' do + get '/writers', :to => 'italians#writers', :constraints => ::TestRoutingMapper::IpRestrictor + get '/sculptors', :to => 'italians#sculptors' + get '/painters/:painter', :to => 'italians#painters', :constraints => {:painter => /michelangelo/} + end + end + get '/italians/writers' assert_equal 'Not Found', @response.body @@ -2150,6 +2443,18 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_custom_resource_actions_defined_using_string + draw do + resources :customers do + resources :invoices do + get "aged/:months", :on => :collection, :action => :aged, :as => :aged + end + + get "inactive", :on => :collection + post "deactivate", :on => :member + get "old", :on => :collection, :as => :stale + end + end + get '/customers/inactive' assert_equal 'customers#inactive', @response.body assert_equal '/customers/inactive', inactive_customers_path @@ -2168,18 +2473,38 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_route_defined_in_resources_scope_level + draw do + resources :customers do + get "export" + end + end + get '/customers/1/export' assert_equal 'customers#export', @response.body assert_equal '/customers/1/export', customer_export_path(:customer_id => '1') end def test_named_character_classes_in_regexp_constraints + draw do + get '/purchases/:token/:filename', + :to => 'purchases#fetch', + :token => /[[:alnum:]]{10}/, + :filename => /(.+)/, + :as => :purchase + end + get '/purchases/315004be7e/Ruby_on_Rails_3.pdf' assert_equal 'purchases#fetch', @response.body assert_equal '/purchases/315004be7e/Ruby_on_Rails_3.pdf', purchase_path(:token => '315004be7e', :filename => 'Ruby_on_Rails_3.pdf') end def test_nested_resource_constraints + draw do + resources :lists, :id => /([A-Za-z0-9]{25})|default/ do + resources :todos, :id => /\d+/ + end + end + get '/lists/01234012340123401234fffff' assert_equal 'lists#show', @response.body assert_equal '/lists/01234012340123401234fffff', list_path(:id => '01234012340123401234fffff') @@ -2194,6 +2519,17 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_named_routes_collision_is_avoided_unless_explicitly_given_as + draw do + scope :as => "routes" do + get "/c/:id", :as => :collision, :to => "collision#show" + get "/collision", :to => "collision#show" + get "/no_collision", :to => "collision#show", :as => nil + + get "/fc/:id", :as => :forced_collision, :to => "forced_collision#show" + get "/forced_collision", :as => :forced_collision, :to => "forced_collision#show" + end + end + assert_equal "/c/1", routes_collision_path(1) assert_equal "/fc/1", routes_forced_collision_path(1) end @@ -2204,86 +2540,100 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_explicitly_avoiding_the_named_route + draw do + scope :as => "routes" do + get "/c/:id", :as => :collision, :to => "collision#show" + get "/collision", :to => "collision#show" + get "/no_collision", :to => "collision#show", :as => nil + + get "/fc/:id", :as => :forced_collision, :to => "forced_collision#show" + get "/forced_collision", :as => :forced_collision, :to => "forced_collision#show" + end + end + assert !respond_to?(:routes_no_collision_path) end def test_controller_name_with_leading_slash_raise_error assert_raise(ArgumentError) do - self.class.stub_controllers do |routes| - routes.draw { get '/feeds/:service', :to => '/feeds#show' } - end + draw { get '/feeds/:service', :to => '/feeds#show' } end assert_raise(ArgumentError) do - self.class.stub_controllers do |routes| - routes.draw { get '/feeds/:service', :controller => '/feeds', :action => 'show' } - end + draw { get '/feeds/:service', :controller => '/feeds', :action => 'show' } end assert_raise(ArgumentError) do - self.class.stub_controllers do |routes| - routes.draw { get '/api/feeds/:service', :to => '/api/feeds#show' } - end + draw { get '/api/feeds/:service', :to => '/api/feeds#show' } end assert_raise(ArgumentError) do - self.class.stub_controllers do |routes| - routes.draw { controller("/feeds") { get '/feeds/:service', :to => :show } } - end + draw { controller("/feeds") { get '/feeds/:service', :to => :show } } end assert_raise(ArgumentError) do - self.class.stub_controllers do |routes| - routes.draw { resources :feeds, :controller => '/feeds' } - end + draw { resources :feeds, :controller => '/feeds' } end end def test_invalid_route_name_raises_error assert_raise(ArgumentError) do - self.class.stub_controllers do |routes| - routes.draw { get '/products', :to => 'products#index', :as => 'products ' } - end + draw { get '/products', :to => 'products#index', :as => 'products ' } end assert_raise(ArgumentError) do - self.class.stub_controllers do |routes| - routes.draw { get '/products', :to => 'products#index', :as => ' products' } - end + draw { get '/products', :to => 'products#index', :as => ' products' } end assert_raise(ArgumentError) do - self.class.stub_controllers do |routes| - routes.draw { get '/products', :to => 'products#index', :as => 'products!' } - end + draw { get '/products', :to => 'products#index', :as => 'products!' } end assert_raise(ArgumentError) do - self.class.stub_controllers do |routes| - routes.draw { get '/products', :to => 'products#index', :as => 'products index' } - end + draw { get '/products', :to => 'products#index', :as => 'products index' } end assert_raise(ArgumentError) do - self.class.stub_controllers do |routes| - routes.draw { get '/products', :to => 'products#index', :as => '1products' } - end + draw { get '/products', :to => 'products#index', :as => '1products' } end end def test_nested_route_in_nested_resource + draw do + resources :posts, :only => [:index, :show] do + resources :comments, :except => :destroy do + get "views" => "comments#views", :as => :views + end + end + end + get "/posts/1/comments/2/views" assert_equal "comments#views", @response.body assert_equal "/posts/1/comments/2/views", post_comment_views_path(:post_id => '1', :comment_id => '2') end def test_root_in_deeply_nested_scope + draw do + resources :posts, :only => [:index, :show] do + namespace :admin do + root :to => "index#index" + end + end + end + get "/posts/1/admin" assert_equal "admin/index#index", @response.body assert_equal "/posts/1/admin", post_admin_root_path(:post_id => '1') end def test_custom_param + draw do + resources :profiles, :param => :username do + get :details, :on => :member + resources :messages + end + end + get '/profiles/bob' assert_equal 'profiles#show', @response.body assert_equal 'bob', @request.params[:username] @@ -2297,6 +2647,13 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_custom_param_constraint + draw do + resources :profiles, :param => :username, :username => /[a-z]+/ do + get :details, :on => :member + resources :messages + end + end + get '/profiles/bob1' assert_equal 404, @response.status @@ -2308,12 +2665,41 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_shallow_custom_param + draw do + resources :orders do + constraints :download => /[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}/ do + resources :downloads, :param => :download, :shallow => true + end + end + end + get '/downloads/0c0c0b68-d24b-11e1-a861-001ff3fffe6f.zip' assert_equal 'downloads#show', @response.body assert_equal '0c0c0b68-d24b-11e1-a861-001ff3fffe6f', @request.params[:download] end private + + def draw(&block) + self.class.stub_controllers do |routes| + @app = routes + @app.default_url_options = { host: 'www.example.com' } + @app.draw(&block) + end + end + + def url_for(options = {}) + @app.url_helpers.url_for(options) + end + + def method_missing(method, *args, &block) + if method.to_s =~ /_(path|url)$/ + @app.url_helpers.send(method, *args, &block) + else + super + end + end + def with_https old_https = https? https! @@ -2333,6 +2719,67 @@ private end end +class TestAltApp < ActionDispatch::IntegrationTest + class AltRequest + def initialize(env) + @env = env + end + + def path_info + "/" + end + + def request_method + "GET" + end + + def ip + "127.0.0.1" + end + + def x_header + @env["HTTP_X_HEADER"] || "" + end + end + + class XHeader + def call(env) + [200, {"Content-Type" => "text/html"}, ["XHeader"]] + end + end + + class AltApp + def call(env) + [200, {"Content-Type" => "text/html"}, ["Alternative App"]] + end + end + + AltRoutes = ActionDispatch::Routing::RouteSet.new(AltRequest) + AltRoutes.draw do + get "/" => TestAltApp::XHeader.new, :constraints => {:x_header => /HEADER/} + get "/" => TestAltApp::AltApp.new + end + + def app + AltRoutes + end + + def test_alt_request_without_header + get "/" + assert_equal "Alternative App", @response.body + end + + def test_alt_request_with_matched_header + get "/", {}, "HTTP_X_HEADER" => "HEADER" + assert_equal "XHeader", @response.body + end + + def test_alt_request_with_unmatched_header + get "/", {}, "HTTP_X_HEADER" => "NON_MATCH" + assert_equal "Alternative App", @response.body + end +end + class TestAppendingRoutes < ActionDispatch::IntegrationTest def simple_app(resp) lambda { |e| [ 200, { 'Content-Type' => 'text/plain' }, [resp] ] } diff --git a/actionpack/test/fixtures/functional_caching/fragment_cached_without_digest.html.erb b/actionpack/test/fixtures/functional_caching/fragment_cached_without_digest.html.erb new file mode 100644 index 0000000000..3125583a28 --- /dev/null +++ b/actionpack/test/fixtures/functional_caching/fragment_cached_without_digest.html.erb @@ -0,0 +1,3 @@ +<body> +<%= cache 'nodigest', skip_digest: true do %><p>ERB</p><% end %> +</body> diff --git a/actionpack/test/template/form_options_helper_test.rb b/actionpack/test/template/form_options_helper_test.rb index 54ab8b4d3a..c5efed3195 100644 --- a/actionpack/test/template/form_options_helper_test.rb +++ b/actionpack/test/template/form_options_helper_test.rb @@ -346,7 +346,7 @@ class FormOptionsHelperTest < ActionView::TestCase def test_optgroups_with_with_options_with_hash assert_dom_equal( - "<optgroup label=\"Europe\"><option value=\"Denmark\">Denmark</option>\n<option value=\"Germany\">Germany</option></optgroup><optgroup label=\"North America\"><option value=\"United States\">United States</option>\n<option value=\"Canada\">Canada</option></optgroup>", + "<optgroup label=\"North America\"><option value=\"United States\">United States</option>\n<option value=\"Canada\">Canada</option></optgroup><optgroup label=\"Europe\"><option value=\"Denmark\">Denmark</option>\n<option value=\"Germany\">Germany</option></optgroup>", grouped_options_for_select({'North America' => ['United States','Canada'], 'Europe' => ['Denmark','Germany']}) ) end diff --git a/activemodel/CHANGELOG.md b/activemodel/CHANGELOG.md index 133bb558a9..5135e47db1 100644 --- a/activemodel/CHANGELOG.md +++ b/activemodel/CHANGELOG.md @@ -1,5 +1,14 @@ ## Rails 4.0.0 (unreleased) ## +* Fixed length validator to correctly handle nil values. Fixes #7180. + + *Michal Zima* + +* Removed dispensable `require` statements. Make sure to require `active_model` before requiring + individual parts of the framework. + + *Yves Senn* + * Use BCrypt's MIN_COST in the test environment for speedier tests when using `has_secure_pasword`. *Brian Cardarella + Jeremy Kemper + Trevor Turk* diff --git a/activemodel/lib/active_model/attribute_methods.rb b/activemodel/lib/active_model/attribute_methods.rb index 86eaad830e..af11da1351 100644 --- a/activemodel/lib/active_model/attribute_methods.rb +++ b/activemodel/lib/active_model/attribute_methods.rb @@ -8,7 +8,7 @@ module ActiveModel # # user = User.first # user.pets.select(:id).first.user_id - # # => ActiveModel::MissingAttributeError: missing attribute: user_id + # # => ActiveModel::MissingAttributeError: missing attribute: user_id class MissingAttributeError < NoMethodError end # == Active \Model Attribute Methods @@ -202,7 +202,7 @@ module ActiveModel # person.name # => "Bob" # person.nickname # => "Bob" # person.name_short? # => true - # person.nickname_short? # => true + # person.nickname_short? # => true def alias_attribute(new_name, old_name) self.attribute_aliases = attribute_aliases.merge(new_name.to_s => old_name.to_s) attribute_method_matchers.each do |matcher| diff --git a/activemodel/lib/active_model/callbacks.rb b/activemodel/lib/active_model/callbacks.rb index eab94554cc..c52e4947ae 100644 --- a/activemodel/lib/active_model/callbacks.rb +++ b/activemodel/lib/active_model/callbacks.rb @@ -1,5 +1,3 @@ -require 'active_support/callbacks' - module ActiveModel # == Active \Model \Callbacks # diff --git a/activemodel/lib/active_model/conversion.rb b/activemodel/lib/active_model/conversion.rb index 42de32e63e..1f5d23dd8e 100644 --- a/activemodel/lib/active_model/conversion.rb +++ b/activemodel/lib/active_model/conversion.rb @@ -1,5 +1,3 @@ -require 'active_support/inflector' - module ActiveModel # == Active \Model Conversions # diff --git a/activemodel/lib/active_model/dirty.rb b/activemodel/lib/active_model/dirty.rb index 9d09353ef2..d47c3ae1bb 100644 --- a/activemodel/lib/active_model/dirty.rb +++ b/activemodel/lib/active_model/dirty.rb @@ -1,4 +1,3 @@ -require 'active_model/attribute_methods' require 'active_support/hash_with_indifferent_access' require 'active_support/core_ext/object/duplicable' diff --git a/activemodel/lib/active_model/naming.rb b/activemodel/lib/active_model/naming.rb index 264880eecd..6887f6d781 100644 --- a/activemodel/lib/active_model/naming.rb +++ b/activemodel/lib/active_model/naming.rb @@ -1,4 +1,3 @@ -require 'active_support/inflector' require 'active_support/core_ext/hash/except' require 'active_support/core_ext/module/introspection' @@ -56,8 +55,8 @@ module ActiveModel # end # # BlogPost.model_name <=> 'BlogPost' # => 0 - # BlogPost.model_name <=> 'Blog' # => 1 - # BlogPost.model_name <=> 'BlogPosts' # => -1 + # BlogPost.model_name <=> 'Blog' # => 1 + # BlogPost.model_name <=> 'BlogPosts' # => -1 ## # :method: =~ diff --git a/activemodel/lib/active_model/observing.rb b/activemodel/lib/active_model/observing.rb index 5f1c99ce62..b4e40bada4 100644 --- a/activemodel/lib/active_model/observing.rb +++ b/activemodel/lib/active_model/observing.rb @@ -5,7 +5,6 @@ require 'active_support/core_ext/module/remove_method' require 'active_support/core_ext/string/inflections' require 'active_support/core_ext/enumerable' require 'active_support/core_ext/object/try' -require 'active_support/descendants_tracker' module ActiveModel # == Active \Model Observers Activation @@ -49,7 +48,7 @@ module ActiveModel # end # # ORM.observers = :cacher, :garbage_collector - # ORM.observers # => [:cacher, :garbage_collector] + # ORM.observers # => [:cacher, :garbage_collector] # ORM.observers.class # => ActiveModel::ObserverArray def observers @observers ||= ObserverArray.new(self) @@ -328,8 +327,8 @@ module ActiveModel # Returns the class observed by default. It's inferred from the observer's # class name. # - # PersonObserver.observed_class # => Person - # AccountObserver.observed_class # => Account + # PersonObserver.observed_class # => Person + # AccountObserver.observed_class # => Account def observed_class name[/(.*)Observer/, 1].try :constantize end diff --git a/activemodel/lib/active_model/validations.rb b/activemodel/lib/active_model/validations.rb index 2524b8d065..2db4a25f61 100644 --- a/activemodel/lib/active_model/validations.rb +++ b/activemodel/lib/active_model/validations.rb @@ -1,9 +1,6 @@ require 'active_support/core_ext/array/extract_options' require 'active_support/core_ext/hash/keys' require 'active_support/core_ext/hash/except' -require 'active_model/errors' -require 'active_model/validations/callbacks' -require 'active_model/validator' module ActiveModel diff --git a/activemodel/lib/active_model/validations/callbacks.rb b/activemodel/lib/active_model/validations/callbacks.rb index a8fb4fdfc2..e28ad2841b 100644 --- a/activemodel/lib/active_model/validations/callbacks.rb +++ b/activemodel/lib/active_model/validations/callbacks.rb @@ -1,5 +1,3 @@ -require 'active_support/callbacks' - module ActiveModel module Validations # == Active \Model Validation Callbacks @@ -85,8 +83,8 @@ module ActiveModel # person = Person.new # person.name = '' # person.valid? # => false - # person.status # => false - # person.name = 'bob' + # person.status # => false + # person.name = 'bob' # person.valid? # => true # person.status # => true def after_validation(*args, &block) diff --git a/activemodel/lib/active_model/validations/length.rb b/activemodel/lib/active_model/validations/length.rb index 70ef589cd7..675fb5f1e5 100644 --- a/activemodel/lib/active_model/validations/length.rb +++ b/activemodel/lib/active_model/validations/length.rb @@ -14,6 +14,10 @@ module ActiveModel options[:minimum], options[:maximum] = range.min, range.max end + if options[:allow_blank] == false && options[:minimum].nil? && options[:is].nil? + options[:minimum] = 1 + end + super end @@ -40,7 +44,10 @@ module ActiveModel CHECKS.each do |key, validity_check| next unless check_value = options[key] - next if value_length.send(validity_check, check_value) + + if !value.nil? || skip_nil_check?(key) + next if value_length.send(validity_check, check_value) + end errors_options[:count] = check_value @@ -58,6 +65,10 @@ module ActiveModel options[:tokenizer].call(value) end || value end + + def skip_nil_check?(key) + key == :maximum && options[:allow_nil].nil? && options[:allow_blank].nil? + end end module HelperMethods @@ -79,7 +90,8 @@ module ActiveModel # # Configuration options: # * <tt>:minimum</tt> - The minimum size of the attribute. - # * <tt>:maximum</tt> - The maximum size of the attribute. + # * <tt>:maximum</tt> - The maximum size of the attribute. Allows +nil+ by + # default if not used with :minimum. # * <tt>:is</tt> - The exact size of the attribute. # * <tt>:within</tt> - A range specifying the minimum and maximum size of # the attribute. diff --git a/activemodel/test/cases/railtie_test.rb b/activemodel/test/cases/railtie_test.rb index f89a288f8f..a0cd1402b1 100644 --- a/activemodel/test/cases/railtie_test.rb +++ b/activemodel/test/cases/railtie_test.rb @@ -5,10 +5,11 @@ class RailtieTest < ActiveModel::TestCase include ActiveSupport::Testing::Isolation def setup - require 'rails/all' + require 'active_model/railtie' - @app ||= Class.new(::Rails::Application).tap do |app| - app.config.eager_load = false + @app ||= Class.new(::Rails::Application) do + config.eager_load = false + config.logger = Logger.new(STDOUT) end end diff --git a/activemodel/test/cases/validations/length_validation_test.rb b/activemodel/test/cases/validations/length_validation_test.rb index 113bfd6337..1a40ca8efc 100644 --- a/activemodel/test/cases/validations/length_validation_test.rb +++ b/activemodel/test/cases/validations/length_validation_test.rb @@ -375,4 +375,43 @@ class LengthValidationTest < ActiveModel::TestCase t.author_name = "A very long author name that should still be valid." * 100 assert t.valid? end + + def test_validates_length_of_using_maximum_should_not_allow_nil_when_nil_not_allowed + Topic.validates_length_of :title, :maximum => 10, :allow_nil => false + t = Topic.new + assert t.invalid? + end + + def test_validates_length_of_using_maximum_should_not_allow_nil_and_empty_string_when_blank_not_allowed + Topic.validates_length_of :title, :maximum => 10, :allow_blank => false + t = Topic.new + assert t.invalid? + + t.title = "" + assert t.invalid? + end + + def test_validates_length_of_using_both_minimum_and_maximum_should_not_allow_nil + Topic.validates_length_of :title, :minimum => 5, :maximum => 10 + t = Topic.new + assert t.invalid? + end + + def test_validates_length_of_using_minimum_0_should_not_allow_nil + Topic.validates_length_of :title, :minimum => 0 + t = Topic.new + assert t.invalid? + + t.title = "" + assert t.valid? + end + + def test_validates_length_of_using_is_0_should_not_allow_nil + Topic.validates_length_of :title, :is => 0 + t = Topic.new + assert t.invalid? + + t.title = "" + assert t.valid? + end end diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 201eddbcae..66359fb80f 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,5 +1,11 @@ ## Rails 4.0.0 (unreleased) ## +* Fix dirty attribute checks for TimeZoneConversion with nil and blank + datetime attributes. Setting a nil datetime to a blank string should not + result in a change being flagged. Fix #8310 + + *Alisdair McDiarmid* + * Prevent mass assignment to the type column of polymorphic associations when using `build` Fix #8265 diff --git a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb index 427c61079a..47a8b576c0 100644 --- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb +++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb @@ -34,6 +34,7 @@ module ActiveRecord if create_time_zone_conversion_attribute?(attr_name, columns_hash[attr_name]) method_body, line = <<-EOV, __LINE__ + 1 def #{attr_name}=(original_time) + original_time = nil if original_time.blank? time = original_time unless time.acts_like?(:time) time = time.is_a?(String) ? Time.zone.parse(time) : time.to_time rescue time diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb index 4f3eebce7d..c3d15ca929 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -287,7 +287,7 @@ module ActiveRecord # Inserts the given fixture into the table. Overridden in adapters that require # something beyond a simple insert (eg. Oracle). def insert_fixture(fixture, table_name) - columns = Hash[columns(table_name).map { |c| [c.name, c] }] + columns = schema_cache.columns_hash(table_name) key_list = [] value_list = fixture.map do |name, value| diff --git a/activerecord/lib/active_record/connection_adapters/schema_cache.rb b/activerecord/lib/active_record/connection_adapters/schema_cache.rb index aad1f9a7ef..5839d1d3b4 100644 --- a/activerecord/lib/active_record/connection_adapters/schema_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/schema_cache.rb @@ -1,7 +1,7 @@ module ActiveRecord module ConnectionAdapters class SchemaCache - attr_reader :columns, :columns_hash, :primary_keys, :tables, :version + attr_reader :primary_keys, :tables, :version attr_accessor :connection def initialize(conn) @@ -30,6 +30,25 @@ module ActiveRecord end end + # Get the columns for a table + def columns(table = nil) + if table + @columns[table] + else + @columns + end + end + + # Get the columns for a table as a hash, key is the column name + # value is the column object. + def columns_hash(table = nil) + if table + @columns_hash[table] + else + @columns_hash + end + end + # Clears out internal caches def clear! @columns.clear diff --git a/activerecord/lib/active_record/explain.rb b/activerecord/lib/active_record/explain.rb index af772996f1..70683eb731 100644 --- a/activerecord/lib/active_record/explain.rb +++ b/activerecord/lib/active_record/explain.rb @@ -6,11 +6,12 @@ module ActiveRecord base.mattr_accessor :auto_explain_threshold_in_seconds, instance_accessor: false end - # If auto explain is enabled, this method triggers EXPLAIN logging for the - # queries triggered by the block if it takes more than the threshold as a - # whole. That is, the threshold is not checked against each individual - # query, but against the duration of the entire block. This approach is - # convenient for relations. + # If the database adapter supports explain and auto explain is enabled, + # this method triggers EXPLAIN logging for the queries triggered by the + # block if it takes more than the threshold as a whole. That is, the + # threshold is not checked against each individual query, but against the + # duration of the entire block. This approach is convenient for relations. + # # The available_queries_for_explain thread variable collects the queries # to be explained. If the value is nil, it means queries are not being @@ -21,7 +22,7 @@ module ActiveRecord threshold = auto_explain_threshold_in_seconds current = Thread.current - if threshold && current[:available_queries_for_explain].nil? + if connection.supports_explain? && threshold && current[:available_queries_for_explain].nil? begin queries = current[:available_queries_for_explain] = [] start = Time.now diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 7922bbcfa0..c5ad14722e 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -872,11 +872,7 @@ module ActiveRecord end def teardown_fixtures - return unless defined?(ActiveRecord) && !ActiveRecord::Base.configurations.blank? - - unless run_in_transaction? - ActiveRecord::FixtureSet.reset_cache - end + return if ActiveRecord::Base.configurations.blank? # Rollback changes if a transaction is active. if run_in_transaction? @@ -884,7 +880,10 @@ module ActiveRecord connection.rollback_transaction if connection.transaction_open? end @fixture_connections.clear + else + ActiveRecord::FixtureSet.reset_cache end + ActiveRecord::Base.clear_active_connections! end diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index 5464ca6066..a73473f59c 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -136,6 +136,13 @@ module ActiveRecord end end + initializer "active_record.validate_explain_support" do |app| + if app.config.active_record[:auto_explain_threshold_in_seconds] && + !ActiveRecord::Base.connection.supports_explain? + warn "auto_explain_threshold_in_seconds is set but will be ignored because your adapter does not support this feature. Please unset the configuration to avoid this warning." + end + end + # Expose database runtime to controller for logging. initializer "active_record.log_runtime" do |app| require "active_record/railties/controller_runtime" diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index 2ded97582d..e5022d49f1 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -1579,7 +1579,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal [tagging], post.taggings end - def test_build_with_polymotphic_has_many_does_not_allow_to_override_type_and_id + def test_build_with_polymorphic_has_many_does_not_allow_to_override_type_and_id welcome = posts(:welcome) tagging = welcome.taggings.build(:taggable_id => 99, :taggable_type => 'ShouldNotChange') diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb index d4fc5f204b..6fd9a9d11d 100644 --- a/activerecord/test/cases/dirty_test.rb +++ b/activerecord/test/cases/dirty_test.rb @@ -203,6 +203,20 @@ class DirtyTest < ActiveRecord::TestCase end end + def test_nullable_datetime_not_marked_as_changed_if_new_value_is_blank + in_time_zone 'Edinburgh' do + target = Class.new(ActiveRecord::Base) + target.table_name = 'topics' + + topic = target.create + assert_equal nil, topic.written_on + + topic.written_on = "" + assert_equal nil, topic.written_on + assert !topic.written_on_changed? + end + end + def test_integer_zero_to_string_zero_not_marked_as_changed pirate = Pirate.new pirate.parrot_id = 0 diff --git a/activerecord/test/cases/explain_test.rb b/activerecord/test/cases/explain_test.rb index 6dce8ccdd1..a7e5fdf709 100644 --- a/activerecord/test/cases/explain_test.rb +++ b/activerecord/test/cases/explain_test.rb @@ -108,6 +108,16 @@ if ActiveRecord::Base.connection.supports_explain? assert_equal expected, base.exec_explain(queries) end + def test_unsupported_connection_adapter + connection.stubs(:supports_explain?).returns(false) + + base.logger.expects(:warn).never + + with_threshold(0) do + Car.where(:name => 'honda').to_a + end + end + def test_silence_auto_explain base.expects(:collecting_sqls_for_explain).never base.logger.expects(:warn).never diff --git a/rails.gemspec b/rails.gemspec index ba94354bf1..ef25cca07a 100644 --- a/rails.gemspec +++ b/rails.gemspec @@ -18,6 +18,7 @@ Gem::Specification.new do |s| s.bindir = 'bin' s.executables = [] + s.require_paths = [] s.files = Dir['guides/**/*'] s.add_dependency 'activesupport', version diff --git a/railties/test/application/rake_test.rb b/railties/test/application/rake_test.rb index c6aea03d8c..076c5b3bbd 100644 --- a/railties/test/application/rake_test.rb +++ b/railties/test/application/rake_test.rb @@ -110,7 +110,6 @@ module ApplicationTests app_name = File.basename(app_path) app_dir = File.dirname(app_path) moved_app_name = app_name + '_moved' - moved_app_path = "#{app_path}/#{moved_app_name}" Dir.chdir(app_dir) do # Go from "./app/" to "./app/app_moved" diff --git a/railties/test/generators/namespaced_generators_test.rb b/railties/test/generators/namespaced_generators_test.rb index 9e7626647e..db268ed6f2 100644 --- a/railties/test/generators/namespaced_generators_test.rb +++ b/railties/test/generators/namespaced_generators_test.rb @@ -6,13 +6,15 @@ require 'rails/generators/mailer/mailer_generator' require 'rails/generators/rails/scaffold/scaffold_generator' class NamespacedGeneratorTestCase < Rails::Generators::TestCase + include GeneratorsTestHelper + def setup + super Rails::Generators.namespace = TestApp end end class NamespacedControllerGeneratorTest < NamespacedGeneratorTestCase - include GeneratorsTestHelper arguments %w(Account foo bar) tests Rails::Generators::ControllerGenerator @@ -81,7 +83,6 @@ class NamespacedControllerGeneratorTest < NamespacedGeneratorTestCase end class NamespacedModelGeneratorTest < NamespacedGeneratorTestCase - include GeneratorsTestHelper arguments %w(Account name:string age:integer) tests Rails::Generators::ModelGenerator @@ -142,7 +143,6 @@ class NamespacedModelGeneratorTest < NamespacedGeneratorTestCase end class NamespacedObserverGeneratorTest < NamespacedGeneratorTestCase - include GeneratorsTestHelper arguments %w(account) tests Rails::Generators::ObserverGenerator @@ -163,7 +163,6 @@ class NamespacedObserverGeneratorTest < NamespacedGeneratorTestCase end class NamespacedMailerGeneratorTest < NamespacedGeneratorTestCase - include GeneratorsTestHelper arguments %w(notifier foo bar) tests Rails::Generators::MailerGenerator |