aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore6
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.lock11
-rw-r--r--actionpack/CHANGELOG.md27
-rw-r--r--actionpack/lib/action_controller/test_case.rb10
-rw-r--r--actionpack/lib/action_dispatch/http/filter_parameters.rb6
-rw-r--r--actionpack/lib/action_dispatch/http/headers.rb2
-rw-r--r--actionpack/lib/action_dispatch/http/mime_negotiation.rb6
-rw-r--r--actionpack/lib/action_dispatch/http/request.rb8
-rw-r--r--actionpack/lib/action_dispatch/http/url.rb2
-rw-r--r--actionpack/lib/action_dispatch/middleware/cookies.rb4
-rw-r--r--actionpack/lib/action_dispatch/middleware/flash.rb2
-rw-r--r--actionpack/lib/action_dispatch/middleware/session/abstract_store.rb2
-rw-r--r--actionpack/lib/action_dispatch/middleware/session/cache_store.rb6
-rw-r--r--actionpack/lib/action_dispatch/middleware/session/cookie_store.rb8
-rw-r--r--actionpack/lib/action_dispatch/middleware/ssl.rb128
-rw-r--r--actionpack/lib/action_dispatch/middleware/static.rb6
-rw-r--r--actionpack/lib/action_dispatch/request/session.rb4
-rw-r--r--actionpack/lib/action_dispatch/request/utils.rb2
-rw-r--r--actionpack/test/abstract_unit.rb1
-rw-r--r--actionpack/test/controller/caching_test.rb40
-rw-r--r--actionpack/test/controller/params_wrapper_test.rb42
-rw-r--r--actionpack/test/controller/request_forgery_protection_test.rb1
-rw-r--r--actionpack/test/controller/rescue_test.rb11
-rw-r--r--actionpack/test/dispatch/debug_exceptions_test.rb9
-rw-r--r--actionpack/test/dispatch/exception_wrapper_test.rb16
-rw-r--r--actionpack/test/dispatch/request/session_test.rb2
-rw-r--r--actionpack/test/dispatch/request_test.rb102
-rw-r--r--actionpack/test/dispatch/session/abstract_store_test.rb4
-rw-r--r--actionpack/test/dispatch/ssl_test.rb297
-rw-r--r--actionview/lib/action_view/helpers/url_helper.rb2
-rw-r--r--actionview/lib/action_view/template.rb2
-rw-r--r--activemodel/CHANGELOG.md18
-rw-r--r--activemodel/lib/active_model/validations.rb2
-rw-r--r--activemodel/test/cases/validations/validations_context_test.rb19
-rw-r--r--activerecord/CHANGELOG.md36
-rw-r--r--activerecord/lib/active_record/associations.rb134
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb10
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb21
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb3
-rw-r--r--activerecord/lib/active_record/errors.rb38
-rw-r--r--activerecord/lib/active_record/inheritance.rb2
-rw-r--r--activerecord/lib/active_record/migration.rb42
-rw-r--r--activerecord/lib/active_record/model_schema.rb2
-rw-r--r--activerecord/lib/active_record/railties/databases.rake2
-rw-r--r--activerecord/lib/active_record/reflection.rb27
-rw-r--r--activerecord/lib/active_record/relation.rb23
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder.rb4
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb4
-rw-r--r--activerecord/lib/active_record/relation/query_methods.rb2
-rw-r--r--activerecord/lib/active_record/relation/where_clause_factory.rb1
-rw-r--r--activerecord/lib/active_record/scoping/default.rb12
-rw-r--r--activerecord/lib/active_record/table_metadata.rb12
-rw-r--r--activerecord/lib/active_record/tasks/database_tasks.rb2
-rw-r--r--activerecord/lib/active_record/validations.rb20
-rw-r--r--activerecord/test/cases/adapters/postgresql/active_schema_test.rb18
-rw-r--r--activerecord/test/cases/adapters/postgresql/schema_test.rb4
-rw-r--r--activerecord/test/cases/adapters/postgresql/view_test.rb64
-rw-r--r--activerecord/test/cases/errors_test.rb16
-rw-r--r--activerecord/test/cases/persistence_test.rb2
-rw-r--r--activerecord/test/cases/relations_test.rb12
-rw-r--r--activerecord/test/cases/tasks/database_tasks_test.rb4
-rw-r--r--activerecord/test/cases/validations_test.rb7
-rw-r--r--activerecord/test/cases/view_test.rb66
-rw-r--r--activerecord/test/models/face.rb2
-rw-r--r--activerecord/test/models/topic.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/file/atomic.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/string/conversions.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/time/calculations.rb1
-rw-r--r--activesupport/test/constantize_test_cases.rb4
-rw-r--r--guides/source/3_2_release_notes.md2
-rw-r--r--guides/source/configuring.md4
-rw-r--r--guides/source/upgrading_ruby_on_rails.md2
-rw-r--r--railties/lib/rails/generators.rb2
-rw-r--r--railties/lib/rails/tasks/engine.rake2
-rw-r--r--railties/lib/rails/test_unit/minitest_plugin.rb11
-rw-r--r--railties/lib/rails/test_unit/test_requirer.rb2
-rw-r--r--railties/lib/rails/test_unit/testing.rake10
-rw-r--r--railties/test/application/test_runner_test.rb5
81 files changed, 911 insertions, 546 deletions
diff --git a/.gitignore b/.gitignore
index c3cb009140..9268977c2f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,11 +1,11 @@
# Don't put *.swp, *.bak, etc here; those belong in a global ~/.gitignore.
# Check out https://help.github.com/articles/ignoring-files for how to set that up.
-debug.log
.Gemfile
-/.bundle
-/.ruby-version
+.ruby-version
+debug.log
pkg
+/.bundle
/dist
/doc/rdoc
/*/doc
diff --git a/Gemfile b/Gemfile
index b831a265d0..d829a2b448 100644
--- a/Gemfile
+++ b/Gemfile
@@ -90,7 +90,7 @@ platforms :ruby do
group :db do
gem 'pg', '>= 0.18.0'
gem 'mysql', '>= 2.9.0'
- gem 'mysql2', '>= 0.3.18'
+ gem 'mysql2', '>= 0.4.0', github: 'brianmario/mysql2'
end
end
diff --git a/Gemfile.lock b/Gemfile.lock
index 948c03574e..0700980bca 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -20,6 +20,12 @@ GIT
redis-namespace
GIT
+ remote: git://github.com/brianmario/mysql2.git
+ revision: 4d76557499b762d5a62aebb7f6a56510ad221eab
+ specs:
+ mysql2 (0.4.0)
+
+GIT
remote: git://github.com/mikel/mail.git
revision: 64ef1a12efcdda53fd63e1456c2c564044bf82ce
branch: master
@@ -29,7 +35,7 @@ GIT
GIT
remote: git://github.com/rack/rack.git
- revision: 6c4160b8c5173299f4b49ea2c9e4aab76f6b9054
+ revision: 4224c028c71c4ccca7cdb3e5a10c51af797a4d4d
branch: master
specs:
rack (2.0.0.alpha)
@@ -203,7 +209,6 @@ GEM
multi_json (1.11.2)
mustache (1.0.2)
mysql (2.9.1)
- mysql2 (0.3.19)
nokogiri (1.6.6.2)
mini_portile (~> 0.6.0)
nokogiri (1.6.6.2-x64-mingw32)
@@ -313,7 +318,7 @@ DEPENDENCIES
minitest (< 5.3.4)
mocha (~> 0.14)
mysql (>= 2.9.0)
- mysql2 (>= 0.3.18)
+ mysql2 (>= 0.4.0)!
nokogiri (>= 1.4.5)
pg (>= 0.18.0)
psych (~> 2.0)
diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md
index 5c12ffe092..73a833a23b 100644
--- a/actionpack/CHANGELOG.md
+++ b/actionpack/CHANGELOG.md
@@ -2,6 +2,33 @@
*Bernerd Schaefer*
+* Make it easier to opt in to `config.force_ssl` and `config.ssl_options` by
+ making them less dangerous to try and easier to disable.
+
+ SSL redirect:
+ * Move `:host` and `:port` options within `redirect: { … }`. Deprecate.
+ * Introduce `:status` and `:body` to customize the redirect response.
+ The 301 permanent default makes it difficult to test the redirect and
+ back out of it since browsers remember the 301. Test with a 302 or 307
+ instead, then switch to 301 once you're confident that all is well.
+
+ HTTP Strict Transport Security (HSTS):
+ * Shorter max-age. Shorten the default max-age from 1 year to 180 days,
+ the low end for https://www.ssllabs.com/ssltest/ grading and greater
+ than the 18-week minimum to qualify for browser preload lists.
+ * Disabling HSTS. Setting `hsts: false` now sets `hsts { expires: 0 }`
+ instead of omitting the header. Omitting does nothing to disable HSTS
+ since browsers hang on to your previous settings until they expire.
+ Sending `{ hsts: { expires: 0 }}` flushes out old browser settings and
+ actually disables HSTS:
+ http://tools.ietf.org/html/rfc6797#section-6.1.1
+ * HSTS Preload. Introduce `preload: true` to set the `preload` flag,
+ indicating that your site may be included in browser preload lists,
+ including Chrome, Firefox, Safari, IE11, and Edge. Submit your site:
+ https://hstspreload.appspot.com
+
+ *Jeremy Daer*
+
* Update `ActionController::TestSession#fetch` to behave more like
`ActionDispatch::Request::Session#fetch` when using non-string keys.
diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb
index ebb4ebdd46..472bb74add 100644
--- a/actionpack/lib/action_controller/test_case.rb
+++ b/actionpack/lib/action_controller/test_case.rb
@@ -70,7 +70,7 @@ module ActionController
self.content_type = ENCODER.content_type
data = ENCODER.build_multipart non_path_parameters
else
- get_header('CONTENT_TYPE') do |k|
+ fetch_header('CONTENT_TYPE') do |k|
set_header k, 'application/x-www-form-urlencoded'
end
@@ -98,7 +98,7 @@ module ActionController
set_header 'rack.input', StringIO.new(data)
end
- get_header("PATH_INFO") do |k|
+ fetch_header("PATH_INFO") do |k|
set_header k, generated_path
end
path_parameters[:controller] = controller_path
@@ -149,7 +149,7 @@ module ActionController
# Methods #destroy and #load! are overridden to avoid calling methods on the
# @store object, which does not exist for the TestSession class.
class TestSession < Rack::Session::Abstract::SessionHash #:nodoc:
- DEFAULT_OPTIONS = Rack::Session::Abstract::ID::DEFAULT_OPTIONS
+ DEFAULT_OPTIONS = Rack::Session::Abstract::Persisted::DEFAULT_OPTIONS
def initialize(session = {})
super(nil, nil)
@@ -500,7 +500,7 @@ module ActionController
if xhr
@request.set_header 'HTTP_X_REQUESTED_WITH', 'XMLHttpRequest'
- @request.get_header('HTTP_ACCEPT') do |k|
+ @request.fetch_header('HTTP_ACCEPT') do |k|
@request.set_header k, [Mime::JS, Mime::HTML, Mime::XML, 'text/xml', Mime::ALL].join(', ')
end
end
@@ -508,7 +508,7 @@ module ActionController
@controller.request = @request
@controller.response = @response
- @request.get_header("SCRIPT_NAME") do |k|
+ @request.fetch_header("SCRIPT_NAME") do |k|
@request.set_header k, @controller.config.relative_url_root
end
diff --git a/actionpack/lib/action_dispatch/http/filter_parameters.rb b/actionpack/lib/action_dispatch/http/filter_parameters.rb
index 3f2c6ceba3..9c0f39f2e7 100644
--- a/actionpack/lib/action_dispatch/http/filter_parameters.rb
+++ b/actionpack/lib/action_dispatch/http/filter_parameters.rb
@@ -23,7 +23,7 @@ module ActionDispatch
NULL_PARAM_FILTER = ParameterFilter.new # :nodoc:
NULL_ENV_FILTER = ParameterFilter.new ENV_MATCH # :nodoc:
- def initialize(env)
+ def initialize
super
@filtered_parameters = nil
@filtered_env = nil
@@ -48,13 +48,13 @@ module ActionDispatch
protected
def parameter_filter
- parameter_filter_for get_header("action_dispatch.parameter_filter") {
+ parameter_filter_for fetch_header("action_dispatch.parameter_filter") {
return NULL_PARAM_FILTER
}
end
def env_filter
- user_key = get_header("action_dispatch.parameter_filter") {
+ user_key = fetch_header("action_dispatch.parameter_filter") {
return NULL_ENV_FILTER
}
parameter_filter_for(Array(user_key) + ENV_MATCH)
diff --git a/actionpack/lib/action_dispatch/http/headers.rb b/actionpack/lib/action_dispatch/http/headers.rb
index fbdec6c132..9a3aaca3f0 100644
--- a/actionpack/lib/action_dispatch/http/headers.rb
+++ b/actionpack/lib/action_dispatch/http/headers.rb
@@ -64,7 +64,7 @@ module ActionDispatch
# If the code block is provided, then it will be run and
# its result returned.
def fetch(key, default = DEFAULT)
- @req.get_header(env_name(key)) do
+ @req.fetch_header(env_name(key)) do
return default unless default == DEFAULT
return yield if block_given?
raise NameError, key
diff --git a/actionpack/lib/action_dispatch/http/mime_negotiation.rb b/actionpack/lib/action_dispatch/http/mime_negotiation.rb
index e01d5ecc8f..cab60a508a 100644
--- a/actionpack/lib/action_dispatch/http/mime_negotiation.rb
+++ b/actionpack/lib/action_dispatch/http/mime_negotiation.rb
@@ -15,7 +15,7 @@ module ActionDispatch
# For backward compatibility, the post \format is extracted from the
# X-Post-Data-Format HTTP header if present.
def content_mime_type
- get_header("action_dispatch.request.content_type") do |k|
+ fetch_header("action_dispatch.request.content_type") do |k|
v = if get_header('CONTENT_TYPE') =~ /^([^,\;]*)/
Mime::Type.lookup($1.strip.downcase)
else
@@ -31,7 +31,7 @@ module ActionDispatch
# Returns the accepted MIME type for the request.
def accepts
- get_header("action_dispatch.request.accepts") do |k|
+ fetch_header("action_dispatch.request.accepts") do |k|
header = get_header('HTTP_ACCEPT').to_s.strip
v = if header.empty?
@@ -54,7 +54,7 @@ module ActionDispatch
end
def formats
- get_header("action_dispatch.request.formats") do |k|
+ fetch_header("action_dispatch.request.formats") do |k|
params_readable = begin
parameters[:format]
rescue ActionController::BadRequest
diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb
index df621f1074..18504eba6d 100644
--- a/actionpack/lib/action_dispatch/http/request.rb
+++ b/actionpack/lib/action_dispatch/http/request.rb
@@ -13,12 +13,14 @@ require 'action_dispatch/http/url'
require 'active_support/core_ext/array/conversions'
module ActionDispatch
- class Request < Rack::Request
+ class Request
+ include Rack::Request::Helpers
include ActionDispatch::Http::Cache::Request
include ActionDispatch::Http::MimeNegotiation
include ActionDispatch::Http::Parameters
include ActionDispatch::Http::FilterParameters
include ActionDispatch::Http::URL
+ include Rack::Request::Env
autoload :Session, 'action_dispatch/request/session'
autoload :Utils, 'action_dispatch/request/utils'
@@ -335,7 +337,7 @@ module ActionDispatch
# Override Rack's GET method to support indifferent access
def GET
- get_header("action_dispatch.request.query_parameters") do |k|
+ fetch_header("action_dispatch.request.query_parameters") do |k|
set_header k, Request::Utils.normalize_encode_params(super || {})
end
rescue Rack::Utils::ParameterTypeError, Rack::Utils::InvalidParameterError => e
@@ -345,7 +347,7 @@ module ActionDispatch
# Override Rack's POST method to support indifferent access
def POST
- get_header("action_dispatch.request.request_parameters") do
+ fetch_header("action_dispatch.request.request_parameters") do
self.request_parameters = Request::Utils.normalize_encode_params(super || {})
end
rescue Rack::Utils::ParameterTypeError, Rack::Utils::InvalidParameterError => e
diff --git a/actionpack/lib/action_dispatch/http/url.rb b/actionpack/lib/action_dispatch/http/url.rb
index 54e64f892a..92b10b6d3b 100644
--- a/actionpack/lib/action_dispatch/http/url.rb
+++ b/actionpack/lib/action_dispatch/http/url.rb
@@ -183,7 +183,7 @@ module ActionDispatch
end
end
- def initialize(env)
+ def initialize
super
@protocol = nil
@port = nil
diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb
index f958a88e4b..f37439e4d7 100644
--- a/actionpack/lib/action_dispatch/middleware/cookies.rb
+++ b/actionpack/lib/action_dispatch/middleware/cookies.rb
@@ -4,9 +4,9 @@ require 'active_support/message_verifier'
require 'active_support/json'
module ActionDispatch
- class Request < Rack::Request
+ class Request
def cookie_jar
- get_header('action_dispatch.cookies'.freeze) do
+ fetch_header('action_dispatch.cookies'.freeze) do
self.cookie_jar = Cookies::CookieJar.build(self, cookies)
end
end
diff --git a/actionpack/lib/action_dispatch/middleware/flash.rb b/actionpack/lib/action_dispatch/middleware/flash.rb
index 6041f84834..c482b1c5e7 100644
--- a/actionpack/lib/action_dispatch/middleware/flash.rb
+++ b/actionpack/lib/action_dispatch/middleware/flash.rb
@@ -1,7 +1,7 @@
require 'active_support/core_ext/hash/keys'
module ActionDispatch
- class Request < Rack::Request
+ class Request
# Access the contents of the flash. Use <tt>flash["notice"]</tt> to
# read a notice you put there or <tt>flash["notice"] = "hello"</tt>
# to put a new one.
diff --git a/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb
index b924df789f..9e50fea3fc 100644
--- a/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb
+++ b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb
@@ -79,7 +79,7 @@ module ActionDispatch
end
end
- class AbstractStore < Rack::Session::Abstract::ID
+ class AbstractStore < Rack::Session::Abstract::Persisted
include Compatibility
include StaleSessionCheck
include SessionObject
diff --git a/actionpack/lib/action_dispatch/middleware/session/cache_store.rb b/actionpack/lib/action_dispatch/middleware/session/cache_store.rb
index 857e49a682..589ae46e38 100644
--- a/actionpack/lib/action_dispatch/middleware/session/cache_store.rb
+++ b/actionpack/lib/action_dispatch/middleware/session/cache_store.rb
@@ -18,7 +18,7 @@ module ActionDispatch
end
# Get a session from the cache.
- def get_session(env, sid)
+ def find_session(env, sid)
unless sid and session = @cache.read(cache_key(sid))
sid, session = generate_sid, {}
end
@@ -26,7 +26,7 @@ module ActionDispatch
end
# Set a session in the cache.
- def set_session(env, sid, session, options)
+ def write_session(env, sid, session, options)
key = cache_key(sid)
if session
@cache.write(key, session, :expires_in => options[:expire_after])
@@ -37,7 +37,7 @@ module ActionDispatch
end
# Remove a session from the cache.
- def destroy_session(env, sid, options)
+ def delete_session(env, sid, options)
@cache.delete(cache_key(sid))
generate_sid
end
diff --git a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb
index e225f356df..3f7011d100 100644
--- a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb
+++ b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb
@@ -62,7 +62,7 @@ module ActionDispatch
# would set the session cookie to expire automatically 14 days after creation.
# Other useful options include <tt>:key</tt>, <tt>:secure</tt> and
# <tt>:httponly</tt>.
- class CookieStore < Rack::Session::Abstract::ID
+ class CookieStore < Rack::Session::Abstract::Persisted
include Compatibility
include StaleSessionCheck
include SessionObject
@@ -71,7 +71,7 @@ module ActionDispatch
super(app, options.merge!(:cookie_only => true))
end
- def destroy_session(req, session_id, options)
+ def delete_session(req, session_id, options)
new_sid = generate_sid unless options[:drop]
# Reset hash and Assign the new session id
req.set_header("action_dispatch.request.unsigned_session_cookie", new_sid ? { "session_id" => new_sid } : {})
@@ -95,7 +95,7 @@ module ActionDispatch
end
def unpacked_cookie_data(req)
- req.get_header("action_dispatch.request.unsigned_session_cookie") do |k|
+ req.fetch_header("action_dispatch.request.unsigned_session_cookie") do |k|
v = stale_session_check! do
if data = get_cookie(req)
data.stringify_keys!
@@ -112,7 +112,7 @@ module ActionDispatch
data
end
- def set_session(req, sid, session_data, options)
+ def write_session(req, sid, session_data, options)
session_data["session_id"] = sid
session_data
end
diff --git a/actionpack/lib/action_dispatch/middleware/ssl.rb b/actionpack/lib/action_dispatch/middleware/ssl.rb
index 7b3d8bcc5b..b72953f1d1 100644
--- a/actionpack/lib/action_dispatch/middleware/ssl.rb
+++ b/actionpack/lib/action_dispatch/middleware/ssl.rb
@@ -1,72 +1,128 @@
module ActionDispatch
+ # This middleware is added to the stack when `config.force_ssl = true`.
+ # It does three jobs to enforce secure HTTP requests:
+ #
+ # 1. TLS redirect. http:// requests are permanently redirected to https://
+ # with the same URL host, path, etc. Pass `:host` and/or `:port` to
+ # modify the destination URL. This is always enabled.
+ #
+ # 2. Secure cookies. Sets the `secure` flag on cookies to tell browsers they
+ # mustn't be sent along with http:// requests. This is always enabled.
+ #
+ # 3. HTTP Strict Transport Security (HSTS). Tells the browser to remember
+ # this site as TLS-only and automatically redirect non-TLS requests.
+ # Enabled by default. Pass `hsts: false` to disable.
+ #
+ # Configure HSTS with `hsts: { … }`:
+ # * `expires`: How long, in seconds, these settings will stick. Defaults to
+ # `18.weeks`, the minimum required to qualify for browser preload lists.
+ # * `subdomains`: Set to `true` to tell the browser to apply these settings
+ # to all subdomains. This protects your cookies from interception by a
+ # vulnerable site on a subdomain. Defaults to `false`.
+ # * `preload`: Advertise that this site may be included in browsers'
+ # preloaded HSTS lists. HSTS protects your site on every visit *except the
+ # first visit* since it hasn't seen your HSTS header yet. To close this
+ # gap, browser vendors include a baked-in list of HSTS-enabled sites.
+ # Go to https://hstspreload.appspot.com to submit your site for inclusion.
+ #
+ # Disabling HSTS: To turn off HSTS, omitting the header is not enough.
+ # Browsers will remember the original HSTS directive until it expires.
+ # Instead, use the header to tell browsers to expire HSTS immediately.
+ # Setting `hsts: false` is a shortcut for `hsts: { expires: 0 }`.
class SSL
- YEAR = 31536000
+ # Default to 180 days, the low end for https://www.ssllabs.com/ssltest/
+ # and greater than the 18-week requirement for browser preload lists.
+ HSTS_EXPIRES_IN = 15552000
def self.default_hsts_options
- { :expires => YEAR, :subdomains => false }
+ { expires: HSTS_EXPIRES_IN, subdomains: false, preload: false }
end
- def initialize(app, options = {})
+ def initialize(app, redirect: {}, hsts: {}, **options)
@app = app
- @hsts = options.fetch(:hsts, {})
- @hsts = {} if @hsts == true
- @hsts = self.class.default_hsts_options.merge(@hsts) if @hsts
+ if options[:host] || options[:port]
+ ActiveSupport::Deprecation.warn <<-end_warning.strip_heredoc
+ The `:host` and `:port` options are moving within `:redirect`:
+ `config.ssl_options = { redirect: { host: …, port: … }}`.
+ end_warning
+ @redirect = options.slice(:host, :port)
+ else
+ @redirect = redirect
+ end
- @host = options[:host]
- @port = options[:port]
+ @hsts_header = build_hsts_header(normalize_hsts_options(hsts))
end
def call(env)
- request = Request.new(env)
+ request = Request.new env
if request.ssl?
- status, headers, body = @app.call(env)
- headers.reverse_merge!(hsts_headers)
- flag_cookies_as_secure!(headers)
- [status, headers, body]
+ @app.call(env).tap do |status, headers, body|
+ set_hsts_header! headers
+ flag_cookies_as_secure! headers
+ end
else
- redirect_to_https(request)
+ redirect_to_https request
end
end
private
- def redirect_to_https(request)
- host = @host || request.host
- port = @port || request.port
-
- location = "https://#{host}"
- location << ":#{port}" if port != 80
- location << request.fullpath
-
- headers = { 'Content-Type' => 'text/html', 'Location' => location }
-
- [301, headers, []]
+ def set_hsts_header!(headers)
+ headers['Strict-Transport-Security'.freeze] ||= @hsts_header
end
- # http://tools.ietf.org/html/draft-hodges-strict-transport-sec-02
- def hsts_headers
- if @hsts
- value = "max-age=#{@hsts[:expires].to_i}"
- value += "; includeSubDomains" if @hsts[:subdomains]
- { 'Strict-Transport-Security' => value }
+ def normalize_hsts_options(options)
+ case options
+ # Explicitly disabling HSTS clears the existing setting from browsers
+ # by setting expiry to 0.
+ when false
+ self.class.default_hsts_options.merge(expires: 0)
+ # Default to enabled, with default options.
+ when nil, true
+ self.class.default_hsts_options
else
- {}
+ self.class.default_hsts_options.merge(options)
end
end
+ # http://tools.ietf.org/html/rfc6797#section-6.1
+ def build_hsts_header(hsts)
+ value = "max-age=#{hsts[:expires].to_i}"
+ value << "; includeSubDomains" if hsts[:subdomains]
+ value << "; preload" if hsts[:preload]
+ value
+ end
+
def flag_cookies_as_secure!(headers)
- if cookies = headers['Set-Cookie']
- cookies = cookies.split("\n")
+ if cookies = headers['Set-Cookie'.freeze]
+ cookies = cookies.split("\n".freeze)
- headers['Set-Cookie'] = cookies.map { |cookie|
+ headers['Set-Cookie'.freeze] = cookies.map { |cookie|
if cookie !~ /;\s*secure\s*(;|$)/i
"#{cookie}; secure"
else
cookie
end
- }.join("\n")
+ }.join("\n".freeze)
end
end
+
+ def redirect_to_https(request)
+ [ @redirect.fetch(:status, 301),
+ { 'Content-Type' => 'text/html',
+ 'Location' => https_location_for(request) },
+ @redirect.fetch(:body, []) ]
+ end
+
+ def https_location_for(request)
+ host = @redirect[:host] || request.host
+ port = @redirect[:port] || request.port
+
+ location = "https://#{host}"
+ location << ":#{port}" if port != 80 && port != 443
+ location << request.fullpath
+ location
+ end
end
end
diff --git a/actionpack/lib/action_dispatch/middleware/static.rb b/actionpack/lib/action_dispatch/middleware/static.rb
index 9462ae4278..c4344c9609 100644
--- a/actionpack/lib/action_dispatch/middleware/static.rb
+++ b/actionpack/lib/action_dispatch/middleware/static.rb
@@ -28,7 +28,7 @@ module ActionDispatch
# Used by the `Static` class to check the existence of a valid file
# in the server's `public/` directory (see Static#call).
def match?(path)
- path = URI.parser.unescape(path)
+ path = ::Rack::Utils.unescape_path path
return false unless path.valid_encoding?
path = Rack::Utils.clean_path_info path
@@ -43,7 +43,7 @@ module ActionDispatch
end
}
- return ::Rack::Utils.escape(match)
+ return ::Rack::Utils.escape_path(match)
end
end
@@ -90,7 +90,7 @@ module ActionDispatch
def gzip_file_path(path)
can_gzip_mime = content_type(path) =~ /\A(?:text\/|application\/javascript)/
gzip_path = "#{path}.gz"
- if can_gzip_mime && File.exist?(File.join(@root, ::Rack::Utils.unescape(gzip_path)))
+ if can_gzip_mime && File.exist?(File.join(@root, ::Rack::Utils.unescape_path(gzip_path)))
gzip_path
else
false
diff --git a/actionpack/lib/action_dispatch/request/session.rb b/actionpack/lib/action_dispatch/request/session.rb
index b946ccb49f..9e7fcbd849 100644
--- a/actionpack/lib/action_dispatch/request/session.rb
+++ b/actionpack/lib/action_dispatch/request/session.rb
@@ -1,7 +1,7 @@
require 'rack/session/abstract/id'
module ActionDispatch
- class Request < Rack::Request
+ class Request
# Session is responsible for lazily loading the session from store.
class Session # :nodoc:
ENV_SESSION_KEY = Rack::RACK_SESSION # :nodoc:
@@ -77,7 +77,7 @@ module ActionDispatch
def destroy
clear
options = self.options || {}
- @by.send(:destroy_session, @req, options.id(@req), options)
+ @by.send(:delete_session, @req, options.id(@req), options)
# Load the new sid to be written with the response
@loaded = false
diff --git a/actionpack/lib/action_dispatch/request/utils.rb b/actionpack/lib/action_dispatch/request/utils.rb
index 3973ea6346..a8151a8224 100644
--- a/actionpack/lib/action_dispatch/request/utils.rb
+++ b/actionpack/lib/action_dispatch/request/utils.rb
@@ -1,5 +1,5 @@
module ActionDispatch
- class Request < Rack::Request
+ class Request
class Utils # :nodoc:
mattr_accessor :perform_deep_munge
diff --git a/actionpack/test/abstract_unit.rb b/actionpack/test/abstract_unit.rb
index 1954324222..3c498960e4 100644
--- a/actionpack/test/abstract_unit.rb
+++ b/actionpack/test/abstract_unit.rb
@@ -406,7 +406,6 @@ def jruby_skip(message = '')
skip message if defined?(JRUBY_VERSION)
end
-require 'mocha/setup' # FIXME: stop using mocha
require 'active_support/testing/method_call_assertions'
class ForkingExecutor
diff --git a/actionpack/test/controller/caching_test.rb b/actionpack/test/controller/caching_test.rb
index 5698159eba..bc0ffd3eaa 100644
--- a/actionpack/test/controller/caching_test.rb
+++ b/actionpack/test/controller/caching_test.rb
@@ -299,30 +299,42 @@ class CacheHelperOutputBufferTest < ActionController::TestCase
def test_output_buffer
output_buffer = ActionView::OutputBuffer.new
controller = MockController.new
- cache_helper = Object.new
+ cache_helper = Class.new do
+ def self.controller; end;
+ def self.output_buffer; end;
+ def self.output_buffer=; end;
+ end
cache_helper.extend(ActionView::Helpers::CacheHelper)
- cache_helper.expects(:controller).returns(controller).at_least(0)
- cache_helper.expects(:output_buffer).returns(output_buffer).at_least(0)
- # if the output_buffer is changed, the new one should be html_safe and of the same type
- cache_helper.expects(:output_buffer=).with(responds_with(:html_safe?, true)).with(instance_of(output_buffer.class)).at_least(0)
- assert_nothing_raised do
- cache_helper.send :fragment_for, 'Test fragment name', 'Test fragment', &Proc.new{ nil }
+ cache_helper.stub :controller, controller do
+ cache_helper.stub :output_buffer, output_buffer do
+ assert_called_with cache_helper, :output_buffer=, [output_buffer.class.new(output_buffer)] do
+ assert_nothing_raised do
+ cache_helper.send :fragment_for, 'Test fragment name', 'Test fragment', &Proc.new{ nil }
+ end
+ end
+ end
end
end
def test_safe_buffer
output_buffer = ActiveSupport::SafeBuffer.new
controller = MockController.new
- cache_helper = Object.new
+ cache_helper = Class.new do
+ def self.controller; end;
+ def self.output_buffer; end;
+ def self.output_buffer=; end;
+ end
cache_helper.extend(ActionView::Helpers::CacheHelper)
- cache_helper.expects(:controller).returns(controller).at_least(0)
- cache_helper.expects(:output_buffer).returns(output_buffer).at_least(0)
- # if the output_buffer is changed, the new one should be html_safe and of the same type
- cache_helper.expects(:output_buffer=).with(responds_with(:html_safe?, true)).with(instance_of(output_buffer.class)).at_least(0)
- assert_nothing_raised do
- cache_helper.send :fragment_for, 'Test fragment name', 'Test fragment', &Proc.new{ nil }
+ cache_helper.stub :controller, controller do
+ cache_helper.stub :output_buffer, output_buffer do
+ assert_called_with cache_helper, :output_buffer=, [output_buffer.class.new(output_buffer)] do
+ assert_nothing_raised do
+ cache_helper.send :fragment_for, 'Test fragment name', 'Test fragment', &Proc.new{ nil }
+ end
+ end
+ end
end
end
end
diff --git a/actionpack/test/controller/params_wrapper_test.rb b/actionpack/test/controller/params_wrapper_test.rb
index 8bf016d060..7226beed26 100644
--- a/actionpack/test/controller/params_wrapper_test.rb
+++ b/actionpack/test/controller/params_wrapper_test.rb
@@ -28,8 +28,17 @@ class ParamsWrapperTest < ActionController::TestCase
end
end
- class User; end
- class Person; end
+ class User
+ def self.attribute_names
+ []
+ end
+ end
+
+ class Person
+ def self.attribute_names
+ []
+ end
+ end
tests UsersController
@@ -155,33 +164,28 @@ class ParamsWrapperTest < ActionController::TestCase
end
def test_derived_wrapped_keys_from_matching_model
- User.expects(:respond_to?).with(:attribute_names).returns(true)
- User.expects(:attribute_names).twice.returns(["username"])
-
- with_default_wrapper_options do
- @request.env['CONTENT_TYPE'] = 'application/json'
- post :parse, params: { 'username' => 'sikachu', 'title' => 'Developer' }
- assert_parameters({ 'username' => 'sikachu', 'title' => 'Developer', 'user' => { 'username' => 'sikachu' }})
+ assert_called(User, :attribute_names, times: 2, returns: ["username"]) do
+ with_default_wrapper_options do
+ @request.env['CONTENT_TYPE'] = 'application/json'
+ post :parse, params: { 'username' => 'sikachu', 'title' => 'Developer' }
+ assert_parameters({ 'username' => 'sikachu', 'title' => 'Developer', 'user' => { 'username' => 'sikachu' }})
+ end
end
end
def test_derived_wrapped_keys_from_specified_model
with_default_wrapper_options do
- Person.expects(:respond_to?).with(:attribute_names).returns(true)
- Person.expects(:attribute_names).twice.returns(["username"])
+ assert_called(Person, :attribute_names, times: 2, returns: ["username"]) do
+ UsersController.wrap_parameters Person
- UsersController.wrap_parameters Person
-
- @request.env['CONTENT_TYPE'] = 'application/json'
- post :parse, params: { 'username' => 'sikachu', 'title' => 'Developer' }
- assert_parameters({ 'username' => 'sikachu', 'title' => 'Developer', 'person' => { 'username' => 'sikachu' }})
+ @request.env['CONTENT_TYPE'] = 'application/json'
+ post :parse, params: { 'username' => 'sikachu', 'title' => 'Developer' }
+ assert_parameters({ 'username' => 'sikachu', 'title' => 'Developer', 'person' => { 'username' => 'sikachu' }})
+ end
end
end
def test_not_wrapping_abstract_model
- User.expects(:respond_to?).with(:attribute_names).returns(true)
- User.expects(:attribute_names).returns([])
-
with_default_wrapper_options do
@request.env['CONTENT_TYPE'] = 'application/json'
post :parse, params: { 'username' => 'sikachu', 'title' => 'Developer' }
diff --git a/actionpack/test/controller/request_forgery_protection_test.rb b/actionpack/test/controller/request_forgery_protection_test.rb
index 90fd8669c2..94ffbe3cd0 100644
--- a/actionpack/test/controller/request_forgery_protection_test.rb
+++ b/actionpack/test/controller/request_forgery_protection_test.rb
@@ -379,7 +379,6 @@ module RequestForgeryProtectionTests
end
def test_should_not_raise_error_if_token_is_not_a_string
- @controller.unstub(:valid_authenticity_token?)
assert_blocked do
patch :index, params: { custom_authenticity_token: { foo: 'bar' } }
end
diff --git a/actionpack/test/controller/rescue_test.rb b/actionpack/test/controller/rescue_test.rb
index e767323773..f53f061e10 100644
--- a/actionpack/test/controller/rescue_test.rb
+++ b/actionpack/test/controller/rescue_test.rb
@@ -246,12 +246,15 @@ class RescueControllerTest < ActionController::TestCase
end
def test_rescue_handler_with_argument
- @controller.expects(:show_errors).once.with { |e| e.is_a?(Exception) }
- get :record_invalid
+ assert_called_with @controller, :show_errors, [Exception] do
+ get :record_invalid
+ end
end
+
def test_rescue_handler_with_argument_as_string
- @controller.expects(:show_errors).once.with { |e| e.is_a?(Exception) }
- get :record_invalid_raise_as_string
+ assert_called_with @controller, :show_errors, [Exception] do
+ get :record_invalid_raise_as_string
+ end
end
def test_proc_rescue_handler
diff --git a/actionpack/test/dispatch/debug_exceptions_test.rb b/actionpack/test/dispatch/debug_exceptions_test.rb
index f9f379780c..93258fbceb 100644
--- a/actionpack/test/dispatch/debug_exceptions_test.rb
+++ b/actionpack/test/dispatch/debug_exceptions_test.rb
@@ -272,9 +272,12 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest
test 'uses backtrace cleaner from env' do
@app = DevelopmentApp
- cleaner = stub(:clean => ['passed backtrace cleaner'])
- get "/", headers: { 'action_dispatch.show_exceptions' => true, 'action_dispatch.backtrace_cleaner' => cleaner }
- assert_match(/passed backtrace cleaner/, body)
+ backtrace_cleaner = ActiveSupport::BacktraceCleaner.new
+
+ backtrace_cleaner.stub :clean, ['passed backtrace cleaner'] do
+ get "/", headers: { 'action_dispatch.show_exceptions' => true, 'action_dispatch.backtrace_cleaner' => backtrace_cleaner }
+ assert_match(/passed backtrace cleaner/, body)
+ end
end
test 'logs exception backtrace when all lines silenced' do
diff --git a/actionpack/test/dispatch/exception_wrapper_test.rb b/actionpack/test/dispatch/exception_wrapper_test.rb
index f37cce4d45..dfbb91c0ca 100644
--- a/actionpack/test/dispatch/exception_wrapper_test.rb
+++ b/actionpack/test/dispatch/exception_wrapper_test.rb
@@ -25,27 +25,29 @@ module ActionDispatch
exception = TestError.new("lib/file.rb:42:in `index'")
wrapper = ExceptionWrapper.new(nil, exception)
- wrapper.expects(:source_fragment).with('lib/file.rb', 42).returns('foo')
-
- assert_equal [ code: 'foo', line_number: 42 ], wrapper.source_extracts
+ assert_called_with(wrapper, :source_fragment, ['lib/file.rb', 42], returns: 'foo') do
+ assert_equal [ code: 'foo', line_number: 42 ], wrapper.source_extracts
+ end
end
test '#source_extracts works with Windows paths' do
exc = TestError.new("c:/path/to/rails/app/controller.rb:27:in 'index':")
wrapper = ExceptionWrapper.new(nil, exc)
- wrapper.expects(:source_fragment).with('c:/path/to/rails/app/controller.rb', 27).returns('nothing')
- assert_equal [ code: 'nothing', line_number: 27 ], wrapper.source_extracts
+ assert_called_with(wrapper, :source_fragment, ['c:/path/to/rails/app/controller.rb', 27], returns: 'nothing') do
+ assert_equal [ code: 'nothing', line_number: 27 ], wrapper.source_extracts
+ end
end
test '#source_extracts works with non standard backtrace' do
exc = TestError.new('invalid')
wrapper = ExceptionWrapper.new(nil, exc)
- wrapper.expects(:source_fragment).with('invalid', 0).returns('nothing')
- assert_equal [ code: 'nothing', line_number: 0 ], wrapper.source_extracts
+ assert_called_with(wrapper, :source_fragment, ['invalid', 0], returns: 'nothing') do
+ assert_equal [ code: 'nothing', line_number: 0 ], wrapper.source_extracts
+ end
end
test '#application_trace returns traces only from the application' do
diff --git a/actionpack/test/dispatch/request/session_test.rb b/actionpack/test/dispatch/request/session_test.rb
index 410e3194e2..ae0e7e93ed 100644
--- a/actionpack/test/dispatch/request/session_test.rb
+++ b/actionpack/test/dispatch/request/session_test.rb
@@ -110,7 +110,7 @@ module ActionDispatch
Class.new {
def load_session(env); [1, {}]; end
def session_exists?(env); true; end
- def destroy_session(env, id, options); 123; end
+ def delete_session(env, id, options); 123; end
}.new
end
end
diff --git a/actionpack/test/dispatch/request_test.rb b/actionpack/test/dispatch/request_test.rb
index ff63c10e8d..258d097b7c 100644
--- a/actionpack/test/dispatch/request_test.rb
+++ b/actionpack/test/dispatch/request_test.rb
@@ -749,20 +749,23 @@ end
class RequestFormat < BaseRequestTest
test "xml format" do
request = stub_request
- request.expects(:parameters).at_least_once.returns({ :format => 'xml' })
- assert_equal Mime::XML, request.format
+ assert_called(request, :parameters, times: 2, returns: {format: :xml}) do
+ assert_equal Mime::XML, request.format
+ end
end
test "xhtml format" do
request = stub_request
- request.expects(:parameters).at_least_once.returns({ :format => 'xhtml' })
- assert_equal Mime::HTML, request.format
+ assert_called(request, :parameters, times: 2, returns: {format: :xhtml}) do
+ assert_equal Mime::HTML, request.format
+ end
end
test "txt format" do
request = stub_request
- request.expects(:parameters).at_least_once.returns({ :format => 'txt' })
- assert_equal Mime::TEXT, request.format
+ assert_called(request, :parameters, times: 2, returns: {format: :txt}) do
+ assert_equal Mime::TEXT, request.format
+ end
end
test "XMLHttpRequest" do
@@ -770,21 +773,25 @@ class RequestFormat < BaseRequestTest
'HTTP_X_REQUESTED_WITH' => 'XMLHttpRequest',
'HTTP_ACCEPT' => [Mime::JS, Mime::HTML, Mime::XML, "text/xml", Mime::ALL].join(",")
)
- request.expects(:parameters).at_least_once.returns({})
- assert request.xhr?
- assert_equal Mime::JS, request.format
+
+ assert_called(request, :parameters, times: 1, returns: {}) do
+ assert request.xhr?
+ assert_equal Mime::JS, request.format
+ end
end
test "can override format with parameter negative" do
request = stub_request
- request.expects(:parameters).at_least_once.returns({ :format => :txt })
- assert !request.format.xml?
+ assert_called(request, :parameters, times: 2, returns: {format: :txt}) do
+ assert !request.format.xml?
+ end
end
test "can override format with parameter positive" do
request = stub_request
- request.expects(:parameters).at_least_once.returns({ :format => :xml })
- assert request.format.xml?
+ assert_called(request, :parameters, times: 2, returns: {format: :xml}) do
+ assert request.format.xml?
+ end
end
test "formats text/html with accept header" do
@@ -810,23 +817,26 @@ class RequestFormat < BaseRequestTest
test "formats format:text with accept header" do
request = stub_request
- request.expects(:parameters).at_least_once.returns({ :format => :txt })
- assert_equal [Mime::TEXT], request.formats
+ assert_called(request, :parameters, times: 2, returns: {format: :txt}) do
+ assert_equal [Mime::TEXT], request.formats
+ end
end
test "formats format:unknown with accept header" do
request = stub_request
- request.expects(:parameters).at_least_once.returns({ :format => :unknown })
- assert_instance_of Mime::NullType, request.format
+ assert_called(request, :parameters, times: 2, returns: {format: :unknown}) do
+ assert_instance_of Mime::NullType, request.format
+ end
end
test "format is not nil with unknown format" do
request = stub_request
- request.expects(:parameters).at_least_once.returns({ format: :hello })
- assert request.format.nil?
- assert_not request.format.html?
- assert_not request.format.xml?
- assert_not request.format.json?
+ assert_called(request, :parameters, times: 2, returns: {format: :hello}) do
+ assert request.format.nil?
+ assert_not request.format.html?
+ assert_not request.format.xml?
+ assert_not request.format.json?
+ end
end
test "format does not throw exceptions when malformed parameters" do
@@ -837,8 +847,9 @@ class RequestFormat < BaseRequestTest
test "formats with xhr request" do
request = stub_request 'HTTP_X_REQUESTED_WITH' => "XMLHttpRequest"
- request.expects(:parameters).at_least_once.returns({})
- assert_equal [Mime::JS], request.formats
+ assert_called(request, :parameters, times: 1, returns: {}) do
+ assert_equal [Mime::JS], request.formats
+ end
end
test "ignore_accept_header" do
@@ -847,30 +858,37 @@ class RequestFormat < BaseRequestTest
begin
request = stub_request 'HTTP_ACCEPT' => 'application/xml'
- request.expects(:parameters).at_least_once.returns({})
- assert_equal [ Mime::HTML ], request.formats
+ assert_called(request, :parameters, times: 1, returns: {}) do
+ assert_equal [ Mime::HTML ], request.formats
+ end
request = stub_request 'HTTP_ACCEPT' => 'koz-asked/something-crazy'
- request.expects(:parameters).at_least_once.returns({})
- assert_equal [ Mime::HTML ], request.formats
+ assert_called(request, :parameters, times: 1, returns: {}) do
+ assert_equal [ Mime::HTML ], request.formats
+ end
request = stub_request 'HTTP_ACCEPT' => '*/*;q=0.1'
- request.expects(:parameters).at_least_once.returns({})
- assert_equal [ Mime::HTML ], request.formats
+ assert_called(request, :parameters, times: 1, returns: {}) do
+ assert_equal [ Mime::HTML ], request.formats
+ end
request = stub_request 'HTTP_ACCEPT' => 'application/jxw'
- request.expects(:parameters).at_least_once.returns({})
- assert_equal [ Mime::HTML ], request.formats
+ assert_called(request, :parameters, times: 1, returns: {}) do
+ assert_equal [ Mime::HTML ], request.formats
+ end
request = stub_request 'HTTP_ACCEPT' => 'application/xml',
'HTTP_X_REQUESTED_WITH' => "XMLHttpRequest"
- request.expects(:parameters).at_least_once.returns({})
- assert_equal [ Mime::JS ], request.formats
+
+ assert_called(request, :parameters, times: 1, returns: {}) do
+ assert_equal [ Mime::JS ], request.formats
+ end
request = stub_request 'HTTP_ACCEPT' => 'application/xml',
'HTTP_X_REQUESTED_WITH' => "XMLHttpRequest"
- request.expects(:parameters).at_least_once.returns({:format => :json})
- assert_equal [ Mime::JSON ], request.formats
+ assert_called(request, :parameters, times: 2, returns: {format: :json}) do
+ assert_equal [ Mime::JSON ], request.formats
+ end
ensure
ActionDispatch::Request.ignore_accept_header = old_ignore_accept_header
end
@@ -922,12 +940,14 @@ end
class RequestParameters < BaseRequestTest
test "parameters" do
request = stub_request
- request.expects(:request_parameters).at_least_once.returns({ "foo" => 1 })
- request.expects(:query_parameters).at_least_once.returns({ "bar" => 2 })
- assert_equal({"foo" => 1, "bar" => 2}, request.parameters)
- assert_equal({"foo" => 1}, request.request_parameters)
- assert_equal({"bar" => 2}, request.query_parameters)
+ assert_called(request, :request_parameters, times: 2, returns: {"foo" => 1}) do
+ assert_called(request, :query_parameters, times: 2, returns: {"bar" => 2}) do
+ assert_equal({"foo" => 1, "bar" => 2}, request.parameters)
+ assert_equal({"foo" => 1}, request.request_parameters)
+ assert_equal({"bar" => 2}, request.query_parameters)
+ end
+ end
end
test "parameters not accessible after rack parse error" do
diff --git a/actionpack/test/dispatch/session/abstract_store_test.rb b/actionpack/test/dispatch/session/abstract_store_test.rb
index 1c35144e6f..d38d1bbce6 100644
--- a/actionpack/test/dispatch/session/abstract_store_test.rb
+++ b/actionpack/test/dispatch/session/abstract_store_test.rb
@@ -10,13 +10,13 @@ module ActionDispatch
super
end
- def get_session(env, sid)
+ def find_session(env, sid)
sid ||= 1
session = @sessions[sid] ||= {}
[sid, session]
end
- def set_session(env, sid, session, options)
+ def write_session(env, sid, session, options)
@sessions[sid] = session
end
end
diff --git a/actionpack/test/dispatch/ssl_test.rb b/actionpack/test/dispatch/ssl_test.rb
index 017e9ba2dd..7a5b8393dc 100644
--- a/actionpack/test/dispatch/ssl_test.rb
+++ b/actionpack/test/dispatch/ssl_test.rb
@@ -1,230 +1,199 @@
require 'abstract_unit'
class SSLTest < ActionDispatch::IntegrationTest
- def default_app
- lambda { |env|
- headers = {'Content-Type' => "text/html"}
- headers['Set-Cookie'] = "id=1; path=/\ntoken=abc; path=/; secure; HttpOnly"
- [200, headers, ["OK"]]
+ HEADERS = Rack::Utils::HeaderHash.new 'Content-Type' => 'text/html'
+
+ attr_accessor :app
+
+ def build_app(headers: {}, ssl_options: {})
+ headers = HEADERS.merge(headers)
+ ActionDispatch::SSL.new lambda { |env| [200, headers, []] }, ssl_options
+ end
+end
+
+class RedirectSSLTest < SSLTest
+ def assert_not_redirected(url, headers: {})
+ self.app = build_app
+ get url, headers: headers
+ assert_response :ok
+ end
+
+ def assert_redirected(host: nil, port: nil, status: 301, body: [],
+ deprecated_host: nil, deprecated_port: nil,
+ from: 'http://a/b?c=d', to: from.sub('http', 'https'))
+
+ self.app = build_app ssl_options: {
+ redirect: { host: host, port: port, status: status, body: body },
+ host: deprecated_host, port: deprecated_port
}
+
+ get from
+ assert_response status
+ assert_redirected_to to
+ assert_equal body.join, @response.body
end
- def app
- @app ||= ActionDispatch::SSL.new(default_app)
+ test 'https is not redirected' do
+ assert_not_redirected 'https://example.org'
end
- attr_writer :app
- def test_allows_https_url
- get "https://example.org/path?key=value"
- assert_response :success
+ test 'proxied https is not redirected' do
+ assert_not_redirected 'http://example.org', headers: { 'HTTP_X_FORWARDED_PROTO' => 'https' }
end
- def test_allows_https_proxy_header_url
- get "http://example.org/", headers: { 'HTTP_X_FORWARDED_PROTO' => "https" }
- assert_response :success
+ test 'http is redirected to https' do
+ assert_redirected
end
- def test_redirects_http_to_https
- get "http://example.org/path?key=value"
- assert_response :redirect
- assert_equal "https://example.org/path?key=value",
- response.headers['Location']
+ test 'redirect with non-301 status' do
+ assert_redirected status: 307
end
- def test_hsts_header_by_default
- get "https://example.org/"
- assert_equal "max-age=31536000",
- response.headers['Strict-Transport-Security']
+ test 'redirect with custom body' do
+ assert_redirected body: ['foo']
end
- def test_no_hsts_with_insecure_connection
- get "http://example.org/"
- assert_not response.headers['Strict-Transport-Security']
+ test 'redirect to specific host' do
+ assert_redirected host: 'ssl', to: 'https://ssl/b?c=d'
end
- def test_hsts_header
- self.app = ActionDispatch::SSL.new(default_app, :hsts => true)
- get "https://example.org/"
- assert_equal "max-age=31536000",
- response.headers['Strict-Transport-Security']
+ test 'redirect to default port' do
+ assert_redirected port: 443
end
- def test_disable_hsts_header
- self.app = ActionDispatch::SSL.new(default_app, :hsts => false)
- get "https://example.org/"
- assert_not response.headers['Strict-Transport-Security']
+ test 'redirect to non-default port' do
+ assert_redirected port: 8443, to: 'https://a:8443/b?c=d'
end
- def test_hsts_expires
- self.app = ActionDispatch::SSL.new(default_app, :hsts => { :expires => 500 })
- get "https://example.org/"
- assert_equal "max-age=500",
- response.headers['Strict-Transport-Security']
+ test 'redirect to different host and non-default port' do
+ assert_redirected host: 'ssl', port: 8443, to: 'https://ssl:8443/b?c=d'
end
- def test_hsts_expires_with_duration
- self.app = ActionDispatch::SSL.new(default_app, :hsts => { :expires => 1.year })
- get "https://example.org/"
- assert_equal "max-age=31557600",
- response.headers['Strict-Transport-Security']
+ test 'redirect to different host including port' do
+ assert_redirected host: 'ssl:443', to: 'https://ssl:443/b?c=d'
end
- def test_hsts_include_subdomains
- self.app = ActionDispatch::SSL.new(default_app, :hsts => { :subdomains => true })
- get "https://example.org/"
- assert_equal "max-age=31536000; includeSubDomains",
- response.headers['Strict-Transport-Security']
+ test ':host is deprecated, moved within redirect: { host: … }' do
+ assert_deprecated do
+ assert_redirected deprecated_host: 'foo', to: 'https://foo/b?c=d'
+ end
end
- def test_flag_cookies_as_secure
- get "https://example.org/"
- assert_equal ["id=1; path=/; secure", "token=abc; path=/; secure; HttpOnly" ],
- response.headers['Set-Cookie'].split("\n")
+ test ':port is deprecated, moved within redirect: { port: … }' do
+ assert_deprecated do
+ assert_redirected deprecated_port: 1, to: 'https://a:1/b?c=d'
+ end
end
+end
- def test_flag_cookies_as_secure_at_end_of_line
- self.app = ActionDispatch::SSL.new(lambda { |env|
- headers = {
- 'Content-Type' => "text/html",
- 'Set-Cookie' => "problem=def; path=/; HttpOnly; secure"
- }
- [200, headers, ["OK"]]
- })
+class StrictTransportSecurityTest < SSLTest
+ EXPECTED = 'max-age=15552000'
- get "https://example.org/"
- assert_equal ["problem=def; path=/; HttpOnly; secure"],
- response.headers['Set-Cookie'].split("\n")
+ def assert_hsts(expected, url: 'https://example.org', hsts: {}, headers: {})
+ self.app = build_app ssl_options: { hsts: hsts }, headers: headers
+ get url
+ assert_equal expected, response.headers['Strict-Transport-Security']
end
- def test_flag_cookies_as_secure_with_more_spaces_before
- self.app = ActionDispatch::SSL.new(lambda { |env|
- headers = {
- 'Content-Type' => "text/html",
- 'Set-Cookie' => "problem=def; path=/; HttpOnly; secure"
- }
- [200, headers, ["OK"]]
- })
+ test 'enabled by default' do
+ assert_hsts EXPECTED
+ end
- get "https://example.org/"
- assert_equal ["problem=def; path=/; HttpOnly; secure"],
- response.headers['Set-Cookie'].split("\n")
+ test 'not sent with http:// responses' do
+ assert_hsts nil, url: 'http://example.org'
end
- def test_flag_cookies_as_secure_with_more_spaces_after
- self.app = ActionDispatch::SSL.new(lambda { |env|
- headers = {
- 'Content-Type' => "text/html",
- 'Set-Cookie' => "problem=def; path=/; secure; HttpOnly"
- }
- [200, headers, ["OK"]]
- })
+ test 'defers to app-provided header' do
+ assert_hsts 'app-provided', headers: { 'Strict-Transport-Security' => 'app-provided' }
+ end
- get "https://example.org/"
- assert_equal ["problem=def; path=/; secure; HttpOnly"],
- response.headers['Set-Cookie'].split("\n")
+ test 'hsts: true enables default settings' do
+ assert_hsts EXPECTED, hsts: true
end
+ test 'hsts: false sets max-age to zero, clearing browser HSTS settings' do
+ assert_hsts 'max-age=0', hsts: false
+ end
- def test_flag_cookies_as_secure_with_has_not_spaces_before
- self.app = ActionDispatch::SSL.new(lambda { |env|
- headers = {
- 'Content-Type' => "text/html",
- 'Set-Cookie' => "problem=def; path=/;secure; HttpOnly"
- }
- [200, headers, ["OK"]]
- })
+ test ':expires sets max-age' do
+ assert_hsts 'max-age=500', hsts: { expires: 500 }
+ end
- get "https://example.org/"
- assert_equal ["problem=def; path=/;secure; HttpOnly"],
- response.headers['Set-Cookie'].split("\n")
+ test ':expires supports AS::Duration arguments' do
+ assert_hsts 'max-age=31557600', hsts: { expires: 1.year }
end
- def test_flag_cookies_as_secure_with_has_not_spaces_after
- self.app = ActionDispatch::SSL.new(lambda { |env|
- headers = {
- 'Content-Type' => "text/html",
- 'Set-Cookie' => "problem=def; path=/; secure;HttpOnly"
- }
- [200, headers, ["OK"]]
- })
+ test 'include subdomains' do
+ assert_hsts "#{EXPECTED}; includeSubDomains", hsts: { subdomains: true }
+ end
- get "https://example.org/"
- assert_equal ["problem=def; path=/; secure;HttpOnly"],
- response.headers['Set-Cookie'].split("\n")
+ test 'exclude subdomains' do
+ assert_hsts EXPECTED, hsts: { subdomains: false }
end
- def test_flag_cookies_as_secure_with_ignore_case
- self.app = ActionDispatch::SSL.new(lambda { |env|
- headers = {
- 'Content-Type' => "text/html",
- 'Set-Cookie' => "problem=def; path=/; Secure; HttpOnly"
- }
- [200, headers, ["OK"]]
- })
+ test 'opt in to browser preload lists' do
+ assert_hsts "#{EXPECTED}; preload", hsts: { preload: true }
+ end
- get "https://example.org/"
- assert_equal ["problem=def; path=/; Secure; HttpOnly"],
- response.headers['Set-Cookie'].split("\n")
+ test 'opt out of browser preload lists' do
+ assert_hsts EXPECTED, hsts: { preload: false }
end
+end
- def test_no_cookies
- self.app = ActionDispatch::SSL.new(lambda { |env|
- [200, {'Content-Type' => "text/html"}, ["OK"]]
- })
- get "https://example.org/"
- assert !response.headers['Set-Cookie']
+class SecureCookiesTest < SSLTest
+ DEFAULT = %(id=1; path=/\ntoken=abc; path=/; secure; HttpOnly)
+
+ def get(**options)
+ self.app = build_app(**options)
+ super 'https://example.org'
+ end
+
+ def assert_cookies(*expected)
+ assert_equal expected, response.headers['Set-Cookie'].split("\n")
+ end
+
+ def test_flag_cookies_as_secure
+ get headers: { 'Set-Cookie' => DEFAULT }
+ assert_cookies 'id=1; path=/; secure', 'token=abc; path=/; secure; HttpOnly'
end
- def test_redirect_to_host
- self.app = ActionDispatch::SSL.new(default_app, :host => "ssl.example.org")
- get "http://example.org/path?key=value"
- assert_equal "https://ssl.example.org/path?key=value",
- response.headers['Location']
+ def test_flag_cookies_as_secure_at_end_of_line
+ get headers: { 'Set-Cookie' => 'problem=def; path=/; HttpOnly; secure' }
+ assert_cookies 'problem=def; path=/; HttpOnly; secure'
+ end
+
+ def test_flag_cookies_as_secure_with_more_spaces_before
+ get headers: { 'Set-Cookie' => 'problem=def; path=/; HttpOnly; secure' }
+ assert_cookies 'problem=def; path=/; HttpOnly; secure'
end
- def test_redirect_to_port
- self.app = ActionDispatch::SSL.new(default_app, :port => 8443)
- get "http://example.org/path?key=value"
- assert_equal "https://example.org:8443/path?key=value",
- response.headers['Location']
+ def test_flag_cookies_as_secure_with_more_spaces_after
+ get headers: { 'Set-Cookie' => 'problem=def; path=/; secure; HttpOnly' }
+ assert_cookies 'problem=def; path=/; secure; HttpOnly'
end
- def test_redirect_to_host_and_port
- self.app = ActionDispatch::SSL.new(default_app, :host => "ssl.example.org", :port => 8443)
- get "http://example.org/path?key=value"
- assert_equal "https://ssl.example.org:8443/path?key=value",
- response.headers['Location']
+ def test_flag_cookies_as_secure_with_has_not_spaces_before
+ get headers: { 'Set-Cookie' => 'problem=def; path=/;secure; HttpOnly' }
+ assert_cookies 'problem=def; path=/;secure; HttpOnly'
end
- def test_redirect_to_host_with_port
- self.app = ActionDispatch::SSL.new(default_app, :host => "ssl.example.org:443")
- get "http://example.org/path?key=value"
- assert_equal "https://ssl.example.org:443/path?key=value",
- response.headers['Location']
+ def test_flag_cookies_as_secure_with_has_not_spaces_after
+ get headers: { 'Set-Cookie' => 'problem=def; path=/; secure;HttpOnly' }
+ assert_cookies 'problem=def; path=/; secure;HttpOnly'
end
- def test_redirect_to_secure_host_when_on_subdomain
- self.app = ActionDispatch::SSL.new(default_app, :host => "ssl.example.org")
- get "http://ssl.example.org/path?key=value"
- assert_equal "https://ssl.example.org/path?key=value",
- response.headers['Location']
+ def test_flag_cookies_as_secure_with_ignore_case
+ get headers: { 'Set-Cookie' => 'problem=def; path=/; Secure; HttpOnly' }
+ assert_cookies 'problem=def; path=/; Secure; HttpOnly'
end
- def test_redirect_to_secure_subdomain_when_on_deep_subdomain
- self.app = ActionDispatch::SSL.new(default_app, :host => "example.co.uk")
- get "http://double.rainbow.what.does.it.mean.example.co.uk/path?key=value"
- assert_equal "https://example.co.uk/path?key=value",
- response.headers['Location']
+ def test_no_cookies
+ get
+ assert_nil response.headers['Set-Cookie']
end
def test_keeps_original_headers_behavior
- headers = Rack::Utils::HeaderHash.new(
- "Content-Type" => "text/html",
- "Connection" => ["close"]
- )
- self.app = ActionDispatch::SSL.new(lambda { |env| [200, headers, ["OK"]] })
-
- get "https://example.org/"
- assert_equal "close", response.headers["Connection"]
+ get headers: { 'Connection' => %w[close] }
+ assert_equal 'close', response.headers['Connection']
end
end
diff --git a/actionview/lib/action_view/helpers/url_helper.rb b/actionview/lib/action_view/helpers/url_helper.rb
index d676a0a931..5684de35e8 100644
--- a/actionview/lib/action_view/helpers/url_helper.rb
+++ b/actionview/lib/action_view/helpers/url_helper.rb
@@ -464,7 +464,7 @@ module ActionView
extras = %w{ cc bcc body subject reply_to }.map! { |item|
option = html_options.delete(item).presence || next
- "#{item.dasherize}=#{Rack::Utils.escape_path(option)}"
+ "#{item.dasherize}=#{ERB::Util.url_encode(option)}"
}.compact
extras = extras.empty? ? '' : '?' + extras.join('&')
diff --git a/actionview/lib/action_view/template.rb b/actionview/lib/action_view/template.rb
index e232808dcb..0ed208f27e 100644
--- a/actionview/lib/action_view/template.rb
+++ b/actionview/lib/action_view/template.rb
@@ -141,7 +141,7 @@ module ActionView
@compile_mutex = Mutex.new
end
- # Returns if the underlying handler supports streaming. If so,
+ # Returns whether the underlying handler supports streaming. If so,
# a streaming buffer *may* be passed when it start rendering.
def supports_streaming?
handler.respond_to?(:supports_streaming?) && handler.supports_streaming?
diff --git a/activemodel/CHANGELOG.md b/activemodel/CHANGELOG.md
index 54fe5794e1..d7e64b8bf0 100644
--- a/activemodel/CHANGELOG.md
+++ b/activemodel/CHANGELOG.md
@@ -1,3 +1,21 @@
+* Validate multiple contexts on `valid?` and `invalid?` at once.
+
+ Example:
+
+ class Person
+ include ActiveModel::Validations
+
+ attr_reader :name, :title
+ validates_presence_of :name, on: :create
+ validates_presence_of :title, on: :update
+ end
+
+ person = Person.new
+ person.valid?([:create, :update]) # => true
+ person.errors.messages # => {:name=>["can't be blank"], :title=>["can't be blank"]}
+
+ *Dmitry Polushkin*
+
* Add case_sensitive option for confirmation validator in models.
*Akshat Sharma*
diff --git a/activemodel/lib/active_model/validations.rb b/activemodel/lib/active_model/validations.rb
index 5f1dde4aa3..f23c920d87 100644
--- a/activemodel/lib/active_model/validations.rb
+++ b/activemodel/lib/active_model/validations.rb
@@ -162,7 +162,7 @@ module ActiveModel
options = options.dup
options[:if] = Array(options[:if])
options[:if].unshift ->(o) {
- Array(options[:on]).include?(o.validation_context)
+ !(Array(options[:on]) & Array(o.validation_context)).empty?
}
end
diff --git a/activemodel/test/cases/validations/validations_context_test.rb b/activemodel/test/cases/validations/validations_context_test.rb
index 150dce379f..b901a1523e 100644
--- a/activemodel/test/cases/validations/validations_context_test.rb
+++ b/activemodel/test/cases/validations/validations_context_test.rb
@@ -8,6 +8,7 @@ class ValidationsContextTest < ActiveModel::TestCase
end
ERROR_MESSAGE = "Validation error from validator"
+ ANOTHER_ERROR_MESSAGE = "Another validation error from validator"
class ValidatorThatAddsErrors < ActiveModel::Validator
def validate(record)
@@ -15,6 +16,12 @@ class ValidationsContextTest < ActiveModel::TestCase
end
end
+ class AnotherValidatorThatAddsErrors < ActiveModel::Validator
+ def validate(record)
+ record.errors[:base] << ANOTHER_ERROR_MESSAGE
+ end
+ end
+
test "with a class that adds errors on create and validating a new model with no arguments" do
Topic.validates_with(ValidatorThatAddsErrors, on: :create)
topic = Topic.new
@@ -46,4 +53,16 @@ class ValidationsContextTest < ActiveModel::TestCase
assert topic.invalid?(:context2), "Validation did not run on context2 when 'on' is set to context1 and context2"
assert topic.errors[:base].include?(ERROR_MESSAGE)
end
+
+ test "with a class that validating a model for a multiple contexts" do
+ Topic.validates_with(ValidatorThatAddsErrors, on: :context1)
+ Topic.validates_with(AnotherValidatorThatAddsErrors, on: :context2)
+
+ topic = Topic.new
+ assert topic.valid?, "Validation ran with no context given when 'on' is set to context1 and context2"
+
+ assert topic.invalid?([:context1, :context2]), "Validation did not run on context1 when 'on' is set to context1 and context2"
+ assert topic.errors[:base].include?(ERROR_MESSAGE)
+ assert topic.errors[:base].include?(ANOTHER_ERROR_MESSAGE)
+ end
end
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md
index ec3ec06c2b..554bec17d6 100644
--- a/activerecord/CHANGELOG.md
+++ b/activerecord/CHANGELOG.md
@@ -1,3 +1,25 @@
+* Correct query for PostgreSQL 8.2 compatibility.
+
+ *Ben Murphy*, *Matthew Draper*
+
+* `bin/rake db:migrate` uses
+ `ActiveRecord::Tasks::DatabaseTasks.migrations_paths` instead of
+ `Migrator.migrations_paths`.
+
+ *Tobias Bielohlawek*
+
+* Support dropping indexes concurrently in PostgreSQL.
+
+ See http://www.postgresql.org/docs/9.4/static/sql-dropindex.html for more
+ details.
+
+ *Grey Baker*
+
+* Deprecate passing conditions to `ActiveRecord::Relation#delete_all`
+ and `ActiveRecord::Relation#destroy_all`.
+
+ *Wojciech Wnętrzak*
+
* PostgreSQL, `create_schema`, `drop_schema` and `rename_table` now quote
schema names.
@@ -25,7 +47,7 @@
* Uniqueness validator raises descriptive error when running on a persisted
record without primary key.
- Closes #21304.
+ Fixes #21304.
*Yves Senn*
@@ -41,7 +63,7 @@
* Descriptive error message when fixtures contain a missing column.
- Closes #21201.
+ Fixes #21201.
*Yves Senn*
@@ -65,11 +87,11 @@
sleep 10 # Throttles the delete queries
end
- Closes #20933.
+ Fixes #20933.
*Sina Siadat*
-* Added methods for PostgreSQL geometric data types to use in migrations
+* Added methods for PostgreSQL geometric data types to use in migrations.
Example:
@@ -1095,7 +1117,7 @@
* `eager_load` preserves readonly flag for associations.
- Closes #15853.
+ Fixes #15853.
*Takashi Kokubun*
@@ -1151,7 +1173,7 @@
* Fix bug with `ActiveRecord::Type::Numeric` that caused negative values to
be marked as having changed when set to the same negative value.
- Closes #18161.
+ Fixes #18161.
*Daniel Fox*
@@ -1166,7 +1188,7 @@
before loading the schema. This is left for the user to do.
`db:test:prepare` will still purge the database.
- Closes #17945.
+ Fixes #17945.
*Yves Senn*
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb
index 19ef37e228..65eb6fba27 100644
--- a/activerecord/lib/active_record/associations.rb
+++ b/activerecord/lib/active_record/associations.rb
@@ -5,65 +5,105 @@ require 'active_record/errors'
module ActiveRecord
class AssociationNotFoundError < ConfigurationError #:nodoc:
- def initialize(record, association_name)
- super("Association named '#{association_name}' was not found on #{record.class.name}; perhaps you misspelled it?")
+ def initialize(record = nil, association_name = nil)
+ if record && association_name
+ super("Association named '#{association_name}' was not found on #{record.class.name}; perhaps you misspelled it?")
+ else
+ super("Association was not found.")
+ end
end
end
class InverseOfAssociationNotFoundError < ActiveRecordError #:nodoc:
- def initialize(reflection, associated_class = nil)
- super("Could not find the inverse association for #{reflection.name} (#{reflection.options[:inverse_of].inspect} in #{associated_class.nil? ? reflection.class_name : associated_class.name})")
+ def initialize(reflection = nil, associated_class = nil)
+ if reflection
+ super("Could not find the inverse association for #{reflection.name} (#{reflection.options[:inverse_of].inspect} in #{associated_class.nil? ? reflection.class_name : associated_class.name})")
+ else
+ super("Could not find the inverse association.")
+ end
end
end
class HasManyThroughAssociationNotFoundError < ActiveRecordError #:nodoc:
- def initialize(owner_class_name, reflection)
- super("Could not find the association #{reflection.options[:through].inspect} in model #{owner_class_name}")
+ def initialize(owner_class_name = nil, reflection = nil)
+ if owner_class_name && reflection
+ super("Could not find the association #{reflection.options[:through].inspect} in model #{owner_class_name}")
+ else
+ super("Could not find the association.")
+ end
end
end
class HasManyThroughAssociationPolymorphicSourceError < ActiveRecordError #:nodoc:
- def initialize(owner_class_name, reflection, source_reflection)
- super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' on the polymorphic object '#{source_reflection.class_name}##{source_reflection.name}' without 'source_type'. Try adding 'source_type: \"#{reflection.name.to_s.classify}\"' to 'has_many :through' definition.")
+ def initialize(owner_class_name = nil, reflection = nil, source_reflection = nil)
+ if owner_class_name && reflection && source_reflection
+ super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' on the polymorphic object '#{source_reflection.class_name}##{source_reflection.name}' without 'source_type'. Try adding 'source_type: \"#{reflection.name.to_s.classify}\"' to 'has_many :through' definition.")
+ else
+ super("Cannot have a has_many :through association.")
+ end
end
end
class HasManyThroughAssociationPolymorphicThroughError < ActiveRecordError #:nodoc:
- def initialize(owner_class_name, reflection)
- super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' which goes through the polymorphic association '#{owner_class_name}##{reflection.through_reflection.name}'.")
+ def initialize(owner_class_name = nil, reflection = nil)
+ if owner_class_name && reflection
+ super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' which goes through the polymorphic association '#{owner_class_name}##{reflection.through_reflection.name}'.")
+ else
+ super("Cannot have a has_many :through association.")
+ end
end
end
class HasManyThroughAssociationPointlessSourceTypeError < ActiveRecordError #:nodoc:
- def initialize(owner_class_name, reflection, source_reflection)
- super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' with a :source_type option if the '#{reflection.through_reflection.class_name}##{source_reflection.name}' is not polymorphic. Try removing :source_type on your association.")
+ def initialize(owner_class_name = nil, reflection = nil, source_reflection = nil)
+ if owner_class_name && reflection && source_reflection
+ super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' with a :source_type option if the '#{reflection.through_reflection.class_name}##{source_reflection.name}' is not polymorphic. Try removing :source_type on your association.")
+ else
+ super("Cannot have a has_many :through association.")
+ end
end
end
class HasOneThroughCantAssociateThroughCollection < ActiveRecordError #:nodoc:
- def initialize(owner_class_name, reflection, through_reflection)
- super("Cannot have a has_one :through association '#{owner_class_name}##{reflection.name}' where the :through association '#{owner_class_name}##{through_reflection.name}' is a collection. Specify a has_one or belongs_to association in the :through option instead.")
+ def initialize(owner_class_name = nil, reflection = nil, through_reflection = nil)
+ if owner_class_name && reflection && through_reflection
+ super("Cannot have a has_one :through association '#{owner_class_name}##{reflection.name}' where the :through association '#{owner_class_name}##{through_reflection.name}' is a collection. Specify a has_one or belongs_to association in the :through option instead.")
+ else
+ super("Cannot have a has_one :through association.")
+ end
end
end
class HasOneAssociationPolymorphicThroughError < ActiveRecordError #:nodoc:
- def initialize(owner_class_name, reflection)
- super("Cannot have a has_one :through association '#{owner_class_name}##{reflection.name}' which goes through the polymorphic association '#{owner_class_name}##{reflection.through_reflection.name}'.")
+ def initialize(owner_class_name = nil, reflection = nil)
+ if owner_class_name && reflection
+ super("Cannot have a has_one :through association '#{owner_class_name}##{reflection.name}' which goes through the polymorphic association '#{owner_class_name}##{reflection.through_reflection.name}'.")
+ else
+ super("Cannot have a has_one :through association.")
+ end
end
end
class HasManyThroughSourceAssociationNotFoundError < ActiveRecordError #:nodoc:
- def initialize(reflection)
- through_reflection = reflection.through_reflection
- source_reflection_names = reflection.source_reflection_names
- source_associations = reflection.through_reflection.klass._reflections.keys
- super("Could not find the source association(s) #{source_reflection_names.collect(&:inspect).to_sentence(:two_words_connector => ' or ', :last_word_connector => ', or ', :locale => :en)} in model #{through_reflection.klass}. Try 'has_many #{reflection.name.inspect}, :through => #{through_reflection.name.inspect}, :source => <name>'. Is it one of #{source_associations.to_sentence(:two_words_connector => ' or ', :last_word_connector => ', or ', :locale => :en)}?")
+ def initialize(reflection = nil)
+ if reflection
+ through_reflection = reflection.through_reflection
+ source_reflection_names = reflection.source_reflection_names
+ source_associations = reflection.through_reflection.klass._reflections.keys
+ super("Could not find the source association(s) #{source_reflection_names.collect(&:inspect).to_sentence(:two_words_connector => ' or ', :last_word_connector => ', or ', :locale => :en)} in model #{through_reflection.klass}. Try 'has_many #{reflection.name.inspect}, :through => #{through_reflection.name.inspect}, :source => <name>'. Is it one of #{source_associations.to_sentence(:two_words_connector => ' or ', :last_word_connector => ', or ', :locale => :en)}?")
+ else
+ super("Could not find the source association(s).")
+ end
end
end
class ThroughCantAssociateThroughHasOneOrManyReflection < ActiveRecordError #:nodoc:
- def initialize(owner, reflection)
- super("Cannot modify association '#{owner.class.name}##{reflection.name}' because the source reflection class '#{reflection.source_reflection.class_name}' is associated to '#{reflection.through_reflection.class_name}' via :#{reflection.source_reflection.macro}.")
+ def initialize(owner = nil, reflection = nil)
+ if owner && reflection
+ super("Cannot modify association '#{owner.class.name}##{reflection.name}' because the source reflection class '#{reflection.source_reflection.class_name}' is associated to '#{reflection.through_reflection.class_name}' via :#{reflection.source_reflection.macro}.")
+ else
+ super("Cannot modify association.")
+ end
end
end
@@ -74,20 +114,32 @@ module ActiveRecord
end
class HasManyThroughCantAssociateNewRecords < ActiveRecordError #:nodoc:
- def initialize(owner, reflection)
- super("Cannot associate new records through '#{owner.class.name}##{reflection.name}' on '#{reflection.source_reflection.class_name rescue nil}##{reflection.source_reflection.name rescue nil}'. Both records must have an id in order to create the has_many :through record associating them.")
+ def initialize(owner = nil, reflection = nil)
+ if owner && reflection
+ super("Cannot associate new records through '#{owner.class.name}##{reflection.name}' on '#{reflection.source_reflection.class_name rescue nil}##{reflection.source_reflection.name rescue nil}'. Both records must have an id in order to create the has_many :through record associating them.")
+ else
+ super("Cannot associate new records.")
+ end
end
end
class HasManyThroughCantDissociateNewRecords < ActiveRecordError #:nodoc:
- def initialize(owner, reflection)
- super("Cannot dissociate new records through '#{owner.class.name}##{reflection.name}' on '#{reflection.source_reflection.class_name rescue nil}##{reflection.source_reflection.name rescue nil}'. Both records must have an id in order to delete the has_many :through record associating them.")
+ def initialize(owner = nil, reflection = nil)
+ if owner && reflection
+ super("Cannot dissociate new records through '#{owner.class.name}##{reflection.name}' on '#{reflection.source_reflection.class_name rescue nil}##{reflection.source_reflection.name rescue nil}'. Both records must have an id in order to delete the has_many :through record associating them.")
+ else
+ super("Cannot dissociate new records.")
+ end
end
end
class ThroughNestedAssociationsAreReadonly < ActiveRecordError #:nodoc:
- def initialize(owner, reflection)
- super("Cannot modify association '#{owner.class.name}##{reflection.name}' because it goes through more than one other association.")
+ def initialize(owner = nil, reflection = nil)
+ if owner && reflection
+ super("Cannot modify association '#{owner.class.name}##{reflection.name}' because it goes through more than one other association.")
+ else
+ super("Through nested associations are read-only.")
+ end
end
end
@@ -98,14 +150,22 @@ module ActiveRecord
end
class EagerLoadPolymorphicError < ActiveRecordError #:nodoc:
- def initialize(reflection)
- super("Cannot eagerly load the polymorphic association #{reflection.name.inspect}")
+ def initialize(reflection = nil)
+ if reflection
+ super("Cannot eagerly load the polymorphic association #{reflection.name.inspect}")
+ else
+ super("Eager load polymorphic error.")
+ end
end
end
class ReadOnlyAssociation < ActiveRecordError #:nodoc:
- def initialize(reflection)
- super("Cannot add to a has_many :through association. Try adding to #{reflection.through_reflection.name.inspect}.")
+ def initialize(reflection = nil)
+ if reflection
+ super("Cannot add to a has_many :through association. Try adding to #{reflection.through_reflection.name.inspect}.")
+ else
+ super("Read-only reflection error.")
+ end
end
end
@@ -113,8 +173,12 @@ module ActiveRecord
# (has_many, has_one) when there is at least 1 child associated instance.
# ex: if @project.tasks.size > 0, DeleteRestrictionError will be raised when trying to destroy @project
class DeleteRestrictionError < ActiveRecordError #:nodoc:
- def initialize(name)
- super("Cannot delete record because of dependent #{name}")
+ def initialize(name = nil)
+ if name
+ super("Cannot delete record because of dependent #{name}")
+ else
+ super("Delete restriction error.")
+ end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
index a30945d0ee..7974ff3710 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
@@ -23,6 +23,11 @@ module ActiveRecord
table_name[0...table_alias_length].tr('.', '_')
end
+ # Returns an array of table names defined in the database.
+ def tables(name = nil)
+ raise NotImplementedError, "#tables is not implemented"
+ end
+
# Checks to see if the table +table_name+ exists on the database.
#
# table_exists?(:developers)
@@ -607,10 +612,7 @@ module ActiveRecord
# remove_index :accounts, name: :by_branch_party
#
def remove_index(table_name, options = {})
- remove_index!(table_name, index_name_for_remove(table_name, options))
- end
-
- def remove_index!(table_name, index_name) #:nodoc:
+ index_name = index_name_for_remove(table_name, options)
execute "DROP INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)}"
end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
index ff43c7ec42..734b384e80 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
@@ -1,6 +1,6 @@
require 'active_record/connection_adapters/abstract_mysql_adapter'
-gem 'mysql2', '~> 0.3.18'
+gem 'mysql2', '>= 0.3.18', '< 0.5'
require 'mysql2'
module ActiveRecord
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb
index 191c828e60..6155e53632 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb
@@ -36,7 +36,7 @@ module ActiveRecord
WHERE
t.typname IN (%s)
OR t.typtype IN (%s)
- OR t.typinput::varchar = 'array_in'
+ OR t.typinput = 'array_in(cstring,oid,integer)'::regprocedure
OR t.typelem != 0
SQL
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
index a3fc8fbc51..69aa02ccf4 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
@@ -101,15 +101,19 @@ module ActiveRecord
# Verifies existence of an index with a given name.
def index_name_exists?(table_name, index_name, default)
+ table = Utils.extract_schema_qualified_name(table_name.to_s)
+ index = Utils.extract_schema_qualified_name(index_name.to_s)
+
select_value(<<-SQL, 'SCHEMA').to_i > 0
SELECT COUNT(*)
FROM pg_class t
INNER JOIN pg_index d ON t.oid = d.indrelid
INNER JOIN pg_class i ON d.indexrelid = i.oid
+ LEFT JOIN pg_namespace n ON n.oid = i.relnamespace
WHERE i.relkind = 'i'
- AND i.relname = '#{index_name}'
- AND t.relname = '#{table_name}'
- AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname = ANY (current_schemas(false)) )
+ AND i.relname = '#{index.identifier}'
+ AND t.relname = '#{table.identifier}'
+ AND n.nspname = #{index.schema ? "'#{index.schema}'" : 'ANY (current_schemas(false))'}
SQL
end
@@ -447,8 +451,15 @@ module ActiveRecord
execute "CREATE #{index_type} INDEX #{index_algorithm} #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} #{index_using} (#{index_columns})#{index_options}"
end
- def remove_index!(table_name, index_name) #:nodoc:
- execute "DROP INDEX #{quote_table_name(index_name)}"
+ def remove_index(table_name, options = {}) #:nodoc:
+ index_name = index_name_for_remove(table_name, options)
+ algorithm =
+ if Hash === options && options.key?(:algorithm)
+ index_algorithms.fetch(options[:algorithm]) do
+ raise ArgumentError.new("Algorithm must be one of the following: #{index_algorithms.keys.map(&:inspect).join(', ')}")
+ end
+ end
+ execute "DROP INDEX #{algorithm} #{quote_table_name(index_name)}"
end
# Renames an index of a table. Raises error if length of new
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
index 358039723f..24fc67938d 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
@@ -375,7 +375,8 @@ module ActiveRecord
pks[0]['name']
end
- def remove_index!(table_name, index_name) #:nodoc:
+ def remove_index(table_name, options = {}) #:nodoc:
+ index_name = index_name_for_remove(table_name, options)
exec_query "DROP INDEX #{quote_column_name(index_name)}"
end
diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb
index 718f04871d..6721fe144f 100644
--- a/activerecord/lib/active_record/errors.rb
+++ b/activerecord/lib/active_record/errors.rb
@@ -63,7 +63,7 @@ module ActiveRecord
class RecordNotSaved < ActiveRecordError
attr_reader :record
- def initialize(message, record = nil)
+ def initialize(message = nil, record = nil)
@record = record
super(message)
end
@@ -80,7 +80,7 @@ module ActiveRecord
class RecordNotDestroyed < ActiveRecordError
attr_reader :record
- def initialize(message, record = nil)
+ def initialize(message = nil, record = nil)
@record = record
super(message)
end
@@ -92,9 +92,9 @@ module ActiveRecord
class StatementInvalid < ActiveRecordError
attr_reader :original_exception
- def initialize(message, original_exception = nil)
- super(message)
+ def initialize(message = nil, original_exception = nil)
@original_exception = original_exception
+ super(message)
end
end
@@ -134,10 +134,14 @@ module ActiveRecord
class StaleObjectError < ActiveRecordError
attr_reader :record, :attempted_action
- def initialize(record, attempted_action)
- super("Attempted to #{attempted_action} a stale object: #{record.class.name}")
- @record = record
- @attempted_action = attempted_action
+ def initialize(record = nil, attempted_action = nil)
+ if record && attempted_action
+ @record = record
+ @attempted_action = attempted_action
+ super("Attempted to #{attempted_action} a stale object: #{record.class.name}.")
+ else
+ super("Stale object error.")
+ end
end
end
@@ -196,7 +200,7 @@ module ActiveRecord
class AttributeAssignmentError < ActiveRecordError
attr_reader :exception, :attribute
- def initialize(message, exception, attribute)
+ def initialize(message = nil, exception = nil, attribute = nil)
super(message)
@exception = exception
@attribute = attribute
@@ -209,7 +213,7 @@ module ActiveRecord
class MultiparameterAssignmentErrors < ActiveRecordError
attr_reader :errors
- def initialize(errors)
+ def initialize(errors = nil)
@errors = errors
end
end
@@ -218,11 +222,15 @@ module ActiveRecord
class UnknownPrimaryKey < ActiveRecordError
attr_reader :model
- def initialize(model, description = nil)
- message = "Unknown primary key for table #{model.table_name} in model #{model}."
- message += "\n#{description}" if description
- super(message)
- @model = model
+ def initialize(model = nil, description = nil)
+ if model
+ message = "Unknown primary key for table #{model.table_name} in model #{model}."
+ message += "\n#{description}" if description
+ @model = model
+ super(message)
+ else
+ super("Unknown primary key.")
+ end
end
end
diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb
index e613d157aa..c26842014d 100644
--- a/activerecord/lib/active_record/inheritance.rb
+++ b/activerecord/lib/active_record/inheritance.rb
@@ -82,7 +82,7 @@ module ActiveRecord
# Returns the class descending directly from ActiveRecord::Base, or
# an abstract class, if any, in the inheritance hierarchy.
#
- # If A extends AR::Base, A.base_class will return A. If B descends from A
+ # If A extends ActiveRecord::Base, A.base_class will return A. If B descends from A
# through some arbitrarily deep hierarchy, B.base_class will return A.
#
# If B < A and C < B and if A is an abstract_class then both B.base_class
diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb
index 112d52024f..3b90ab1e31 100644
--- a/activerecord/lib/active_record/migration.rb
+++ b/activerecord/lib/active_record/migration.rb
@@ -84,35 +84,53 @@ module ActiveRecord
end
class DuplicateMigrationVersionError < MigrationError#:nodoc:
- def initialize(version)
- super("Multiple migrations have the version number #{version}")
+ def initialize(version = nil)
+ if version
+ super("Multiple migrations have the version number #{version}.")
+ else
+ super("Duplicate migration version error.")
+ end
end
end
class DuplicateMigrationNameError < MigrationError#:nodoc:
- def initialize(name)
- super("Multiple migrations have the name #{name}")
+ def initialize(name = nil)
+ if name
+ super("Multiple migrations have the name #{name}.")
+ else
+ super("Duplicate migration name.")
+ end
end
end
class UnknownMigrationVersionError < MigrationError #:nodoc:
- def initialize(version)
- super("No migration with version number #{version}")
+ def initialize(version = nil)
+ if version
+ super("No migration with version number #{version}.")
+ else
+ super("Unknown migration version.")
+ end
end
end
class IllegalMigrationNameError < MigrationError#:nodoc:
- def initialize(name)
- super("Illegal name for migration file: #{name}\n\t(only lower case letters, numbers, and '_' allowed)")
+ def initialize(name = nil)
+ if name
+ super("Illegal name for migration file: #{name}\n\t(only lower case letters, numbers, and '_' allowed).")
+ else
+ super("Illegal name for migration.")
+ end
end
end
class PendingMigrationError < MigrationError#:nodoc:
- def initialize
- if defined?(Rails.env)
- super("Migrations are pending. To resolve this issue, run:\n\n\tbin/rake db:migrate RAILS_ENV=#{::Rails.env}")
+ def initialize(message = nil)
+ if !message && defined?(Rails.env)
+ super("Migrations are pending. To resolve this issue, run:\n\n\tbin/rake db:migrate RAILS_ENV=#{::Rails.env}.")
+ elsif !message
+ super("Migrations are pending. To resolve this issue, run:\n\n\tbin/rake db:migrate.")
else
- super("Migrations are pending. To resolve this issue, run:\n\n\tbin/rake db:migrate")
+ super
end
end
end
diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb
index 5a6f42ba09..2b0c755ef4 100644
--- a/activerecord/lib/active_record/model_schema.rb
+++ b/activerecord/lib/active_record/model_schema.rb
@@ -240,7 +240,7 @@ module ActiveRecord
end
# Returns a hash where the keys are column names and the values are
- # default values when instantiating the AR object for this table.
+ # default values when instantiating the Active Record object for this table.
def column_defaults
load_schema
_default_attributes.to_hash
diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake
index 6a72d528b4..63ea305eae 100644
--- a/activerecord/lib/active_record/railties/databases.rake
+++ b/activerecord/lib/active_record/railties/databases.rake
@@ -229,7 +229,7 @@ db_namespace = namespace :db do
end
namespace :schema do
- desc 'Creates a db/schema.rb file that is portable against any DB supported by AR'
+ desc 'Creates a db/schema.rb file that is portable against any DB supported by Active Record'
task :dump => [:environment, :load_config] do
require 'active_record/schema_dumper'
filename = ENV['SCHEMA'] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, 'schema.rb')
diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb
index 5360db6a19..f8913eba06 100644
--- a/activerecord/lib/active_record/reflection.rb
+++ b/activerecord/lib/active_record/reflection.rb
@@ -32,6 +32,7 @@ module ActiveRecord
end
def self.add_reflection(ar, name, reflection)
+ ar.clear_reflections_cache
ar._reflections = ar._reflections.merge(name.to_s => reflection)
end
@@ -67,18 +68,22 @@ module ActiveRecord
#
# @api public
def reflections
- ref = {}
- _reflections.each do |name, reflection|
- parent_reflection = reflection.parent_reflection
+ @__reflections ||= begin
+ ref = {}
- if parent_reflection
- parent_name = parent_reflection.name
- ref[parent_name.to_s] = parent_reflection
- else
- ref[name] = reflection
+ _reflections.each do |name, reflection|
+ parent_reflection = reflection.parent_reflection
+
+ if parent_reflection
+ parent_name = parent_reflection.name
+ ref[parent_name.to_s] = parent_reflection
+ else
+ ref[name] = reflection
+ end
end
+
+ ref
end
- ref
end
# Returns an array of AssociationReflection objects for all the
@@ -118,6 +123,10 @@ module ActiveRecord
def reflect_on_all_autosave_associations
reflections.values.select { |reflection| reflection.options[:autosave] }
end
+
+ def clear_reflections_cache #:nodoc:
+ @__reflections = nil
+ end
end
# Holds all the methods that are shared between MacroReflection, AssociationReflection
diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb
index e47b7b1ed9..bf08cdbbf3 100644
--- a/activerecord/lib/active_record/relation.rb
+++ b/activerecord/lib/active_record/relation.rb
@@ -418,7 +418,7 @@ module ActiveRecord
end
end
- # Destroys the records matching +conditions+ by instantiating each
+ # Destroys the records by instantiating each
# record and calling its +destroy+ method. Each object's callbacks are
# executed (including <tt>:dependent</tt> association options). Returns the
# collection of objects that were destroyed; each will be frozen, to
@@ -431,20 +431,15 @@ module ActiveRecord
# rows quickly, without concern for their associations or callbacks, use
# +delete_all+ instead.
#
- # ==== Parameters
- #
- # * +conditions+ - A string, array, or hash that specifies which records
- # to destroy. If omitted, all records are destroyed. See the
- # Conditions section in the introduction to ActiveRecord::Base for
- # more information.
- #
# ==== Examples
#
- # Person.destroy_all("last_login < '2004-04-04'")
- # Person.destroy_all(status: "inactive")
# Person.where(age: 0..18).destroy_all
def destroy_all(conditions = nil)
if conditions
+ ActiveSupport::Deprecation.warn(<<-MESSAGE.squish)
+ Passing conditions to destroy_all is deprecated and will be removed in Rails 5.1.
+ To achieve the same use where(conditions).destroy_all
+ MESSAGE
where(conditions).destroy_all
else
to_a.each(&:destroy).tap { reset }
@@ -478,15 +473,13 @@ module ActiveRecord
end
end
- # Deletes the records matching +conditions+ without instantiating the records
+ # Deletes the records without instantiating the records
# first, and hence not calling the +destroy+ method nor invoking callbacks. This
# is a single SQL DELETE statement that goes straight to the database, much more
# efficient than +destroy_all+. Be careful with relations though, in particular
# <tt>:dependent</tt> rules defined on associations are not honored. Returns the
# number of rows affected.
#
- # Post.delete_all("person_id = 5 AND (category = 'Something' OR category = 'Else')")
- # Post.delete_all(["person_id = ? AND (category = ? OR category = ?)", 5, 'Something', 'Else'])
# Post.where(person_id: 5).where(category: ['Something', 'Else']).delete_all
#
# Both calls delete the affected posts all at once with a single DELETE statement.
@@ -512,6 +505,10 @@ module ActiveRecord
end
if conditions
+ ActiveSupport::Deprecation.warn(<<-MESSAGE.squish)
+ Passing conditions to delete_all is deprecated and will be removed in Rails 5.1.
+ To achieve the same use where(conditions).delete_all
+ MESSAGE
where(conditions).delete_all
else
stmt = Arel::DeleteManager.new
diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb
index d26db7d4cf..e232516b0c 100644
--- a/activerecord/lib/active_record/relation/predicate_builder.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder.rb
@@ -24,12 +24,12 @@ module ActiveRecord
end
def build_from_hash(attributes)
- attributes = convert_dot_notation_to_hash(attributes.stringify_keys)
+ attributes = convert_dot_notation_to_hash(attributes)
expand_from_hash(attributes)
end
def create_binds(attributes)
- attributes = convert_dot_notation_to_hash(attributes.stringify_keys)
+ attributes = convert_dot_notation_to_hash(attributes)
create_binds_for_hash(attributes)
end
diff --git a/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb
index 159889d3b8..e81be63cd3 100644
--- a/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb
@@ -10,10 +10,10 @@ module ActiveRecord
table = value.associated_table
if value.base_class
- queries[table.association_foreign_type] = value.base_class.name
+ queries[table.association_foreign_type.to_s] = value.base_class.name
end
- queries[table.association_foreign_key] = value.ids
+ queries[table.association_foreign_key.to_s] = value.ids
predicate_builder.build_from_hash(queries)
end
diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb
index 706c99c245..e25b889851 100644
--- a/activerecord/lib/active_record/relation/query_methods.rb
+++ b/activerecord/lib/active_record/relation/query_methods.rb
@@ -548,7 +548,7 @@ module ActiveRecord
# If the condition is any blank-ish object, then #where is a no-op and returns
# the current relation.
def where(opts = :chain, *rest)
- if opts == :chain
+ if :chain == opts
WhereChain.new(spawn)
elsif opts.blank?
self
diff --git a/activerecord/lib/active_record/relation/where_clause_factory.rb b/activerecord/lib/active_record/relation/where_clause_factory.rb
index 0430922be3..23eaab4699 100644
--- a/activerecord/lib/active_record/relation/where_clause_factory.rb
+++ b/activerecord/lib/active_record/relation/where_clause_factory.rb
@@ -15,6 +15,7 @@ module ActiveRecord
when Hash
attributes = predicate_builder.resolve_column_aliases(opts)
attributes = klass.send(:expand_hash_conditions_for_aggregates, attributes)
+ attributes.stringify_keys!
attributes, binds = predicate_builder.create_binds(attributes)
diff --git a/activerecord/lib/active_record/scoping/default.rb b/activerecord/lib/active_record/scoping/default.rb
index a1adf8e3ee..fac566e12b 100644
--- a/activerecord/lib/active_record/scoping/default.rb
+++ b/activerecord/lib/active_record/scoping/default.rb
@@ -6,8 +6,10 @@ module ActiveRecord
included do
# Stores the default scope for the class.
class_attribute :default_scopes, instance_writer: false, instance_predicate: false
+ class_attribute :default_scope_override, instance_predicate: false
self.default_scopes = []
+ self.default_scope_override = nil
end
module ClassMethods
@@ -99,12 +101,18 @@ module ActiveRecord
self.default_scopes += [scope]
end
- def build_default_scope(base_rel = relation) # :nodoc:
+ def build_default_scope(base_rel = nil) # :nodoc:
return if abstract_class?
- if !Base.is_a?(method(:default_scope).owner)
+
+ if self.default_scope_override.nil?
+ self.default_scope_override = !Base.is_a?(method(:default_scope).owner)
+ end
+
+ if self.default_scope_override
# The user has defined their own default scope method, so call that
evaluate_default_scope { default_scope }
elsif default_scopes.any?
+ base_rel ||= relation
evaluate_default_scope do
default_scopes.inject(base_rel) do |default_scope, scope|
default_scope.merge(base_rel.scoping { scope.call })
diff --git a/activerecord/lib/active_record/table_metadata.rb b/activerecord/lib/active_record/table_metadata.rb
index 41f1c55c3c..f9bb1cf5e0 100644
--- a/activerecord/lib/active_record/table_metadata.rb
+++ b/activerecord/lib/active_record/table_metadata.rb
@@ -10,13 +10,15 @@ module ActiveRecord
end
def resolve_column_aliases(hash)
- hash = hash.dup
- hash.keys.grep(Symbol) do |key|
- if klass.attribute_alias? key
- hash[klass.attribute_alias(key)] = hash.delete key
+ # This method is a hot spot, so for now, use Hash[] to dup the hash.
+ # https://bugs.ruby-lang.org/issues/7166
+ new_hash = Hash[hash]
+ hash.each do |key, _|
+ if (key.is_a?(Symbol)) && klass.attribute_alias?(key)
+ new_hash[klass.attribute_alias(key)] = new_hash.delete(key)
end
end
- hash
+ new_hash
end
def arel_attribute(column_name)
diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb
index 683741768b..0b5dc6ed33 100644
--- a/activerecord/lib/active_record/tasks/database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/database_tasks.rb
@@ -134,7 +134,7 @@ module ActiveRecord
version = ENV["VERSION"] ? ENV["VERSION"].to_i : nil
scope = ENV['SCOPE']
verbose_was, Migration.verbose = Migration.verbose, verbose
- Migrator.migrate(Migrator.migrations_paths, version) do |migration|
+ Migrator.migrate(migrations_paths, version) do |migration|
scope.blank? || scope == migration.scope
end
ensure
diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb
index 34d96b19fe..4113ca4561 100644
--- a/activerecord/lib/active_record/validations.rb
+++ b/activerecord/lib/active_record/validations.rb
@@ -12,10 +12,16 @@ module ActiveRecord
class RecordInvalid < ActiveRecordError
attr_reader :record
- def initialize(record)
- @record = record
- errors = @record.errors.full_messages.join(", ")
- super(I18n.t(:"#{@record.class.i18n_scope}.errors.messages.record_invalid", :errors => errors, :default => :"errors.messages.record_invalid"))
+ def initialize(record = nil)
+ if record
+ @record = record
+ errors = @record.errors.full_messages.join(", ")
+ message = I18n.t(:"#{@record.class.i18n_scope}.errors.messages.record_invalid", errors: errors, default: :"errors.messages.record_invalid")
+ else
+ message = "Record invalid"
+ end
+
+ super(message)
end
end
@@ -54,7 +60,7 @@ module ActiveRecord
# Validations with no <tt>:on</tt> option will run no matter the context. Validations with
# some <tt>:on</tt> option will only run in the specified context.
def valid?(context = nil)
- context ||= (new_record? ? :create : :update)
+ context ||= default_validation_context
output = super(context)
errors.empty? && output
end
@@ -63,6 +69,10 @@ module ActiveRecord
protected
+ def default_validation_context
+ new_record? ? :create : :update
+ end
+
def raise_validation_error
raise(RecordInvalid.new(self))
end
diff --git a/activerecord/test/cases/adapters/postgresql/active_schema_test.rb b/activerecord/test/cases/adapters/postgresql/active_schema_test.rb
index dc7ba314c6..24def31e36 100644
--- a/activerecord/test/cases/adapters/postgresql/active_schema_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/active_schema_test.rb
@@ -25,7 +25,7 @@ class PostgresqlActiveSchemaTest < ActiveRecord::PostgreSQLTestCase
def test_add_index
# add_index calls index_name_exists? which can't work since execute is stubbed
- ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.stubs(:index_name_exists?).returns(false)
+ ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.send(:define_method, :index_name_exists?) { |*| false }
expected = %(CREATE UNIQUE INDEX "index_people_on_last_name" ON "people" ("last_name") WHERE state = 'active')
assert_equal expected, add_index(:people, :last_name, :unique => true, :where => "state = 'active'")
@@ -49,6 +49,22 @@ class PostgresqlActiveSchemaTest < ActiveRecord::PostgreSQLTestCase
expected = %(CREATE UNIQUE INDEX "index_people_on_last_name" ON "people" USING gist ("last_name") WHERE state = 'active')
assert_equal expected, add_index(:people, :last_name, :unique => true, :where => "state = 'active'", :using => :gist)
+
+ ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.send :remove_method, :index_name_exists?
+ end
+
+ def test_remove_index
+ # remove_index calls index_name_exists? which can't work since execute is stubbed
+ ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.send(:define_method, :index_name_exists?) { |*| true }
+
+ expected = %(DROP INDEX CONCURRENTLY "index_people_on_last_name")
+ assert_equal expected, remove_index(:people, name: "index_people_on_last_name", algorithm: :concurrently)
+
+ assert_raise ArgumentError do
+ add_index(:people, :last_name, algorithm: :copy)
+ end
+
+ ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.send :remove_method, :index_name_exists?
end
private
diff --git a/activerecord/test/cases/adapters/postgresql/schema_test.rb b/activerecord/test/cases/adapters/postgresql/schema_test.rb
index ea7e5ac587..bee612d8d3 100644
--- a/activerecord/test/cases/adapters/postgresql/schema_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/schema_test.rb
@@ -321,11 +321,11 @@ class SchemaTest < ActiveRecord::PostgreSQLTestCase
def test_with_uppercase_index_name
@connection.execute "CREATE INDEX \"things_Index\" ON #{SCHEMA_NAME}.things (name)"
- assert_nothing_raised { @connection.remove_index! "things", "#{SCHEMA_NAME}.things_Index"}
+ assert_nothing_raised { @connection.remove_index "things", name: "#{SCHEMA_NAME}.things_Index"}
@connection.execute "CREATE INDEX \"things_Index\" ON #{SCHEMA_NAME}.things (name)"
with_schema_search_path SCHEMA_NAME do
- assert_nothing_raised { @connection.remove_index! "things", "things_Index"}
+ assert_nothing_raised { @connection.remove_index "things", name: "things_Index"}
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/view_test.rb b/activerecord/test/cases/adapters/postgresql/view_test.rb
deleted file mode 100644
index 2dd6ec5fe6..0000000000
--- a/activerecord/test/cases/adapters/postgresql/view_test.rb
+++ /dev/null
@@ -1,64 +0,0 @@
-require "cases/helper"
-require "cases/view_test"
-
-class UpdateableViewTest < ActiveRecord::PostgreSQLTestCase
- fixtures :books
-
- class PrintedBook < ActiveRecord::Base
- self.primary_key = "id"
- end
-
- setup do
- @connection = ActiveRecord::Base.connection
- @connection.execute <<-SQL
- CREATE VIEW printed_books
- AS SELECT id, name, status, format FROM books WHERE format = 'paperback'
- SQL
- end
-
- teardown do
- @connection.execute "DROP VIEW printed_books" if @connection.table_exists? "printed_books"
- end
-
- def test_update_record
- book = PrintedBook.first
- book.name = "AWDwR"
- book.save!
- book.reload
- assert_equal "AWDwR", book.name
- end
-
- def test_insert_record
- PrintedBook.create! name: "Rails in Action", status: 0, format: "paperback"
-
- new_book = PrintedBook.last
- assert_equal "Rails in Action", new_book.name
- end
-
- def test_update_record_to_fail_view_conditions
- book = PrintedBook.first
- book.format = "ebook"
- book.save!
-
- assert_raises ActiveRecord::RecordNotFound do
- book.reload
- end
- end
-end
-
-if ActiveRecord::Base.connection.respond_to?(:supports_materialized_views?) &&
- ActiveRecord::Base.connection.supports_materialized_views?
-class MaterializedViewTest < ActiveRecord::PostgreSQLTestCase
- include ViewBehavior
-
- private
- def create_view(name, query)
- @connection.execute "CREATE MATERIALIZED VIEW #{name} AS #{query}"
- end
-
- def drop_view(name)
- @connection.execute "DROP MATERIALIZED VIEW #{name}" if @connection.table_exists? name
-
- end
-end
-end
diff --git a/activerecord/test/cases/errors_test.rb b/activerecord/test/cases/errors_test.rb
new file mode 100644
index 0000000000..0711a372f2
--- /dev/null
+++ b/activerecord/test/cases/errors_test.rb
@@ -0,0 +1,16 @@
+require_relative "../cases/helper"
+
+class ErrorsTest < ActiveRecord::TestCase
+ def test_can_be_instantiated_with_no_args
+ base = ActiveRecord::ActiveRecordError
+ error_klasses = ObjectSpace.each_object(Class).select { |klass| klass < base }
+
+ error_klasses.each do |error_klass|
+ begin
+ error_klass.new.inspect
+ rescue ArgumentError
+ raise "Instance of #{error_klass} can't be initialized with no arguments"
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb
index cdf63957f4..7f14082a9a 100644
--- a/activerecord/test/cases/persistence_test.rb
+++ b/activerecord/test/cases/persistence_test.rb
@@ -126,7 +126,7 @@ class PersistenceTest < ActiveRecord::TestCase
assert ! topics_by_mary.empty?
assert_difference('Topic.count', -topics_by_mary.size) do
- destroyed = Topic.destroy_all(conditions).sort_by(&:id)
+ destroyed = Topic.where(conditions).destroy_all.sort_by(&:id)
assert_equal topics_by_mary, destroyed
assert destroyed.all?(&:frozen?), "destroyed topics should be frozen"
end
diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb
index 5f48c2b40f..8256762f96 100644
--- a/activerecord/test/cases/relations_test.rb
+++ b/activerecord/test/cases/relations_test.rb
@@ -931,6 +931,12 @@ class RelationTest < ActiveRecord::TestCase
assert davids.loaded?
end
+ def test_destroy_all_with_conditions_is_deprecated
+ assert_deprecated do
+ assert_difference('Author.count', -1) { Author.destroy_all(name: 'David') }
+ end
+ end
+
def test_delete_all
davids = Author.where(:name => 'David')
@@ -938,6 +944,12 @@ class RelationTest < ActiveRecord::TestCase
assert ! davids.loaded?
end
+ def test_delete_all_with_conditions_is_deprecated
+ assert_deprecated do
+ assert_difference('Author.count', -1) { Author.delete_all(name: 'David') }
+ end
+ end
+
def test_delete_all_loaded
davids = Author.where(:name => 'David')
diff --git a/activerecord/test/cases/tasks/database_tasks_test.rb b/activerecord/test/cases/tasks/database_tasks_test.rb
index 38164b2228..c8f4179313 100644
--- a/activerecord/test/cases/tasks/database_tasks_test.rb
+++ b/activerecord/test/cases/tasks/database_tasks_test.rb
@@ -277,12 +277,14 @@ module ActiveRecord
def test_migrate_receives_correct_env_vars
verbose, version = ENV['VERBOSE'], ENV['VERSION']
+ ActiveRecord::Tasks::DatabaseTasks.migrations_paths = 'custom/path'
ENV['VERBOSE'] = 'false'
ENV['VERSION'] = '4'
- ActiveRecord::Migrator.expects(:migrate).with(ActiveRecord::Migrator.migrations_paths, 4)
+ ActiveRecord::Migrator.expects(:migrate).with('custom/path', 4)
ActiveRecord::Tasks::DatabaseTasks.migrate
ensure
+ ActiveRecord::Tasks::DatabaseTasks.migrations_paths = nil
ENV['VERBOSE'], ENV['VERSION'] = verbose, version
end
end
diff --git a/activerecord/test/cases/validations_test.rb b/activerecord/test/cases/validations_test.rb
index f4f316f393..a429d06aad 100644
--- a/activerecord/test/cases/validations_test.rb
+++ b/activerecord/test/cases/validations_test.rb
@@ -52,6 +52,13 @@ class ValidationsTest < ActiveRecord::TestCase
assert r.valid?(:special_case)
end
+ def test_invalid_using_multiple_contexts
+ r = WrongReply.new(:title => 'Wrong Create')
+ assert r.invalid?([:special_case, :create])
+ assert_equal "Invalid", r.errors[:author_name].join
+ assert_equal "is Wrong Create", r.errors[:title].join
+ end
+
def test_validate
r = WrongReply.new
diff --git a/activerecord/test/cases/view_test.rb b/activerecord/test/cases/view_test.rb
index f9dca1e196..1eb1430065 100644
--- a/activerecord/test/cases/view_test.rb
+++ b/activerecord/test/cases/view_test.rb
@@ -110,4 +110,70 @@ class ViewWithoutPrimaryKeyTest < ActiveRecord::TestCase
assert_nil Paperback.primary_key
end
end
+
+# sqlite dose not support CREATE, INSERT, and DELETE for VIEW
+if current_adapter?(:MysqlAdapter, :Mysql2Adapter, :PostgreSQLAdapter)
+class UpdateableViewTest < ActiveRecord::TestCase
+ self.use_transactional_tests = false
+ fixtures :books
+
+ class PrintedBook < ActiveRecord::Base
+ self.primary_key = "id"
+ end
+
+ setup do
+ @connection = ActiveRecord::Base.connection
+ @connection.execute <<-SQL
+ CREATE VIEW printed_books
+ AS SELECT id, name, status, format FROM books WHERE format = 'paperback'
+ SQL
+ end
+
+ teardown do
+ @connection.execute "DROP VIEW printed_books" if @connection.table_exists? "printed_books"
+ end
+
+ def test_update_record
+ book = PrintedBook.first
+ book.name = "AWDwR"
+ book.save!
+ book.reload
+ assert_equal "AWDwR", book.name
+ end
+
+ def test_insert_record
+ PrintedBook.create! name: "Rails in Action", status: 0, format: "paperback"
+
+ new_book = PrintedBook.last
+ assert_equal "Rails in Action", new_book.name
+ end
+
+ def test_update_record_to_fail_view_conditions
+ book = PrintedBook.first
+ book.format = "ebook"
+ book.save!
+
+ assert_raises ActiveRecord::RecordNotFound do
+ book.reload
+ end
+ end
+end
+end # end fo `if current_adapter?(:MysqlAdapter, :Mysql2Adapter, :PostgreSQLAdapter)`
+end # end fo `if ActiveRecord::Base.connection.supports_views?`
+
+if ActiveRecord::Base.connection.respond_to?(:supports_materialized_views?) &&
+ ActiveRecord::Base.connection.supports_materialized_views?
+class MaterializedViewTest < ActiveRecord::PostgreSQLTestCase
+ include ViewBehavior
+
+ private
+ def create_view(name, query)
+ @connection.execute "CREATE MATERIALIZED VIEW #{name} AS #{query}"
+ end
+
+ def drop_view(name)
+ @connection.execute "DROP MATERIALIZED VIEW #{name}" if @connection.table_exists? name
+
+ end
+end
end
diff --git a/activerecord/test/models/face.rb b/activerecord/test/models/face.rb
index 91e46f83e5..af76fea52c 100644
--- a/activerecord/test/models/face.rb
+++ b/activerecord/test/models/face.rb
@@ -1,7 +1,7 @@
class Face < ActiveRecord::Base
belongs_to :man, :inverse_of => :face
belongs_to :polymorphic_man, :polymorphic => true, :inverse_of => :polymorphic_face
- # Oracle identifier lengh is limited to 30 bytes or less, `polymorphic` renamed `poly`
+ # Oracle identifier length is limited to 30 bytes or less, `polymorphic` renamed `poly`
belongs_to :poly_man_without_inverse, :polymorphic => true
# These is a "broken" inverse_of for the purposes of testing
belongs_to :horrible_man, :class_name => 'Man', :inverse_of => :horrible_face
diff --git a/activerecord/test/models/topic.rb b/activerecord/test/models/topic.rb
index d17270021a..176bc79dc7 100644
--- a/activerecord/test/models/topic.rb
+++ b/activerecord/test/models/topic.rb
@@ -86,7 +86,7 @@ class Topic < ActiveRecord::Base
end
def destroy_children
- self.class.delete_all "parent_id = #{id}"
+ self.class.where("parent_id = #{id}").delete_all
end
def set_email_address
diff --git a/activesupport/lib/active_support/core_ext/file/atomic.rb b/activesupport/lib/active_support/core_ext/file/atomic.rb
index ab635f6db8..948d647875 100644
--- a/activesupport/lib/active_support/core_ext/file/atomic.rb
+++ b/activesupport/lib/active_support/core_ext/file/atomic.rb
@@ -11,7 +11,7 @@ class File
# This method needs to create a temporary file. By default it will create it
# in the same directory as the destination file. If you don't like this
# behaviour you can provide a different directory but it must be on the
- # same physical filesystem as the the file you're trying to write.
+ # same physical filesystem as the file you're trying to write.
#
# File.atomic_write('/data/something.important', '/data/tmp') do |file|
# file.write('hello')
diff --git a/activesupport/lib/active_support/core_ext/string/conversions.rb b/activesupport/lib/active_support/core_ext/string/conversions.rb
index 3e0cb8a7ac..fd79a40e31 100644
--- a/activesupport/lib/active_support/core_ext/string/conversions.rb
+++ b/activesupport/lib/active_support/core_ext/string/conversions.rb
@@ -14,7 +14,7 @@ class String
# "06:12".to_time # => 2012-12-13 06:12:00 +0100
# "2012-12-13 06:12".to_time # => 2012-12-13 06:12:00 +0100
# "2012-12-13T06:12".to_time # => 2012-12-13 06:12:00 +0100
- # "2012-12-13T06:12".to_time(:utc) # => 2012-12-13 05:12:00 UTC
+ # "2012-12-13T06:12".to_time(:utc) # => 2012-12-13 06:12:00 UTC
# "12/13/2012".to_time # => ArgumentError: argument out of range
def to_time(form = :local)
parts = Date._parse(self, false)
diff --git a/activesupport/lib/active_support/core_ext/time/calculations.rb b/activesupport/lib/active_support/core_ext/time/calculations.rb
index 96156deebb..f13d09f3ad 100644
--- a/activesupport/lib/active_support/core_ext/time/calculations.rb
+++ b/activesupport/lib/active_support/core_ext/time/calculations.rb
@@ -3,6 +3,7 @@ require 'active_support/core_ext/time/conversions'
require 'active_support/time_with_zone'
require 'active_support/core_ext/time/zones'
require 'active_support/core_ext/date_and_time/calculations'
+require 'active_support/core_ext/date/calculations'
class Time
include DateAndTime::Calculations
diff --git a/activesupport/test/constantize_test_cases.rb b/activesupport/test/constantize_test_cases.rb
index 366e4e5ef0..1115bc0fd8 100644
--- a/activesupport/test/constantize_test_cases.rb
+++ b/activesupport/test/constantize_test_cases.rb
@@ -100,6 +100,10 @@ module ConstantizeTestCases
assert_nil yield("Ace::Gas::ConstantizeTestCases")
assert_nil yield("#<Class:0x7b8b718b>::Nested_1")
assert_nil yield("Ace::gas")
+ assert_nil yield('Object::ABC')
+ assert_nil yield('Object::Object::Object::ABC')
+ assert_nil yield('A::Object::B')
+ assert_nil yield('A::Object::Object::Object::B')
assert_raises(NameError) do
with_autoloading_fixtures do
diff --git a/guides/source/3_2_release_notes.md b/guides/source/3_2_release_notes.md
index c52c39b705..f6871c186e 100644
--- a/guides/source/3_2_release_notes.md
+++ b/guides/source/3_2_release_notes.md
@@ -327,7 +327,7 @@ Active Record
* Implemented `ActiveRecord::Relation#explain`.
-* Implements `AR::Base.silence_auto_explain` which allows the user to selectively disable automatic EXPLAINs within a block.
+* Implements `ActiveRecord::Base.silence_auto_explain` which allows the user to selectively disable automatic EXPLAINs within a block.
* Implements automatic EXPLAIN logging for slow queries. A new configuration parameter `config.active_record.auto_explain_threshold_in_seconds` determines what's to be considered a slow query. Setting that to nil disables this feature. Defaults are 0.5 in development mode, and nil in test and production modes. Rails 3.2 supports this feature in SQLite, MySQL (mysql2 adapter), and PostgreSQL.
diff --git a/guides/source/configuring.md b/guides/source/configuring.md
index d63317433d..5e72b96787 100644
--- a/guides/source/configuring.md
+++ b/guides/source/configuring.md
@@ -1096,7 +1096,7 @@ you and wait for a connection from the pool. If it cannot get a connection, a
timeout error similar to that given below will be thrown.
```ruby
-ActiveRecord::ConnectionTimeoutError - could not obtain a database connection within 5 seconds. The max pool size is currently 5; consider increasing it:
+ActiveRecord::ConnectionTimeoutError - could not obtain a database connection within 5.000 seconds (waited 5.000 seconds)
```
If you get the above error, you might want to increase the size of the
@@ -1108,7 +1108,7 @@ NOTE. If you are running in a multi-threaded environment, there could be a chanc
Custom configuration
--------------------
-You can configure your own code through the Rails configuration object with custom configuration. It works like this:
+You can configure your own code through the Rails configuration object with custom configuration under the `config.x` property. It works like this:
```ruby
config.x.payment_processing.schedule = :daily
diff --git a/guides/source/upgrading_ruby_on_rails.md b/guides/source/upgrading_ruby_on_rails.md
index 743241d7a0..30c0fcb294 100644
--- a/guides/source/upgrading_ruby_on_rails.md
+++ b/guides/source/upgrading_ruby_on_rails.md
@@ -895,7 +895,7 @@ CatalogProduct < ActiveRecord::Base
end
```
-* Note that the the prefix takes scopes into account as well, so relations between `Catalog::Category` and `Catalog::Product` or `Catalog::Category` and `CatalogProduct` need to be updated similarly.
+* Note that the prefix takes scopes into account as well, so relations between `Catalog::Category` and `Catalog::Product` or `Catalog::Category` and `CatalogProduct` need to be updated similarly.
### Active Resource
diff --git a/railties/lib/rails/generators.rb b/railties/lib/rails/generators.rb
index b430cf1909..2645102619 100644
--- a/railties/lib/rails/generators.rb
+++ b/railties/lib/rails/generators.rb
@@ -178,7 +178,7 @@ module Rails
options = sorted_groups.flat_map(&:last)
suggestions = options.sort_by {|suggested| levenshtein_distance(namespace.to_s, suggested) }.first(3)
msg = "Could not find generator '#{namespace}'. "
- msg << "Maybe you meant #{ suggestions.map {|s| "'#{s}'"}.to_sentence(last_word_connector: " or ") }\n"
+ msg << "Maybe you meant #{ suggestions.map {|s| "'#{s}'"}.to_sentence(last_word_connector: " or ", locale: :en) }\n"
msg << "Run `rails generate --help` for more options."
puts msg
end
diff --git a/railties/lib/rails/tasks/engine.rake b/railties/lib/rails/tasks/engine.rake
index 16ad1bfc84..c51524f8f6 100644
--- a/railties/lib/rails/tasks/engine.rake
+++ b/railties/lib/rails/tasks/engine.rake
@@ -40,7 +40,7 @@ namespace :db do
desc "Rolls the schema back to the previous version (specify steps w/ STEP=n)."
app_task "rollback"
- desc "Create a db/schema.rb file that can be portably used against any DB supported by AR"
+ desc "Create a db/schema.rb file that can be portably used against any DB supported by Active Record"
app_task "schema:dump"
desc "Load a schema.rb file into the database"
diff --git a/railties/lib/rails/test_unit/minitest_plugin.rb b/railties/lib/rails/test_unit/minitest_plugin.rb
index 8e5301d1e0..dacab08ec3 100644
--- a/railties/lib/rails/test_unit/minitest_plugin.rb
+++ b/railties/lib/rails/test_unit/minitest_plugin.rb
@@ -29,12 +29,21 @@ module Minitest
options[:patterns] = opts.order!
end
+ # Running several Rake tasks in a single command would trip up the runner,
+ # as the patterns would also contain the other Rake tasks.
+ def self.rake_run(patterns) # :nodoc:
+ @rake_patterns = patterns
+ run
+ end
+
def self.plugin_rails_init(options)
self.run_with_rails_extension = true
ENV["RAILS_ENV"] = options[:environment] || "test"
- ::Rails::TestRequirer.require_files options[:patterns] unless run_with_autorun
+ unless run_with_autorun
+ ::Rails::TestRequirer.require_files @rake_patterns || options[:patterns]
+ end
unless options[:full_backtrace] || ENV["BACKTRACE"]
# Plugin can run without Rails loaded, check before filtering.
diff --git a/railties/lib/rails/test_unit/test_requirer.rb b/railties/lib/rails/test_unit/test_requirer.rb
index 84c2256729..83d2c55ffd 100644
--- a/railties/lib/rails/test_unit/test_requirer.rb
+++ b/railties/lib/rails/test_unit/test_requirer.rb
@@ -18,7 +18,7 @@ module Rails
arg = arg.gsub(/:(\d+)?$/, '')
if Dir.exist?(arg)
"#{arg}/**/*_test.rb"
- elsif File.file?(arg)
+ else
arg
end
end
diff --git a/railties/lib/rails/test_unit/testing.rake b/railties/lib/rails/test_unit/testing.rake
index dda492f974..6676c6a079 100644
--- a/railties/lib/rails/test_unit/testing.rake
+++ b/railties/lib/rails/test_unit/testing.rake
@@ -7,7 +7,7 @@ task default: :test
desc "Runs all tests in test folder"
task :test do
$: << "test"
- Minitest.run(['test'])
+ Minitest.rake_run(["test"])
end
namespace :test do
@@ -24,22 +24,22 @@ namespace :test do
["models", "helpers", "controllers", "mailers", "integration", "jobs"].each do |name|
task name => "test:prepare" do
$: << "test"
- Minitest.run(["test/#{name}"])
+ Minitest.rake_run(["test/#{name}"])
end
end
task :generators => "test:prepare" do
$: << "test"
- Minitest.run(["test/lib/generators"])
+ Minitest.rake_run(["test/lib/generators"])
end
task :units => "test:prepare" do
$: << "test"
- Minitest.run(["test/models", "test/helpers", "test/unit"])
+ Minitest.rake_run(["test/models", "test/helpers", "test/unit"])
end
task :functionals => "test:prepare" do
$: << "test"
- Minitest.run(["test/controllers", "test/mailers", "test/functional"])
+ Minitest.rake_run(["test/controllers", "test/mailers", "test/functional"])
end
end
diff --git a/railties/test/application/test_runner_test.rb b/railties/test/application/test_runner_test.rb
index 494e6dd7bd..2d47a31826 100644
--- a/railties/test/application/test_runner_test.rb
+++ b/railties/test/application/test_runner_test.rb
@@ -340,6 +340,11 @@ module ApplicationTests
assert_match '0 runs, 0 assertions', run_test_command('')
end
+ def test_raise_error_when_specified_file_does_not_exist
+ error = capture(:stderr) { run_test_command('test/not_exists.rb') }
+ assert_match(%r{cannot load such file.+test/not_exists\.rb}, error)
+ end
+
private
def run_test_command(arguments = 'test/unit/test_test.rb')
Dir.chdir(app_path) { `bin/rails t #{arguments}` }