aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--actionpack/lib/action_controller/metal/data_streaming.rb2
-rw-r--r--actionpack/lib/action_dispatch/http/content_security_policy.rb4
-rw-r--r--actionpack/lib/action_dispatch/railtie.rb6
-rw-r--r--actionpack/lib/action_dispatch/system_test_case.rb19
-rw-r--r--actionpack/test/dispatch/content_security_policy_test.rb24
-rw-r--r--actionview/lib/action_view/helpers/asset_url_helper.rb2
-rw-r--r--actionview/lib/action_view/helpers/url_helper.rb56
-rw-r--r--actionview/lib/action_view/unbound_template.rb6
-rw-r--r--actionview/test/template/url_helper_test.rb62
-rw-r--r--activemodel/lib/active_model/errors.rb12
-rw-r--r--activemodel/test/cases/errors_test.rb19
-rw-r--r--activerecord/CHANGELOG.md4
-rw-r--r--activerecord/lib/active_record/autosave_association.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb4
-rw-r--r--activerecord/lib/active_record/railties/databases.rake15
-rw-r--r--activerecord/lib/active_record/relation/query_methods.rb2
-rw-r--r--activerecord/test/cases/adapters/postgresql/money_test.rb4
-rw-r--r--activerecord/test/cases/autosave_association_test.rb16
-rw-r--r--activerecord/test/cases/relation_test.rb7
-rw-r--r--activerecord/test/models/mouse.rb6
-rw-r--r--activerecord/test/models/squeak.rb6
-rw-r--r--activerecord/test/schema/schema.rb8
-rw-r--r--activestorage/CHANGELOG.md4
-rw-r--r--activestorage/config/routes.rb2
-rw-r--r--activestorage/lib/active_storage.rb19
-rw-r--r--activestorage/lib/active_storage/attached/model.rb17
-rw-r--r--activestorage/lib/active_storage/engine.rb3
-rw-r--r--activestorage/lib/active_storage/service/disk_service.rb6
-rw-r--r--activestorage/test/analyzer/video_analyzer_test.rb2
-rw-r--r--activestorage/test/models/attached/many_test.rb26
-rw-r--r--activestorage/test/service/disk_service_test.rb10
-rw-r--r--activesupport/lib/active_support/core_ext/module/delegation.rb6
-rw-r--r--activesupport/lib/active_support/inflector/transliterate.rb9
-rw-r--r--activesupport/lib/active_support/parameter_filter.rb7
-rw-r--r--activesupport/lib/active_support/secure_compare_rotator.rb52
-rw-r--r--activesupport/test/core_ext/module_test.rb29
-rw-r--r--activesupport/test/parameter_filter_test.rb7
-rw-r--r--activesupport/test/secure_compare_rotator_test.rb44
-rw-r--r--activesupport/test/transliterate_test.rb8
-rw-r--r--guides/source/5_2_release_notes.md2
-rw-r--r--guides/source/6_0_release_notes.md10
-rw-r--r--guides/source/configuring.md9
-rw-r--r--guides/source/upgrading_ruby_on_rails.md48
-rw-r--r--railties/lib/rails/application/configuration.rb2
-rw-r--r--railties/lib/rails/generators/actions.rb26
-rw-r--r--railties/lib/rails/generators/base.rb7
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/initializers/filter_parameter_logging.rb.tt4
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_6_0.rb.tt4
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/puma.rb.tt3
-rw-r--r--railties/lib/rails/source_annotation_extractor.rb10
-rw-r--r--railties/test/application/configuration_test.rb15
-rw-r--r--railties/test/application/generators_test.rb10
-rw-r--r--railties/test/application/rake/dbs_test.rb16
-rw-r--r--railties/test/application/system_test_case_test.rb45
-rw-r--r--railties/test/generators/actions_test.rb92
55 files changed, 750 insertions, 90 deletions
diff --git a/actionpack/lib/action_controller/metal/data_streaming.rb b/actionpack/lib/action_controller/metal/data_streaming.rb
index 9ef4f50df1..879745a895 100644
--- a/actionpack/lib/action_controller/metal/data_streaming.rb
+++ b/actionpack/lib/action_controller/metal/data_streaming.rb
@@ -53,7 +53,7 @@ module ActionController #:nodoc:
#
# Show a 404 page in the browser:
#
- # send_file '/path/to/404.html', type: 'text/html; charset=utf-8', status: 404
+ # send_file '/path/to/404.html', type: 'text/html; charset=utf-8', disposition: 'inline', status: 404
#
# Read about the other Content-* HTTP headers if you'd like to
# provide the user with more information (such as Content-Description) in
diff --git a/actionpack/lib/action_dispatch/http/content_security_policy.rb b/actionpack/lib/action_dispatch/http/content_security_policy.rb
index 7dedecef34..9c430b57e3 100644
--- a/actionpack/lib/action_dispatch/http/content_security_policy.rb
+++ b/actionpack/lib/action_dispatch/http/content_security_policy.rb
@@ -137,7 +137,11 @@ module ActionDispatch #:nodoc:
object_src: "object-src",
prefetch_src: "prefetch-src",
script_src: "script-src",
+ script_src_attr: "script-src-attr",
+ script_src_elem: "script-src-elem",
style_src: "style-src",
+ style_src_attr: "style-src-attr",
+ style_src_elem: "style-src-elem",
worker_src: "worker-src"
}.freeze
diff --git a/actionpack/lib/action_dispatch/railtie.rb b/actionpack/lib/action_dispatch/railtie.rb
index 66f90980b9..2e09aed41d 100644
--- a/actionpack/lib/action_dispatch/railtie.rb
+++ b/actionpack/lib/action_dispatch/railtie.rb
@@ -54,11 +54,5 @@ module ActionDispatch
ActionDispatch.test_app = app
end
-
- initializer "action_dispatch.system_tests" do |app|
- ActiveSupport.on_load(:action_dispatch_system_test_case) do
- include app.routes.url_helpers
- end
- end
end
end
diff --git a/actionpack/lib/action_dispatch/system_test_case.rb b/actionpack/lib/action_dispatch/system_test_case.rb
index 29864c0f8e..4fda2cf44f 100644
--- a/actionpack/lib/action_dispatch/system_test_case.rb
+++ b/actionpack/lib/action_dispatch/system_test_case.rb
@@ -119,6 +119,17 @@ module ActionDispatch
def initialize(*) # :nodoc:
super
self.class.driver.use
+ @proxy_route = if ActionDispatch.test_app
+ Class.new do
+ include ActionDispatch.test_app.routes.url_helpers
+
+ def url_options
+ default_url_options.merge(host: Capybara.app_host)
+ end
+ end.new
+ else
+ nil
+ end
end
def self.start_application # :nodoc:
@@ -159,8 +170,12 @@ module ActionDispatch
driven_by :selenium
- def url_options # :nodoc:
- default_url_options.merge(host: Capybara.app_host)
+ def method_missing(method, *args, &block)
+ if @proxy_route.respond_to?(method)
+ @proxy_route.send(method, *args, &block)
+ else
+ super
+ end
end
ActiveSupport.run_load_hooks(:action_dispatch_system_test_case, self)
diff --git a/actionpack/test/dispatch/content_security_policy_test.rb b/actionpack/test/dispatch/content_security_policy_test.rb
index a4634626bb..3d60dc1661 100644
--- a/actionpack/test/dispatch/content_security_policy_test.rb
+++ b/actionpack/test/dispatch/content_security_policy_test.rb
@@ -128,12 +128,36 @@ class ContentSecurityPolicyTest < ActiveSupport::TestCase
@policy.script_src false
assert_no_match %r{script-src}, @policy.build
+ @policy.script_src_attr :self
+ assert_match %r{script-src-attr 'self'}, @policy.build
+
+ @policy.script_src_attr false
+ assert_no_match %r{script-src-attr}, @policy.build
+
+ @policy.script_src_elem :self
+ assert_match %r{script-src-elem 'self'}, @policy.build
+
+ @policy.script_src_elem false
+ assert_no_match %r{script-src-elem}, @policy.build
+
@policy.style_src :self
assert_match %r{style-src 'self'}, @policy.build
@policy.style_src false
assert_no_match %r{style-src}, @policy.build
+ @policy.style_src_attr :self
+ assert_match %r{style-src-attr 'self'}, @policy.build
+
+ @policy.style_src_attr false
+ assert_no_match %r{style-src-attr}, @policy.build
+
+ @policy.style_src_elem :self
+ assert_match %r{style-src-elem 'self'}, @policy.build
+
+ @policy.style_src_elem false
+ assert_no_match %r{style-src-elem}, @policy.build
+
@policy.worker_src :self
assert_match %r{worker-src 'self'}, @policy.build
diff --git a/actionview/lib/action_view/helpers/asset_url_helper.rb b/actionview/lib/action_view/helpers/asset_url_helper.rb
index cc62783d60..295f945325 100644
--- a/actionview/lib/action_view/helpers/asset_url_helper.rb
+++ b/actionview/lib/action_view/helpers/asset_url_helper.rb
@@ -133,6 +133,8 @@ module ActionView
# which is implemented by sprockets-rails.
#
# asset_path("application.js") # => "/assets/application-60aa4fdc5cea14baf5400fba1abf4f2a46a5166bad4772b1effe341570f07de9.js"
+ # asset_path('application.js', host: 'example.com') # => "//example.com/assets/application.js"
+ # asset_path("application.js", host: 'example.com', protocol: 'https') # => "https://example.com/assets/application.js"
#
# === Without the asset pipeline (<tt>skip_pipeline: true</tt>)
#
diff --git a/actionview/lib/action_view/helpers/url_helper.rb b/actionview/lib/action_view/helpers/url_helper.rb
index 4b3a258287..85fd549177 100644
--- a/actionview/lib/action_view/helpers/url_helper.rb
+++ b/actionview/lib/action_view/helpers/url_helper.rb
@@ -60,8 +60,8 @@ module ActionView
# Creates an anchor element of the given +name+ using a URL created by the set of +options+.
# See the valid options in the documentation for +url_for+. It's also possible to
- # pass a String instead of an options hash, which generates an anchor element that uses the
- # value of the String as the href for the link. Using a <tt>:back</tt> Symbol instead
+ # pass a \String instead of an options hash, which generates an anchor element that uses the
+ # value of the \String as the href for the link. Using a <tt>:back</tt> \Symbol instead
# of an options hash will generate a link to the referrer (a JavaScript back link
# will be used in place of a referrer if none exists). If +nil+ is passed as the name
# the value of the link itself will become the name.
@@ -226,7 +226,7 @@ module ActionView
# The +options+ hash accepts the same options as +url_for+.
#
# There are a few special +html_options+:
- # * <tt>:method</tt> - Symbol of HTTP verb. Supported verbs are <tt>:post</tt>, <tt>:get</tt>,
+ # * <tt>:method</tt> - \Symbol of HTTP verb. Supported verbs are <tt>:post</tt>, <tt>:get</tt>,
# <tt>:delete</tt>, <tt>:patch</tt>, and <tt>:put</tt>. By default it will be <tt>:post</tt>.
# * <tt>:disabled</tt> - If set to true, it will generate a disabled button.
# * <tt>:data</tt> - This option can be used to add custom data attributes.
@@ -235,7 +235,7 @@ module ActionView
# * <tt>:form</tt> - This hash will be form attributes
# * <tt>:form_class</tt> - This controls the class of the form within which the submit button will
# be placed
- # * <tt>:params</tt> - Hash of parameters to be rendered as hidden fields within the form.
+ # * <tt>:params</tt> - \Hash of parameters to be rendered as hidden fields within the form.
#
# ==== Data attributes
#
@@ -571,6 +571,54 @@ module ActionView
end
end
+ # Creates an SMS anchor link tag to the specified +phone_number+, which is
+ # also used as the name of the link unless +name+ is specified. Additional
+ # HTML attributes for the link can be passed in +html_options+.
+ #
+ # When clicked, an SMS message is prepopulated with the passed phone number
+ # and optional +body+ value.
+ #
+ # +sms_to+ has a +body+ option for customizing the SMS message itself by
+ # passing special keys to +html_options+.
+ #
+ # ==== Options
+ # * <tt>:body</tt> - Preset the body of the message.
+ #
+ # ==== Examples
+ # sms_to "5155555785"
+ # # => <a href="sms:5155555785;">5155555785</a>
+ #
+ # sms_to "5155555785", "Text me"
+ # # => <a href="sms:5155555785;">Text me</a>
+ #
+ # sms_to "5155555785", "Text me",
+ # body: "Hello Jim I have a question about your product."
+ # # => <a href="sms:5155555785;?body=Hello%20Jim%20I%20have%20a%20question%20about%20your%20product">Text me</a>
+ #
+ # You can use a block as well if your link target is hard to fit into the name parameter. \ERB example:
+ #
+ # <%= sms_to "5155555785" do %>
+ # <strong>Text me:</strong>
+ # <% end %>
+ # # => <a href="sms:5155555785;">
+ # <strong>Text me:</strong>
+ # </a>
+ def sms_to(phone_number, name = nil, html_options = {}, &block)
+ html_options, name = name, nil if block_given?
+ html_options = (html_options || {}).stringify_keys
+
+ extras = %w{ body }.map! { |item|
+ option = html_options.delete(item).presence || next
+ "#{item.dasherize}=#{ERB::Util.url_encode(option)}"
+ }.compact
+ extras = extras.empty? ? "" : "?&" + extras.join("&")
+
+ encoded_phone_number = ERB::Util.url_encode(phone_number)
+ html_options["href"] = "sms:#{encoded_phone_number};#{extras}"
+
+ content_tag("a", name || phone_number, html_options, &block)
+ end
+
private
def convert_options_to_data_attributes(options, html_options)
if html_options
diff --git a/actionview/lib/action_view/unbound_template.rb b/actionview/lib/action_view/unbound_template.rb
index d28bab91b6..3d4434b2e9 100644
--- a/actionview/lib/action_view/unbound_template.rb
+++ b/actionview/lib/action_view/unbound_template.rb
@@ -4,9 +4,9 @@ require "concurrent/map"
module ActionView
class UnboundTemplate
- def initialize(source, identifer, handler, options)
+ def initialize(source, identifier, handler, options)
@source = source
- @identifer = identifer
+ @identifier = identifier
@handler = handler
@options = options
@@ -22,7 +22,7 @@ module ActionView
options = @options.merge(locals: locals)
Template.new(
@source,
- @identifer,
+ @identifier,
@handler,
options
)
diff --git a/actionview/test/template/url_helper_test.rb b/actionview/test/template/url_helper_test.rb
index 632b32f09f..bce6e7f370 100644
--- a/actionview/test/template/url_helper_test.rb
+++ b/actionview/test/template/url_helper_test.rb
@@ -708,6 +708,68 @@ class UrlHelperTest < ActiveSupport::TestCase
assert_equal({ class: "special" }, options)
end
+ def test_sms_to
+ assert_dom_equal %{<a href="sms:15155555785;">15155555785</a>}, sms_to("15155555785")
+ assert_dom_equal %{<a href="sms:15155555785;">Jim Jones</a>}, sms_to("15155555785", "Jim Jones")
+ assert_dom_equal(
+ %{<a class="admin" href="sms:15155555785;">Jim Jones</a>},
+ sms_to("15155555785", "Jim Jones", "class" => "admin")
+ )
+ assert_equal sms_to("15155555785", "Jim Jones", "class" => "admin"),
+ sms_to("15155555785", "Jim Jones", class: "admin")
+ end
+
+ def test_sms_to_with_options
+ assert_dom_equal(
+ %{<a class="simple-class" href="sms:15155555785;?&body=Hello%20from%20Jim">Text me</a>},
+ sms_to("15155555785", "Text me", class: "simple-class", body: "Hello from Jim")
+ )
+
+ assert_dom_equal(
+ %{<a href="sms:15155555785;?&body=This%20is%20the%20body%20of%20the%20message.">Text me</a>},
+ sms_to("15155555785", "Text me", body: "This is the body of the message.")
+ )
+ end
+
+ def test_sms_with_img
+ assert_dom_equal %{<a href="sms:15155555785;"><img src="/feedback.png" /></a>},
+ sms_to("15155555785", raw('<img src="/feedback.png" />'))
+ end
+
+ def test_sms_with_html_safe_string
+ assert_dom_equal(
+ %{<a href="sms:1%2B5155555785;">1+5155555785</a>},
+ sms_to(raw("1+5155555785"))
+ )
+ end
+
+ def test_sms_with_nil
+ assert_dom_equal(
+ %{<a href="sms:;"></a>},
+ sms_to(nil)
+ )
+ end
+
+ def test_sms_returns_html_safe_string
+ assert_predicate sms_to("15155555785"), :html_safe?
+ end
+
+ def test_sms_with_block
+ assert_dom_equal %{<a href="sms:15155555785;"><span>Text me</span></a>},
+ sms_to("15155555785") { content_tag(:span, "Text me") }
+ end
+
+ def test_sms_with_block_and_options
+ assert_dom_equal %{<a class="special" href="sms:15155555785;?&body=Hello%20from%20Jim"><span>Text me</span></a>},
+ sms_to("15155555785", body: "Hello from Jim", class: "special") { content_tag(:span, "Text me") }
+ end
+
+ def test_sms_does_not_modify_html_options_hash
+ options = { class: "special" }
+ sms_to "15155555785", "ME!", options
+ assert_equal({ class: "special" }, options)
+ end
+
def protect_against_forgery?
request_forgery
end
diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb
index 22839bd9fb..42c004ce31 100644
--- a/activemodel/lib/active_model/errors.rb
+++ b/activemodel/lib/active_model/errors.rb
@@ -220,7 +220,7 @@ module ActiveModel
# # then yield :name and "must be specified"
# end
def each(&block)
- if block.arity == 1
+ if block.arity <= 1
@errors.each(&block)
else
ActiveSupport::Deprecation.warn(<<~MSG)
@@ -303,6 +303,16 @@ module ActiveModel
hash
end
+ def to_h
+ ActiveSupport::Deprecation.warn(<<~EOM)
+ ActiveModel::Errors#to_h is deprecated and will be removed in Rails 6.2
+ Please use `ActiveModel::Errors.to_hash` instead. The values in the hash
+ returned by `ActiveModel::Errors.to_hash` is an array of error messages.
+ EOM
+
+ to_hash.transform_values { |values| values.last }
+ end
+
def messages
DeprecationHandlingMessageHash.new(self)
end
diff --git a/activemodel/test/cases/errors_test.rb b/activemodel/test/cases/errors_test.rb
index d66d6ceff0..a6cd1da717 100644
--- a/activemodel/test/cases/errors_test.rb
+++ b/activemodel/test/cases/errors_test.rb
@@ -44,6 +44,14 @@ class ErrorsTest < ActiveModel::TestCase
assert_includes errors, "foo", "errors should include 'foo' as :foo"
end
+ def test_each_when_arity_is_negative
+ errors = ActiveModel::Errors.new(Person.new)
+ errors.add(:name, :blank)
+ errors.add(:gender, :blank)
+
+ assert_equal([:name, :gender], errors.map(&:attribute))
+ end
+
def test_any?
errors = ActiveModel::Errors.new(Person.new)
errors.add(:name)
@@ -466,6 +474,17 @@ class ErrorsTest < ActiveModel::TestCase
assert_equal ["name cannot be blank", "name cannot be nil"], person.errors.to_a
end
+ test "to_h is deprecated" do
+ person = Person.new
+ person.errors.add(:name, "cannot be blank")
+ person.errors.add(:name, "too long")
+
+ expected_deprecation = "ActiveModel::Errors#to_h is deprecated"
+ assert_deprecated(expected_deprecation) do
+ assert_equal({ name: "too long" }, person.errors.to_h)
+ end
+ end
+
test "to_hash returns the error messages hash" do
person = Person.new
person.errors.add(:name, "cannot be blank")
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md
index b39f423700..727ddd6bb7 100644
--- a/activerecord/CHANGELOG.md
+++ b/activerecord/CHANGELOG.md
@@ -1,3 +1,7 @@
+* Make currency symbols optional for money column type in PostgreSQL
+
+ *Joel Schneider*
+
* Add support for beginless ranges, introduced in Ruby 2.7.
*Josh Goodall*
diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb
index 94d8134b55..734ebb45ae 100644
--- a/activerecord/lib/active_record/autosave_association.rb
+++ b/activerecord/lib/active_record/autosave_association.rb
@@ -302,7 +302,7 @@ module ActiveRecord
def validate_single_association(reflection)
association = association_instance_get(reflection.name)
record = association && association.reader
- association_valid?(reflection, record) if record
+ association_valid?(reflection, record) if record && record.changed_for_autosave?
end
# Validate the associated records if <tt>:validate</tt> or
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb
index 6434377b57..357493dfc0 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb
@@ -26,9 +26,9 @@ module ActiveRecord
value = value.sub(/^\((.+)\)$/, '-\1') # (4)
case value
- when /^-?\D+[\d,]+\.\d{2}$/ # (1)
+ when /^-?\D*[\d,]+\.\d{2}$/ # (1)
value.gsub!(/[^-\d.]/, "")
- when /^-?\D+[\d.]+,\d{2}$/ # (2)
+ when /^-?\D*[\d.]+,\d{2}$/ # (2)
value.gsub!(/[^-\d,]/, "").sub!(/,/, ".")
end
diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake
index 648fdd0dc4..4d9acc911b 100644
--- a/activerecord/lib/active_record/railties/databases.rake
+++ b/activerecord/lib/active_record/railties/databases.rake
@@ -112,7 +112,7 @@ db_namespace = namespace :db do
end
end
- # desc 'Rollbacks the database one migration and re migrate up (options: STEP=x, VERSION=x).'
+ desc "Rolls back the database one migration and re-migrates up (options: STEP=x, VERSION=x)."
task redo: :load_config do
raise "Empty VERSION provided" if ENV["VERSION"] && ENV["VERSION"].empty?
@@ -128,7 +128,7 @@ db_namespace = namespace :db do
# desc 'Resets your database using your migrations for the current environment'
task reset: ["db:drop", "db:create", "db:migrate"]
- # desc 'Runs the "up" for a given migration VERSION.'
+ desc 'Runs the "up" for a given migration VERSION.'
task up: :load_config do
ActiveRecord::Tasks::DatabaseTasks.raise_for_multi_db(command: "db:migrate:up")
@@ -162,7 +162,7 @@ db_namespace = namespace :db do
end
end
- # desc 'Runs the "down" for a given migration VERSION.'
+ desc 'Runs the "down" for a given migration VERSION.'
task down: :load_config do
ActiveRecord::Tasks::DatabaseTasks.raise_for_multi_db(command: "db:migrate:down")
@@ -230,7 +230,7 @@ db_namespace = namespace :db do
db_namespace["_dump"].invoke
end
- # desc 'Drops and recreates the database from db/schema.rb for the current environment and loads the seeds.'
+ desc "Drops and recreates the database from db/schema.rb for the current environment and loads the seeds."
task reset: [ "db:drop", "db:setup" ]
# desc "Retrieves the charset for the current environment's database"
@@ -297,10 +297,11 @@ db_namespace = namespace :db do
ActiveRecord::Base.configurations.configs_for(env_name: ActiveRecord::Tasks::DatabaseTasks.env).each do |db_config|
ActiveRecord::Base.establish_connection(db_config.config)
- ActiveRecord::Tasks::DatabaseTasks.migrate
-
# Skipped when no database
- ActiveRecord::Tasks::DatabaseTasks.dump_schema(db_config.config, ActiveRecord::Base.schema_format, db_config.spec_name)
+ ActiveRecord::Tasks::DatabaseTasks.migrate
+ if ActiveRecord::Base.dump_schema_after_migration
+ ActiveRecord::Tasks::DatabaseTasks.dump_schema(db_config.config, ActiveRecord::Base.schema_format, db_config.spec_name)
+ end
rescue ActiveRecord::NoDatabaseError
ActiveRecord::Tasks::DatabaseTasks.create_current(db_config.env_name, db_config.spec_name)
diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb
index 87a53966e4..6a181882ae 100644
--- a/activerecord/lib/active_record/relation/query_methods.rb
+++ b/activerecord/lib/active_record/relation/query_methods.rb
@@ -952,7 +952,7 @@ module ActiveRecord
def optimizer_hints!(*args) # :nodoc:
args.flatten!
- self.optimizer_hints_values += args
+ self.optimizer_hints_values |= args
self
end
diff --git a/activerecord/test/cases/adapters/postgresql/money_test.rb b/activerecord/test/cases/adapters/postgresql/money_test.rb
index 1aa0348879..ff2ab22a80 100644
--- a/activerecord/test/cases/adapters/postgresql/money_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/money_test.rb
@@ -54,8 +54,12 @@ class PostgresqlMoneyTest < ActiveRecord::PostgreSQLTestCase
type = PostgresqlMoney.type_for_attribute("wealth")
assert_equal(12345678.12, type.cast(+"$12,345,678.12"))
assert_equal(12345678.12, type.cast(+"$12.345.678,12"))
+ assert_equal(12345678.12, type.cast(+"12,345,678.12"))
+ assert_equal(12345678.12, type.cast(+"12.345.678,12"))
assert_equal(-1.15, type.cast(+"-$1.15"))
assert_equal(-2.25, type.cast(+"($2.25)"))
+ assert_equal(-1.15, type.cast(+"-1.15"))
+ assert_equal(-2.25, type.cast(+"(2.25)"))
end
def test_schema_dumping
diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb
index 2d223a3035..3528ac045f 100644
--- a/activerecord/test/cases/autosave_association_test.rb
+++ b/activerecord/test/cases/autosave_association_test.rb
@@ -13,12 +13,14 @@ require "models/developer"
require "models/computer"
require "models/invoice"
require "models/line_item"
+require "models/mouse"
require "models/order"
require "models/parrot"
require "models/pirate"
require "models/project"
require "models/ship"
require "models/ship_part"
+require "models/squeak"
require "models/tag"
require "models/tagging"
require "models/treasure"
@@ -386,6 +388,20 @@ class TestDefaultAutosaveAssociationOnABelongsToAssociation < ActiveRecord::Test
assert_predicate auditlog, :valid?
end
+
+ def test_validation_does_not_validate_non_dirty_association_target
+ mouse = Mouse.create!(name: "Will")
+ Squeak.create!(mouse: mouse)
+
+ mouse.name = nil
+ mouse.save! validate: false
+
+ squeak = Squeak.last
+
+ assert_equal true, squeak.valid?
+ assert_equal true, squeak.mouse.present?
+ assert_equal true, squeak.valid?
+ end
end
class TestDefaultAutosaveAssociationOnAHasManyAssociationWithAcceptsNestedAttributes < ActiveRecord::TestCase
diff --git a/activerecord/test/cases/relation_test.rb b/activerecord/test/cases/relation_test.rb
index e0743de94b..e74fb1a098 100644
--- a/activerecord/test/cases/relation_test.rb
+++ b/activerecord/test/cases/relation_test.rb
@@ -363,6 +363,13 @@ module ActiveRecord
assert_match %r{/\*\+ BADHINT \*/}, post_with_hint.to_sql
end
+ def test_does_not_duplicate_optimizer_hints_on_merge
+ escaped_table = Post.connection.quote_table_name("posts")
+ expected = "SELECT /*+ OMGHINT */ #{escaped_table}.* FROM #{escaped_table}"
+ query = Post.optimizer_hints("OMGHINT").merge(Post.optimizer_hints("OMGHINT")).to_sql
+ assert_equal expected, query
+ end
+
class EnsureRoundTripTypeCasting < ActiveRecord::Type::Value
def type
:string
diff --git a/activerecord/test/models/mouse.rb b/activerecord/test/models/mouse.rb
new file mode 100644
index 0000000000..75a55c125d
--- /dev/null
+++ b/activerecord/test/models/mouse.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+class Mouse < ActiveRecord::Base
+ has_many :squeaks, autosave: true
+ validates :name, presence: true
+end
diff --git a/activerecord/test/models/squeak.rb b/activerecord/test/models/squeak.rb
new file mode 100644
index 0000000000..e0a643c238
--- /dev/null
+++ b/activerecord/test/models/squeak.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+class Squeak < ActiveRecord::Base
+ belongs_to :mouse
+ accepts_nested_attributes_for :mouse
+end
diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb
index b6c0ae0de2..dd0ff759b6 100644
--- a/activerecord/test/schema/schema.rb
+++ b/activerecord/test/schema/schema.rb
@@ -563,6 +563,10 @@ ActiveRecord::Schema.define do
t.string :type
end
+ create_table :mice, force: true do |t|
+ t.string :name
+ end
+
create_table :movies, force: true, id: false do |t|
t.primary_key :movieid
t.string :name
@@ -843,6 +847,10 @@ ActiveRecord::Schema.define do
end
end
+ create_table :squeaks, force: true do |t|
+ t.integer :mouse_id
+ end
+
create_table :prisoners, force: true do |t|
t.belongs_to :ship
end
diff --git a/activestorage/CHANGELOG.md b/activestorage/CHANGELOG.md
index fdb0f143f4..1475a7a786 100644
--- a/activestorage/CHANGELOG.md
+++ b/activestorage/CHANGELOG.md
@@ -1,3 +1,7 @@
+* Add `config.active_storage.draw_routes` to disable Active Storage routes.
+
+ *Gannon McGibbon*
+
* Image analysis is skipped if ImageMagick returns an error.
`ActiveStorage::Analyzer::ImageAnalyzer#metadata` would previously raise a
diff --git a/activestorage/config/routes.rb b/activestorage/config/routes.rb
index 3af7361cff..bde53e72f3 100644
--- a/activestorage/config/routes.rb
+++ b/activestorage/config/routes.rb
@@ -29,4 +29,4 @@ Rails.application.routes.draw do
resolve("ActiveStorage::Blob") { |blob, options| route_for(:rails_blob, blob, options) }
resolve("ActiveStorage::Attachment") { |attachment, options| route_for(:rails_blob, attachment.blob, options) }
-end
+end if ActiveStorage.draw_routes
diff --git a/activestorage/lib/active_storage.rb b/activestorage/lib/active_storage.rb
index 5c5da551ae..c35a9920d6 100644
--- a/activestorage/lib/active_storage.rb
+++ b/activestorage/lib/active_storage.rb
@@ -43,17 +43,26 @@ module ActiveStorage
mattr_accessor :logger
mattr_accessor :verifier
+ mattr_accessor :variant_processor, default: :mini_magick
+
mattr_accessor :queues, default: {}
+
mattr_accessor :previewers, default: []
- mattr_accessor :analyzers, default: []
- mattr_accessor :variant_processor, default: :mini_magick
+ mattr_accessor :analyzers, default: []
+
mattr_accessor :paths, default: {}
- mattr_accessor :variable_content_types, default: []
+
+ mattr_accessor :variable_content_types, default: []
+ mattr_accessor :binary_content_type, default: "application/octet-stream"
mattr_accessor :content_types_to_serve_as_binary, default: []
- mattr_accessor :content_types_allowed_inline, default: []
- mattr_accessor :binary_content_type, default: "application/octet-stream"
+ mattr_accessor :content_types_allowed_inline, default: []
+
mattr_accessor :service_urls_expire_in, default: 5.minutes
+
mattr_accessor :routes_prefix, default: "/rails/active_storage"
+ mattr_accessor :draw_routes, default: true
+
+ mattr_accessor :replace_on_assign_to_many, default: false
module Transformers
extend ActiveSupport::Autoload
diff --git a/activestorage/lib/active_storage/attached/model.rb b/activestorage/lib/active_storage/attached/model.rb
index ae7f0685f2..06864a846f 100644
--- a/activestorage/lib/active_storage/attached/model.rb
+++ b/activestorage/lib/active_storage/attached/model.rb
@@ -93,12 +93,19 @@ module ActiveStorage
end
def #{name}=(attachables)
- attachment_changes["#{name}"] =
- if attachables.nil? || Array(attachables).none?
- ActiveStorage::Attached::Changes::DeleteMany.new("#{name}", self)
- else
- ActiveStorage::Attached::Changes::CreateMany.new("#{name}", self, attachables)
+ if ActiveStorage.replace_on_assign_to_many
+ attachment_changes["#{name}"] =
+ if attachables.nil? || Array(attachables).none?
+ ActiveStorage::Attached::Changes::DeleteMany.new("#{name}", self)
+ else
+ ActiveStorage::Attached::Changes::CreateMany.new("#{name}", self, attachables)
+ end
+ else
+ if !attachables.nil? || Array(attachables).any?
+ attachment_changes["#{name}"] =
+ ActiveStorage::Attached::Changes::CreateMany.new("#{name}", self, #{name}.blobs + attachables)
end
+ end
end
CODE
diff --git a/activestorage/lib/active_storage/engine.rb b/activestorage/lib/active_storage/engine.rb
index f70d0a512a..9d9cd02d12 100644
--- a/activestorage/lib/active_storage/engine.rb
+++ b/activestorage/lib/active_storage/engine.rb
@@ -73,12 +73,15 @@ module ActiveStorage
ActiveStorage.analyzers = app.config.active_storage.analyzers || []
ActiveStorage.paths = app.config.active_storage.paths || {}
ActiveStorage.routes_prefix = app.config.active_storage.routes_prefix || "/rails/active_storage"
+ ActiveStorage.draw_routes = app.config.active_storage.draw_routes != false
ActiveStorage.variable_content_types = app.config.active_storage.variable_content_types || []
ActiveStorage.content_types_to_serve_as_binary = app.config.active_storage.content_types_to_serve_as_binary || []
ActiveStorage.service_urls_expire_in = app.config.active_storage.service_urls_expire_in || 5.minutes
ActiveStorage.content_types_allowed_inline = app.config.active_storage.content_types_allowed_inline || []
ActiveStorage.binary_content_type = app.config.active_storage.binary_content_type || "application/octet-stream"
+
+ ActiveStorage.replace_on_assign_to_many = app.config.active_storage.replace_on_assign_to_many || false
end
end
diff --git a/activestorage/lib/active_storage/service/disk_service.rb b/activestorage/lib/active_storage/service/disk_service.rb
index 67892d43b2..764a447c69 100644
--- a/activestorage/lib/active_storage/service/disk_service.rb
+++ b/activestorage/lib/active_storage/service/disk_service.rb
@@ -84,8 +84,12 @@ module ActiveStorage
purpose: :blob_key }
)
+ current_uri = URI.parse(current_host)
+
generated_url = url_helpers.rails_disk_service_url(verified_key_with_expiration,
- host: current_host,
+ protocol: current_uri.scheme,
+ host: current_uri.host,
+ port: current_uri.port,
disposition: content_disposition,
content_type: content_type,
filename: filename
diff --git a/activestorage/test/analyzer/video_analyzer_test.rb b/activestorage/test/analyzer/video_analyzer_test.rb
index 57e094908a..172a2f0aae 100644
--- a/activestorage/test/analyzer/video_analyzer_test.rb
+++ b/activestorage/test/analyzer/video_analyzer_test.rb
@@ -13,7 +13,7 @@ class ActiveStorage::Analyzer::VideoAnalyzerTest < ActiveSupport::TestCase
assert_equal 640, metadata[:width]
assert_equal 480, metadata[:height]
assert_equal [4, 3], metadata[:display_aspect_ratio]
- assert_equal true, metadata[:duration].between?(4, 6)
+ assert_equal 5.166648, metadata[:duration]
assert_not_includes metadata, :angle
end
diff --git a/activestorage/test/models/attached/many_test.rb b/activestorage/test/models/attached/many_test.rb
index 878e284049..39ddecb041 100644
--- a/activestorage/test/models/attached/many_test.rb
+++ b/activestorage/test/models/attached/many_test.rb
@@ -269,6 +269,24 @@ class ActiveStorage::ManyAttachedTest < ActiveSupport::TestCase
end
end
+ test "updating an existing record with attachments when appending on assign" do
+ append_on_assign do
+ @user.highlights.attach create_blob(filename: "funky.jpg"), create_blob(filename: "town.jpg")
+
+ assert_difference -> { @user.reload.highlights.count }, +2 do
+ @user.update! highlights: [ create_blob(filename: "whenever.jpg"), create_blob(filename: "wherever.jpg") ]
+ end
+
+ assert_no_difference -> { @user.reload.highlights.count } do
+ @user.update! highlights: [ ]
+ end
+
+ assert_no_difference -> { @user.reload.highlights.count } do
+ @user.update! highlights: nil
+ end
+ end
+ end
+
test "attaching existing blobs to a new record" do
User.new(name: "Jason").tap do |user|
user.highlights.attach create_blob(filename: "funky.jpg"), create_blob(filename: "town.jpg")
@@ -538,4 +556,12 @@ class ActiveStorage::ManyAttachedTest < ActiveSupport::TestCase
User.remove_method :highlights
end
end
+
+ private
+ def append_on_assign
+ ActiveStorage.replace_on_assign_to_many, previous = false, ActiveStorage.replace_on_assign_to_many
+ yield
+ ensure
+ ActiveStorage.replace_on_assign_to_many = previous
+ end
end
diff --git a/activestorage/test/service/disk_service_test.rb b/activestorage/test/service/disk_service_test.rb
index f3c4dd26bd..b766cc3f56 100644
--- a/activestorage/test/service/disk_service_test.rb
+++ b/activestorage/test/service/disk_service_test.rb
@@ -8,8 +8,14 @@ class ActiveStorage::Service::DiskServiceTest < ActiveSupport::TestCase
include ActiveStorage::Service::SharedServiceTests
test "URL generation" do
- assert_match(/^https:\/\/example.com\/rails\/active_storage\/disk\/.*\/avatar\.png\?content_type=image%2Fpng&disposition=inline/,
- @service.url(@key, expires_in: 5.minutes, disposition: :inline, filename: ActiveStorage::Filename.new("avatar.png"), content_type: "image/png"))
+ original_url_options = Rails.application.routes.default_url_options.dup
+ Rails.application.routes.default_url_options.merge!(protocol: "http", host: "test.example.com", port: 3001)
+ begin
+ assert_match(/^https:\/\/example.com\/rails\/active_storage\/disk\/.*\/avatar\.png\?content_type=image%2Fpng&disposition=inline/,
+ @service.url(@key, expires_in: 5.minutes, disposition: :inline, filename: ActiveStorage::Filename.new("avatar.png"), content_type: "image/png"))
+ ensure
+ Rails.application.routes.default_url_options = original_url_options
+ end
end
test "headers_for_direct_upload generation" do
diff --git a/activesupport/lib/active_support/core_ext/module/delegation.rb b/activesupport/lib/active_support/core_ext/module/delegation.rb
index 54271a3970..14d7f0c484 100644
--- a/activesupport/lib/active_support/core_ext/module/delegation.rb
+++ b/activesupport/lib/active_support/core_ext/module/delegation.rb
@@ -276,6 +276,11 @@ class Module
# The delegated method must be public on the target, otherwise it will
# raise +DelegationError+. If you wish to instead return +nil+,
# use the <tt>:allow_nil</tt> option.
+ #
+ # The <tt>marshal_dump</tt> and <tt>_dump</tt> methods are exempt from
+ # delegation due to possible interference when calling
+ # <tt>Marshal.dump(object)</tt>, should the delegation target method
+ # of <tt>object</tt> add or remove instance variables.
def delegate_missing_to(target, allow_nil: nil)
target = target.to_s
target = "self.#{target}" if DELEGATION_RESERVED_METHOD_NAMES.include?(target)
@@ -285,6 +290,7 @@ class Module
# It may look like an oversight, but we deliberately do not pass
# +include_private+, because they do not get delegated.
+ return false if name == :marshal_dump || name == :_dump
#{target}.respond_to?(name) || super
end
diff --git a/activesupport/lib/active_support/inflector/transliterate.rb b/activesupport/lib/active_support/inflector/transliterate.rb
index ea7161a6ba..ec6e9ccb59 100644
--- a/activesupport/lib/active_support/inflector/transliterate.rb
+++ b/activesupport/lib/active_support/inflector/transliterate.rb
@@ -5,9 +5,8 @@ require "active_support/i18n"
module ActiveSupport
module Inflector
- # Replaces non-ASCII characters in a UTF-8 encoded string with an ASCII
- # approximation, or if none exists, a replacement character which
- # defaults to "?".
+ # Replaces non-ASCII characters with an ASCII approximation, or if none
+ # exists, a replacement character which defaults to "?".
#
# transliterate('Ærøskøbing')
# # => "AEroskobing"
@@ -57,12 +56,8 @@ module ActiveSupport
#
# transliterate('Jürgen', locale: :de)
# # => "Juergen"
- #
- # This method requires that `string` be UTF-8 encoded. Passing an argument
- # with a different string encoding will raise an ArgumentError.
def transliterate(string, replacement = "?", locale: nil)
raise ArgumentError, "Can only transliterate strings. Received #{string.class.name}" unless string.is_a?(String)
- raise ArgumentError, "Can only transliterate UTF-8 strings. Received string with encoding #{string.encoding}" unless string.encoding == ::Encoding::UTF_8
I18n.transliterate(
ActiveSupport::Multibyte::Unicode.tidy_bytes(string).unicode_normalize(:nfc),
diff --git a/activesupport/lib/active_support/parameter_filter.rb b/activesupport/lib/active_support/parameter_filter.rb
index 8e5595babf..e1cd7c46c1 100644
--- a/activesupport/lib/active_support/parameter_filter.rb
+++ b/activesupport/lib/active_support/parameter_filter.rb
@@ -109,7 +109,12 @@ module ActiveSupport
elsif value.is_a?(Hash)
value = call(value, parents, original_params)
elsif value.is_a?(Array)
- value = value.map { |v| v.is_a?(Hash) ? call(v, parents, original_params) : v }
+ # If we don't pop the current parent it will be duplicated as we
+ # process each array value.
+ parents.pop if deep_regexps
+ value = value.map { |v| value_for_key(key, v, parents, original_params) }
+ # Restore the parent stack after processing the array.
+ parents.push(key) if deep_regexps
elsif blocks.any?
key = key.dup if key.duplicable?
value = value.dup if value.duplicable?
diff --git a/activesupport/lib/active_support/secure_compare_rotator.rb b/activesupport/lib/active_support/secure_compare_rotator.rb
new file mode 100644
index 0000000000..14a0aee947
--- /dev/null
+++ b/activesupport/lib/active_support/secure_compare_rotator.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+require "active_support/security_utils"
+require "active_support/messages/rotator"
+
+module ActiveSupport
+ # The ActiveSupport::SecureCompareRotator is a wrapper around +ActiveSupport::SecurityUtils.secure_compare+
+ # and allows you to rotate a previously defined value to a new one.
+ #
+ # It can be used as follow:
+ #
+ # rotator = ActiveSupport::SecureCompareRotator.new('new_production_value')
+ # rotator.rotate('previous_production_value')
+ # rotator.secure_compare!('previous_production_value')
+ #
+ # One real use case example would be to rotate a basic auth credentials:
+ #
+ # class MyController < ApplicationController
+ # def authenticate_request
+ # rotator = ActiveSupport::SecureComparerotator.new('new_password')
+ # rotator.rotate('old_password')
+ #
+ # authenticate_or_request_with_http_basic do |username, password|
+ # rotator.secure_compare!(password)
+ # rescue ActiveSupport::SecureCompareRotator::InvalidMatch
+ # false
+ # end
+ # end
+ # end
+ class SecureCompareRotator
+ include SecurityUtils
+ prepend Messages::Rotator
+
+ InvalidMatch = Class.new(StandardError)
+
+ def initialize(value, **_options)
+ @value = value
+ end
+
+ def secure_compare!(other_value, on_rotation: @rotation)
+ secure_compare(@value, other_value) ||
+ run_rotations(on_rotation) { |wrapper| wrapper.secure_compare!(other_value) } ||
+ raise(InvalidMatch)
+ end
+
+ private
+
+ def build_rotation(previous_value, _options)
+ self.class.new(previous_value)
+ end
+ end
+end
diff --git a/activesupport/test/core_ext/module_test.rb b/activesupport/test/core_ext/module_test.rb
index ec9ecd06ee..dd36a9373a 100644
--- a/activesupport/test/core_ext/module_test.rb
+++ b/activesupport/test/core_ext/module_test.rb
@@ -111,6 +111,24 @@ class DecoratedReserved
end
end
+class Maze
+ attr_accessor :cavern, :passages
+end
+
+class Cavern
+ delegate_missing_to :target
+
+ attr_reader :maze
+
+ def initialize(maze)
+ @maze = maze
+ end
+
+ def target
+ @maze.passages = :twisty
+ end
+end
+
class Block
def hello?
true
@@ -411,6 +429,17 @@ class ModuleTest < ActiveSupport::TestCase
assert_respond_to DecoratedTester.new(@david), :extra_missing
end
+ def test_delegate_missing_to_does_not_interfere_with_marshallization
+ maze = Maze.new
+ maze.cavern = Cavern.new(maze)
+
+ array = [maze, nil]
+ serialized_array = Marshal.dump(array)
+ deserialized_array = Marshal.load(serialized_array)
+
+ assert_nil deserialized_array[1]
+ end
+
def test_delegate_with_case
event = Event.new(Tester.new)
assert_equal 1, event.foo
diff --git a/activesupport/test/parameter_filter_test.rb b/activesupport/test/parameter_filter_test.rb
index d2dc71061d..e680a22479 100644
--- a/activesupport/test/parameter_filter_test.rb
+++ b/activesupport/test/parameter_filter_test.rb
@@ -28,10 +28,17 @@ class ParameterFilterTest < ActiveSupport::TestCase
value.replace("world!") if original_params["barg"]["blah"] == "bar" && key == "hello"
}
+ filter_words << lambda { |key, value|
+ value.upcase! if key == "array_elements"
+ }
+
parameter_filter = ActiveSupport::ParameterFilter.new(filter_words)
before_filter["barg"] = { :bargain => "gain", "blah" => "bar", "bar" => { "bargain" => { "blah" => "foo", "hello" => "world" } } }
after_filter["barg"] = { :bargain => "niag", "blah" => "[FILTERED]", "bar" => { "bargain" => { "blah" => "[FILTERED]", "hello" => "world!" } } }
+ before_filter["array_elements"] = %w(element1 element2)
+ after_filter["array_elements"] = %w(ELEMENT1 ELEMENT2)
+
assert_equal after_filter, parameter_filter.filter(before_filter)
end
end
diff --git a/activesupport/test/secure_compare_rotator_test.rb b/activesupport/test/secure_compare_rotator_test.rb
new file mode 100644
index 0000000000..8acf13e38f
--- /dev/null
+++ b/activesupport/test/secure_compare_rotator_test.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/secure_compare_rotator"
+
+class SecureCompareRotatorTest < ActiveSupport::TestCase
+ test "#secure_compare! works correctly after rotation" do
+ wrapper = ActiveSupport::SecureCompareRotator.new("old_secret")
+ wrapper.rotate("new_secret")
+
+ assert_equal(true, wrapper.secure_compare!("new_secret"))
+ end
+
+ test "#secure_compare! works correctly after multiple rotation" do
+ wrapper = ActiveSupport::SecureCompareRotator.new("old_secret")
+ wrapper.rotate("new_secret")
+ wrapper.rotate("another_secret")
+ wrapper.rotate("and_another_one")
+
+ assert_equal(true, wrapper.secure_compare!("and_another_one"))
+ end
+
+ test "#secure_compare! fails correctly when credential is not part of the rotation" do
+ wrapper = ActiveSupport::SecureCompareRotator.new("old_secret")
+ wrapper.rotate("new_secret")
+
+ assert_raises(ActiveSupport::SecureCompareRotator::InvalidMatch) do
+ wrapper.secure_compare!("different_secret")
+ end
+ end
+
+ test "#secure_compare! calls the on_rotation proc" do
+ wrapper = ActiveSupport::SecureCompareRotator.new("old_secret")
+ wrapper.rotate("new_secret")
+ wrapper.rotate("another_secret")
+ wrapper.rotate("and_another_one")
+
+ @witness = nil
+
+ assert_changes(:@witness, from: nil, to: true) do
+ assert_equal(true, wrapper.secure_compare!("and_another_one", on_rotation: -> { @witness = true }))
+ end
+ end
+end
diff --git a/activesupport/test/transliterate_test.rb b/activesupport/test/transliterate_test.rb
index 525b4a8559..9e29a93ea0 100644
--- a/activesupport/test/transliterate_test.rb
+++ b/activesupport/test/transliterate_test.rb
@@ -57,12 +57,4 @@ class TransliterateTest < ActiveSupport::TestCase
end
assert_equal "Can only transliterate strings. Received Object", exception.message
end
-
- def test_transliterate_handles_non_unicode_strings
- ascii_8bit_string = "A".b
- exception = assert_raises ArgumentError do
- assert_equal "A", ActiveSupport::Inflector.transliterate(ascii_8bit_string)
- end
- assert_equal "Can only transliterate UTF-8 strings. Received string with encoding ASCII-8BIT", exception.message
- end
end
diff --git a/guides/source/5_2_release_notes.md b/guides/source/5_2_release_notes.md
index ac247bc3f9..7aac07dbbe 100644
--- a/guides/source/5_2_release_notes.md
+++ b/guides/source/5_2_release_notes.md
@@ -326,7 +326,7 @@ Please refer to the [Changelog][action-view] for detailed changes.
select divider `option`.
([Pull Request](https://github.com/rails/rails/pull/31088))
-* Change `form_with` to generates ids by default.
+* Change `form_with` to generate ids by default.
([Commit](https://github.com/rails/rails/commit/260d6f112a0ffdbe03e6f5051504cb441c1e94cd))
* Add `preload_link_tag` helper.
diff --git a/guides/source/6_0_release_notes.md b/guides/source/6_0_release_notes.md
index c152076628..7c5478a03d 100644
--- a/guides/source/6_0_release_notes.md
+++ b/guides/source/6_0_release_notes.md
@@ -672,6 +672,12 @@ Please refer to the [Changelog][active-storage] for detailed changes.
is saved instead of immediately.
([Pull Request](https://github.com/rails/rails/pull/33303))
+* Optionally replace existing files instead of adding to them when assigning to
+ a collection of attachments (as in `@user.update!(images: [ … ])`). Use
+ `config.active_storage.replace_on_assign_to_many` to control this behavior.
+ ([Pull Request](https://github.com/rails/rails/pull/33303),
+ [Pull Request](https://github.com/rails/rails/pull/36716))
+
* Add the ability to reflect on defined attachments using the existing
Active Record reflection mechanism.
([Pull Request](https://github.com/rails/rails/pull/33018))
@@ -688,10 +694,6 @@ Please refer to the [Changelog][active-storage] for detailed changes.
`mini_magick` directly.
([Pull Request](https://github.com/rails/rails/pull/32471))
-* Replace existing images instead of adding to them when updating an
- attached model via `update` or `update!` with, say, `@user.update!(images: [ … ])`.
- ([Pull Request](https://github.com/rails/rails/pull/33303))
-
Active Model
------------
diff --git a/guides/source/configuring.md b/guides/source/configuring.md
index e53e8b4e92..ded985debe 100644
--- a/guides/source/configuring.md
+++ b/guides/source/configuring.md
@@ -353,7 +353,7 @@ All these configuration options are delegated to the `I18n` library.
* `config.active_record.lock_optimistically` controls whether Active Record will use optimistic locking and is `true` by default.
-* `config.active_record.cache_timestamp_format` controls the format of the timestamp value in the cache key. Default is `:nsec`.
+* `config.active_record.cache_timestamp_format` controls the format of the timestamp value in the cache key. Default is `:usec`.
* `config.active_record.record_timestamps` is a boolean value which controls whether or not timestamping of `create` and `update` operations on a model occur. The default value is `true`.
@@ -881,7 +881,11 @@ text/javascript image/svg+xml application/postscript application/x-shockwave-fla
config.active_storage.routes_prefix = '/files'
```
- The default is `/rails/active_storage`
+ The default is `/rails/active_storage`.
+
+* `config.active_storage.replace_on_assign_to_many` determines whether assigning to a collection of attachments declared with `has_many_attached` replaces any existing attachments or appends to them. The default is `true`.
+
+* `config.active_storage.draw_routes` can be used to toggle Active Storage route generation. The default is `true`.
### Results of `load_defaults`
@@ -917,6 +921,7 @@ text/javascript image/svg+xml application/postscript application/x-shockwave-fla
- `config.active_job.return_false_on_aborted_enqueue`: `true`
- `config.active_storage.queues.analysis`: `:active_storage_analysis`
- `config.active_storage.queues.purge`: `:active_storage_purge`
+- `config.active_storage.replace_on_assign_to_many`: `true`
- `config.active_record.collection_cache_versioning`: `true`
### Configuring a Database
diff --git a/guides/source/upgrading_ruby_on_rails.md b/guides/source/upgrading_ruby_on_rails.md
index f17955e022..05980d1614 100644
--- a/guides/source/upgrading_ruby_on_rails.md
+++ b/guides/source/upgrading_ruby_on_rails.md
@@ -398,6 +398,54 @@ config.load_defaults "6.0"
config.autoloader = :classic
```
+### Active Storage assignment behavior change
+
+In Rails 5.2, assigning to a collection of attachments declared with `has_many_attached` appended new files:
+
+```ruby
+class User < ApplicationRecord
+ has_many_attached :highlights
+end
+
+user.highlights.attach(filename: "funky.jpg", ...)
+user.higlights.count # => 1
+
+blob = ActiveStorage::Blob.create_after_upload!(filename: "town.jpg", ...)
+user.update!(highlights: [ blob ])
+
+user.highlights.count # => 2
+user.highlights.first.filename # => "funky.jpg"
+user.highlights.second.filename # => "town.jpg"
+```
+
+With the default configuration for Rails 6.0, assigning to a collection of attachments replaces existing files
+instead of appending to them. This matches Active Record behavior when assigning to a collection association:
+
+```ruby
+user.highlights.attach(filename: "funky.jpg", ...)
+user.highlights.count # => 1
+
+blob = ActiveStorage::Blob.create_after_upload!(filename: "town.jpg", ...)
+user.update!(highlights: [ blob ])
+
+user.highlights.count # => 1
+user.highlights.first.filename # => "town.jpg"
+```
+
+`#attach` can be used to add new attachments without removing the existing ones:
+
+```ruby
+blob = ActiveStorage::Blob.create_after_upload!(filename: "town.jpg", ...)
+user.highlights.attach(blob)
+
+user.highlights.count # => 2
+user.highlights.first.filename # => "funky.jpg"
+user.highlights.second.filename # => "town.jpg"
+```
+
+Opt in to the new default behavior by setting `config.active_storage.replace_on_assign_to_many` to `true`.
+The old behavior will be deprecated in Rails 6.1 and removed in a subsequent release.
+
Upgrading from Rails 5.1 to Rails 5.2
-------------------------------------
diff --git a/railties/lib/rails/application/configuration.rb b/railties/lib/rails/application/configuration.rb
index 50d43ff69e..934578e9f1 100644
--- a/railties/lib/rails/application/configuration.rb
+++ b/railties/lib/rails/application/configuration.rb
@@ -145,6 +145,8 @@ module Rails
if respond_to?(:active_storage)
active_storage.queues.analysis = :active_storage_analysis
active_storage.queues.purge = :active_storage_purge
+
+ active_storage.replace_on_assign_to_many = true
end
if respond_to?(:active_record)
diff --git a/railties/lib/rails/generators/actions.rb b/railties/lib/rails/generators/actions.rb
index 406a5b8fc7..b6225cd8c0 100644
--- a/railties/lib/rails/generators/actions.rb
+++ b/railties/lib/rails/generators/actions.rb
@@ -40,8 +40,7 @@ module Rails
in_root do
str = "gem #{parts.join(", ")}"
str = indentation + str
- str = "\n" + str
- append_file "Gemfile", str, verbose: false
+ append_file_with_newline "Gemfile", str, verbose: false
end
end
@@ -58,9 +57,9 @@ module Rails
log :gemfile, "group #{str}"
in_root do
- append_file "Gemfile", "\ngroup #{str} do", force: true
+ append_file_with_newline "Gemfile", "\ngroup #{str} do", force: true
with_indentation(&block)
- append_file "Gemfile", "\nend\n", force: true
+ append_file_with_newline "Gemfile", "end", force: true
end
end
@@ -71,9 +70,13 @@ module Rails
log :github, "github #{str}"
in_root do
- append_file "Gemfile", "\n#{indentation}github #{str} do", force: true
+ if @indentation.zero?
+ append_file_with_newline "Gemfile", "\ngithub #{str} do", force: true
+ else
+ append_file_with_newline "Gemfile", "#{indentation}github #{str} do", force: true
+ end
with_indentation(&block)
- append_file "Gemfile", "\n#{indentation}end", force: true
+ append_file_with_newline "Gemfile", "#{indentation}end", force: true
end
end
@@ -91,9 +94,9 @@ module Rails
in_root do
if block
- append_file "Gemfile", "\nsource #{quote(source)} do", force: true
+ append_file_with_newline "Gemfile", "\nsource #{quote(source)} do", force: true
with_indentation(&block)
- append_file "Gemfile", "\nend\n", force: true
+ append_file_with_newline "Gemfile", "end", force: true
else
prepend_file "Gemfile", "source #{quote(source)}\n", verbose: false
end
@@ -344,6 +347,13 @@ module Rails
ensure
@indentation -= 1
end
+
+ # Append string to a file with a newline if necessary
+ def append_file_with_newline(path, str, options = {})
+ gsub_file path, /\n?\z/, options do |match|
+ match.end_with?("\n") ? "" : "\n#{str}\n"
+ end
+ end
end
end
end
diff --git a/railties/lib/rails/generators/base.rb b/railties/lib/rails/generators/base.rb
index 0b91e3223e..a153923ce3 100644
--- a/railties/lib/rails/generators/base.rb
+++ b/railties/lib/rails/generators/base.rb
@@ -20,6 +20,8 @@ module Rails
class_option :skip_namespace, type: :boolean, default: false,
desc: "Skip namespace (affects only isolated applications)"
+ class_option :skip_collision_check, type: :boolean, default: false,
+ desc: "Skip collision check"
add_runtime_options!
strict_args_position!
@@ -249,6 +251,7 @@ module Rails
# application or Ruby on Rails.
def class_collisions(*class_names)
return unless behavior == :invoke
+ return if options.skip_collision_check?
class_names.flatten.each do |class_name|
class_name = class_name.to_s
@@ -261,8 +264,8 @@ module Rails
if last && last.const_defined?(last_name.camelize, false)
raise Error, "The name '#{class_name}' is either already used in your application " \
- "or reserved by Ruby on Rails. Please choose an alternative and run " \
- "this generator again."
+ "or reserved by Ruby on Rails. Please choose an alternative or use --skip-collision-check " \
+ "to skip this check and run this generator again."
end
end
end
diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/filter_parameter_logging.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/initializers/filter_parameter_logging.rb.tt
index 4a994e1e7b..eea99edb65 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/initializers/filter_parameter_logging.rb.tt
+++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/filter_parameter_logging.rb.tt
@@ -1,4 +1,6 @@
# Be sure to restart your server when you modify this file.
# Configure sensitive parameters which will be filtered from the log file.
-Rails.application.config.filter_parameters += [:password]
+Rails.application.config.filter_parameters += [
+ :password, :secret, :token, :_key, :auth, :crypt, :salt, :certificate, :otp, :access, :private, :protected, :ssn
+]
diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_6_0.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_6_0.rb.tt
index abb03e761b..ffe53497bf 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_6_0.rb.tt
+++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_6_0.rb.tt
@@ -26,6 +26,10 @@
# Rails.application.config.active_storage.queues.analysis = :active_storage_analysis
# Rails.application.config.active_storage.queues.purge = :active_storage_purge
+# When assigning to a collection of attachments declared via `has_many_attached`, replace existing
+# attachments instead of appending. Use #attach to add new attachments without replacing existing ones.
+# Rails.application.config.active_storage.replace_on_assign_to_many = true
+
# Use ActionMailer::MailDeliveryJob for sending parameterized and normal mail.
#
# The default delivery jobs (ActionMailer::Parameterized::DeliveryJob, ActionMailer::DeliveryJob),
diff --git a/railties/lib/rails/generators/rails/app/templates/config/puma.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/puma.rb.tt
index 649253aeca..5ed4437744 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/puma.rb.tt
+++ b/railties/lib/rails/generators/rails/app/templates/config/puma.rb.tt
@@ -16,6 +16,9 @@ port ENV.fetch("PORT") { 3000 }
#
environment ENV.fetch("RAILS_ENV") { "development" }
+# Specifies the `pidfile` that Puma will use.
+pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" }
+
# Specifies the number of `workers` to boot in clustered mode.
# Workers are forked web server processes. If using threads and workers together
# the concurrency of the application would be max `threads` * `workers`.
diff --git a/railties/lib/rails/source_annotation_extractor.rb b/railties/lib/rails/source_annotation_extractor.rb
index 9ce22b96a6..77a99036ec 100644
--- a/railties/lib/rails/source_annotation_extractor.rb
+++ b/railties/lib/rails/source_annotation_extractor.rb
@@ -2,11 +2,6 @@
require "active_support/deprecation"
-# Remove this deprecated class in the next minor version
-#:nodoc:
-SourceAnnotationExtractor = ActiveSupport::Deprecation::DeprecatedConstantProxy.
- new("SourceAnnotationExtractor", "Rails::SourceAnnotationExtractor")
-
module Rails
# Implements the logic behind <tt>Rails::Command::NotesCommand</tt>. See <tt>rails notes --help</tt> for usage information.
#
@@ -160,3 +155,8 @@ module Rails
end
end
end
+
+# Remove this deprecated class in the next minor version
+#:nodoc:
+SourceAnnotationExtractor = ActiveSupport::Deprecation::DeprecatedConstantProxy.
+ new("SourceAnnotationExtractor", "Rails::SourceAnnotationExtractor")
diff --git a/railties/test/application/configuration_test.rb b/railties/test/application/configuration_test.rb
index a05d86f738..96678c395c 100644
--- a/railties/test/application/configuration_test.rb
+++ b/railties/test/application/configuration_test.rb
@@ -2593,6 +2593,21 @@ module ApplicationTests
MESSAGE
end
+ test "ActiveStorage.draw_routes can be configured via config.active_storage.draw_routes" do
+ app_file "config/environments/development.rb", <<-RUBY
+ Rails.application.configure do
+ config.active_storage.draw_routes = false
+ end
+ RUBY
+
+ output = rails("routes")
+ assert_not_includes(output, "rails_service_blob")
+ assert_not_includes(output, "rails_blob_representation")
+ assert_not_includes(output, "rails_disk_service")
+ assert_not_includes(output, "update_rails_disk_service")
+ assert_not_includes(output, "rails_direct_uploads")
+ end
+
test "hosts include .localhost in development" do
app "development"
assert_includes Rails.application.config.hosts, ".localhost"
diff --git a/railties/test/application/generators_test.rb b/railties/test/application/generators_test.rb
index e5e557d204..8ec26db772 100644
--- a/railties/test/application/generators_test.rb
+++ b/railties/test/application/generators_test.rb
@@ -198,5 +198,15 @@ module ApplicationTests
assert_no_match "active_record:migration", output
end
end
+
+ test "skip collision check" do
+ rails("generate", "model", "post", "title:string")
+
+ output = rails("generate", "model", "post", "title:string", "body:string")
+ assert_match(/The name 'Post' is either already used in your application or reserved/, output)
+
+ output = rails("generate", "model", "post", "title:string", "body:string", "--skip-collision-check")
+ assert_no_match(/The name 'Post' is either already used in your application or reserved/, output)
+ end
end
end
diff --git a/railties/test/application/rake/dbs_test.rb b/railties/test/application/rake/dbs_test.rb
index 79c521dbf6..c9931c45a6 100644
--- a/railties/test/application/rake/dbs_test.rb
+++ b/railties/test/application/rake/dbs_test.rb
@@ -630,6 +630,22 @@ module ApplicationTests
assert_match(/CreateRecipes: migrated/, output)
end
end
+
+ test "db:prepare does not touch schema when dumping is disabled" do
+ Dir.chdir(app_path) do
+ rails "generate", "model", "book", "title:string"
+ rails "db:create", "db:migrate"
+
+ app_file "db/schema.rb", "Not touched"
+ app_file "config/initializers/disable_dumping_schema.rb", <<-RUBY
+ Rails.application.config.active_record.dump_schema_after_migration = false
+ RUBY
+
+ rails "db:prepare"
+
+ assert_equal("Not touched", File.read("db/schema.rb").strip)
+ end
+ end
end
end
end
diff --git a/railties/test/application/system_test_case_test.rb b/railties/test/application/system_test_case_test.rb
new file mode 100644
index 0000000000..d15a0d9210
--- /dev/null
+++ b/railties/test/application/system_test_case_test.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+require "isolation/abstract_unit"
+require "rack/test"
+
+class SystemTestCaseTest < ActiveSupport::TestCase
+ include ActiveSupport::Testing::Isolation
+
+ def setup
+ build_app
+ end
+
+ def teardown
+ teardown_app
+ end
+
+ test "url helpers are delegated to a proxy class" do
+ app_file "config/routes.rb", <<-RUBY
+ Rails.application.routes.draw do
+ get 'foo', to: 'foo#index', as: 'test_foo'
+ end
+ RUBY
+
+ app("test")
+
+ assert_not_includes(ActionDispatch::SystemTestCase.runnable_methods, :test_foo_url)
+ end
+
+ test "system tests set the Capybara host in the url_options by default" do
+ app_file "config/routes.rb", <<-RUBY
+ Rails.application.routes.draw do
+ get 'foo', to: 'foo#index', as: 'test_foo'
+ end
+ RUBY
+
+ app("test")
+ system_test = ActionDispatch::SystemTestCase.new("my_test")
+ previous_app_host = ::Capybara.app_host
+ ::Capybara.app_host = "https://my_test_example.com"
+
+ assert_equal("https://my_test_example.com/foo", system_test.test_foo_url)
+ ensure
+ ::Capybara.app_host = previous_app_host
+ end
+end
diff --git a/railties/test/generators/actions_test.rb b/railties/test/generators/actions_test.rb
index 150836d4ce..5d6d7f1595 100644
--- a/railties/test/generators/actions_test.rb
+++ b/railties/test/generators/actions_test.rb
@@ -43,7 +43,7 @@ class ActionsTest < Rails::Generators::TestCase
def test_add_source_adds_source_to_gemfile
run_generator
action :add_source, "http://gems.github.com"
- assert_file "Gemfile", /source 'http:\/\/gems\.github\.com'/
+ assert_file "Gemfile", /source 'http:\/\/gems\.github\.com'\n/
end
def test_add_source_with_block_adds_source_to_gemfile_with_gem
@@ -51,7 +51,7 @@ class ActionsTest < Rails::Generators::TestCase
action :add_source, "http://gems.github.com" do
gem "rspec-rails"
end
- assert_file "Gemfile", /source 'http:\/\/gems\.github\.com' do\n gem 'rspec-rails'\nend/
+ assert_file "Gemfile", /\n\nsource 'http:\/\/gems\.github\.com' do\n gem 'rspec-rails'\nend\n\z/
end
def test_add_source_with_block_adds_source_to_gemfile_after_gem
@@ -60,13 +60,25 @@ class ActionsTest < Rails::Generators::TestCase
action :add_source, "http://gems.github.com" do
gem "rspec-rails"
end
- assert_file "Gemfile", /gem 'will-paginate'\nsource 'http:\/\/gems\.github\.com' do\n gem 'rspec-rails'\nend/
+ assert_file "Gemfile", /\ngem 'will-paginate'\n\nsource 'http:\/\/gems\.github\.com' do\n gem 'rspec-rails'\nend\n\z/
+ end
+
+ def test_add_source_should_create_newline_between_blocks
+ run_generator
+ action :add_source, "http://gems.github.com" do
+ gem "rspec-rails"
+ end
+
+ action :add_source, "http://gems2.github.com" do
+ gem "fakeweb"
+ end
+ assert_file "Gemfile", /\n\nsource 'http:\/\/gems\.github\.com' do\n gem 'rspec-rails'\nend\n\nsource 'http:\/\/gems2\.github\.com' do\n gem 'fakeweb'\nend\n\z/
end
def test_gem_should_put_gem_dependency_in_gemfile
run_generator
action :gem, "will-paginate"
- assert_file "Gemfile", /gem 'will\-paginate'/
+ assert_file "Gemfile", /gem 'will\-paginate'\n\z/
end
def test_gem_with_version_should_include_version_in_gemfile
@@ -141,7 +153,7 @@ class ActionsTest < Rails::Generators::TestCase
gem "fakeweb"
end
- assert_file "Gemfile", /\ngroup :development, :test do\n gem 'rspec-rails'\nend\n\ngroup :test do\n gem 'fakeweb'\nend/
+ assert_file "Gemfile", /\n\ngroup :development, :test do\n gem 'rspec-rails'\nend\n\ngroup :test do\n gem 'fakeweb'\nend\n\z/
end
def test_github_should_create_an_indented_block
@@ -153,7 +165,7 @@ class ActionsTest < Rails::Generators::TestCase
gem "baz"
end
- assert_file "Gemfile", /\ngithub 'user\/repo' do\n gem 'foo'\n gem 'bar'\n gem 'baz'\nend/
+ assert_file "Gemfile", /\n\ngithub 'user\/repo' do\n gem 'foo'\n gem 'bar'\n gem 'baz'\nend\n\z/
end
def test_github_should_create_an_indented_block_with_options
@@ -165,7 +177,7 @@ class ActionsTest < Rails::Generators::TestCase
gem "baz"
end
- assert_file "Gemfile", /\ngithub 'user\/repo', a: 'correct', other: true do\n gem 'foo'\n gem 'bar'\n gem 'baz'\nend/
+ assert_file "Gemfile", /\n\ngithub 'user\/repo', a: 'correct', other: true do\n gem 'foo'\n gem 'bar'\n gem 'baz'\nend\n\z/
end
def test_github_should_create_an_indented_block_within_a_group
@@ -177,9 +189,73 @@ class ActionsTest < Rails::Generators::TestCase
gem "bar"
gem "baz"
end
+ github "user/repo2", a: "correct", other: true do
+ gem "foo"
+ gem "bar"
+ gem "baz"
+ end
+ end
+
+ assert_file "Gemfile", /\n\ngroup :magic do\n github 'user\/repo', a: 'correct', other: true do\n gem 'foo'\n gem 'bar'\n gem 'baz'\n end\n github 'user\/repo2', a: 'correct', other: true do\n gem 'foo'\n gem 'bar'\n gem 'baz'\n end\nend\n\z/
+ end
+
+ def test_github_should_create_newline_between_blocks
+ run_generator
+
+ action :github, "user/repo", a: "correct", other: true do
+ gem "foo"
+ gem "bar"
+ gem "baz"
+ end
+
+ action :github, "user/repo2", a: "correct", other: true do
+ gem "foo"
+ gem "bar"
+ gem "baz"
+ end
+
+ assert_file "Gemfile", /\n\ngithub 'user\/repo', a: 'correct', other: true do\n gem 'foo'\n gem 'bar'\n gem 'baz'\nend\n\ngithub 'user\/repo2', a: 'correct', other: true do\n gem 'foo'\n gem 'bar'\n gem 'baz'\nend\n\z/
+ end
+
+ def test_gem_with_gemfile_without_newline_at_the_end
+ run_generator
+ File.open("Gemfile", "a") { |f| f.write("gem 'rspec-rails'") }
+
+ action :gem, "will-paginate"
+ assert_file "Gemfile", /gem 'rspec-rails'\ngem 'will-paginate'\n\z/
+ end
+
+ def test_gem_group_with_gemfile_without_newline_at_the_end
+ run_generator
+ File.open("Gemfile", "a") { |f| f.write("gem 'rspec-rails'") }
+
+ action :gem_group, :test do
+ gem "fakeweb"
+ end
+
+ assert_file "Gemfile", /gem 'rspec-rails'\n\ngroup :test do\n gem 'fakeweb'\nend\n\z/
+ end
+
+ def test_add_source_with_gemfile_without_newline_at_the_end
+ run_generator
+ File.open("Gemfile", "a") { |f| f.write("gem 'rspec-rails'") }
+
+ action :add_source, "http://gems.github.com" do
+ gem "fakeweb"
+ end
+
+ assert_file "Gemfile", /gem 'rspec-rails'\n\nsource 'http:\/\/gems\.github\.com' do\n gem 'fakeweb'\nend\n\z/
+ end
+
+ def test_github_with_gemfile_without_newline_at_the_end
+ run_generator
+ File.open("Gemfile", "a") { |f| f.write("gem 'rspec-rails'") }
+
+ action :github, "user/repo" do
+ gem "fakeweb"
end
- assert_file "Gemfile", /\ngroup :magic do\n github 'user\/repo', a: 'correct', other: true do\n gem 'foo'\n gem 'bar'\n gem 'baz'\n end\nend\n/
+ assert_file "Gemfile", /gem 'rspec-rails'\n\ngithub 'user\/repo' do\n gem 'fakeweb'\nend\n\z/
end
def test_environment_should_include_data_in_environment_initializer_block