diff options
93 files changed, 854 insertions, 473 deletions
diff --git a/.github/autolabeler.yml b/.github/autolabeler.yml index d73b2e3362..4a1d2bb6bf 100644 --- a/.github/autolabeler.yml +++ b/.github/autolabeler.yml @@ -1,5 +1,7 @@ actioncable: - "actioncable/**/*" +actionmailbox: + - "actionmailbox/**/*" actionmailer: - "actionmailer/**/*" actionpack: diff --git a/.travis.yml b/.travis.yml index f3d3c2181a..7bf508b7a7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -35,7 +35,7 @@ bundler_args: --jobs 3 --retry 3 before_install: - "rm ${BUNDLE_GEMFILE}.lock" - "travis_retry gem update --system" - - "travis_retry gem install bundler -v '2.0.0.pre.2'" + - "travis_retry gem install bundler" - "[[ -z $encrypted_0fb9444d0374_key && -z $encrypted_0fb9444d0374_iv ]] || openssl aes-256-cbc -K $encrypted_0fb9444d0374_key -iv $encrypted_0fb9444d0374_iv -in activestorage/test/service/configurations.yml.enc -out activestorage/test/service/configurations.yml -d" - "[[ $GEM != 'actioncable:integration' ]] || yarn install" - "[[ $GEM != 'actionview:ujs' ]] || nvm install node" @@ -29,9 +29,6 @@ gem "json", ">= 2.0.0" gem "rubocop", ">= 0.47", require: false -# https://github.com/guard/rb-inotify/pull/79 -gem "rb-inotify", github: "matthewd/rb-inotify", branch: "close-handling", require: false - group :doc do gem "sdoc", "~> 1.0" gem "redcarpet", "~> 3.2.3", platforms: :ruby diff --git a/Gemfile.lock b/Gemfile.lock index 22a58b80f1..b07fc7b0ec 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,12 +1,4 @@ GIT - remote: https://github.com/matthewd/rb-inotify.git - revision: 856730aad4b285969e8dd621e44808a7c5af4242 - branch: close-handling - specs: - rb-inotify (0.9.9) - ffi (~> 1.0) - -GIT remote: https://github.com/matthewd/websocket-client-simple.git revision: e161305f1a466b9398d86df3b1731b03362da91b branch: close-race @@ -325,7 +317,7 @@ GEM mimemagic (0.3.3) mini_magick (4.9.2) mini_mime (1.0.1) - mini_portile2 (2.3.0) + mini_portile2 (2.4.0) minitest (5.11.3) minitest-bisect (1.4.0) minitest-server (~> 1.0) @@ -348,13 +340,13 @@ GEM mysql2 (0.5.2-x86-mingw32) nio4r (2.3.1) nio4r (2.3.1-java) - nokogiri (1.8.5) - mini_portile2 (~> 2.3.0) - nokogiri (1.8.5-java) - nokogiri (1.8.5-x64-mingw32) - mini_portile2 (~> 2.3.0) - nokogiri (1.8.5-x86-mingw32) - mini_portile2 (~> 2.3.0) + nokogiri (1.9.1) + mini_portile2 (~> 2.4.0) + nokogiri (1.9.1-java) + nokogiri (1.9.1-x64-mingw32) + mini_portile2 (~> 2.4.0) + nokogiri (1.9.1-x86-mingw32) + mini_portile2 (~> 2.4.0) os (1.0.0) parallel (1.12.1) parser (2.5.3.0) @@ -391,6 +383,8 @@ GEM rainbow (3.0.0) rake (12.3.1) rb-fsevent (0.10.3) + rb-inotify (0.10.0) + ffi (~> 1.0) rdoc (6.0.4) redcarpet (3.2.3) redis (4.0.3) @@ -568,7 +562,6 @@ DEPENDENCIES rack-cache (~> 1.2) rails! rake (>= 11.1) - rb-inotify! redcarpet (~> 3.2.3) redis (~> 4.0) redis-namespace diff --git a/MIT-LICENSE b/MIT-LICENSE index 8f769c0767..315a4bc7c4 100644 --- a/MIT-LICENSE +++ b/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2005-2018 David Heinemeier Hansson +Copyright (c) 2005-2019 David Heinemeier Hansson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/actioncable/MIT-LICENSE b/actioncable/MIT-LICENSE index a42759f024..bdb42d988a 100644 --- a/actioncable/MIT-LICENSE +++ b/actioncable/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2015-2018 Basecamp, LLC +Copyright (c) 2015-2019 Basecamp, LLC Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/actioncable/lib/action_cable.rb b/actioncable/lib/action_cable.rb index cb9dfa2268..ad5fd43155 100644 --- a/actioncable/lib/action_cable.rb +++ b/actioncable/lib/action_cable.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true #-- -# Copyright (c) 2015-2018 Basecamp, LLC +# Copyright (c) 2015-2019 Basecamp, LLC # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/actioncable/lib/action_cable/channel/test_case.rb b/actioncable/lib/action_cable/channel/test_case.rb index dd2762ccd0..c4cf0ac0e7 100644 --- a/actioncable/lib/action_cable/channel/test_case.rb +++ b/actioncable/lib/action_cable/channel/test_case.rb @@ -84,7 +84,18 @@ module ActionCable # assert subscription.confirmed? # # # Asserts that the channel subscribes connection to a stream - # assert_equal "chat_1", streams.last + # assert_has_stream "chat_1" + # + # # Asserts that the channel subscribes connection to a specific + # # stream created for a model + # assert_has_stream_for Room.find(1) + # end + # + # def test_does_not_stream_with_incorrect_room_number + # subscribe room_number: -1 + # + # # Asserts that not streams was started + # assert_no_streams # end # # def test_does_not_subscribe_without_room_number @@ -115,8 +126,6 @@ module ActionCable # An instance of the current channel, created when you call `subscribe`. # <b>transmissions</b>:: # A list of all messages that have been transmitted into the channel. - # <b>streams</b>:: - # A list of all created streams subscriptions (as identifiers) for the subscription. # # # == Channel is automatically inferred @@ -167,7 +176,6 @@ module ActionCable class_attribute :_channel_class attr_reader :connection, :subscription - delegate :streams, to: :subscription ActiveSupport.run_load_hooks(:action_cable_channel_test_case, self) end @@ -251,6 +259,39 @@ module ActionCable super(broadcasting_for(stream_or_object), *args) end + # Asserts that no streams have been started. + # + # def test_assert_no_started_stream + # subscribe + # assert_no_streams + # end + # + def assert_no_streams + assert subscription.streams.empty?, "No streams started was expected, but #{subscription.streams.count} found" + end + + # Asserts that the specified stream has been started. + # + # def test_assert_started_stream + # subscribe + # assert_has_stream 'messages' + # end + # + def assert_has_stream(stream) + assert subscription.streams.include?(stream), "Stream #{stream} has not been started" + end + + # Asserts that the specified stream for a model has started. + # + # def test_assert_started_stream_for + # subscribe id: 42 + # assert_has_stream_for User.find(42) + # end + # + def assert_has_stream_for(object) + assert_has_stream(broadcasting_for(object)) + end + private def check_subscribed! raise "Must be subscribed!" if subscription.nil? || subscription.rejected? diff --git a/actioncable/test/channel/test_case_test.rb b/actioncable/test/channel/test_case_test.rb index 63d0d6207e..9c360d5dc3 100644 --- a/actioncable/test/channel/test_case_test.rb +++ b/actioncable/test/channel/test_case_test.rb @@ -93,13 +93,39 @@ class StreamsTestChannelTest < ActionCable::Channel::TestCase def test_stream_without_params subscribe - assert_equal "test_0", streams.last + assert_has_stream "test_0" end def test_stream_with_params subscribe id: 42 - assert_equal "test_42", streams.last + assert_has_stream "test_42" + end +end + +class StreamsForTestChannel < ActionCable::Channel::Base + def subscribed + stream_for User.new(params[:id]) + end +end + +class StreamsForTestChannelTest < ActionCable::Channel::TestCase + def test_stream_with_params + subscribe id: 42 + + assert_has_stream_for User.new(42) + end +end + +class NoStreamsTestChannel < ActionCable::Channel::Base + def subscribed; end # no-op +end + +class NoStreamsTestChannelTest < ActionCable::Channel::TestCase + def test_stream_with_params + subscribe + + assert_no_streams end end diff --git a/actionmailbox/MIT-LICENSE b/actionmailbox/MIT-LICENSE index 4a5fe6361d..e28efa78e1 100644 --- a/actionmailbox/MIT-LICENSE +++ b/actionmailbox/MIT-LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2018 Basecamp, LLC +Copyright (c) 2019 Basecamp, LLC Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/actionmailbox/README.md b/actionmailbox/README.md index 705a4432ff..f0bde03961 100644 --- a/actionmailbox/README.md +++ b/actionmailbox/README.md @@ -6,273 +6,7 @@ The inbound emails are turned into `InboundEmail` records using Active Record an These inbound emails are routed asynchronously using Active Job to one or several dedicated mailboxes, which are capable of interacting directly with the rest of your domain model. - -## How does this compare to Action Mailer's inbound processing? - -Rails has long had an anemic way of [receiving emails using Action Mailer](https://guides.rubyonrails.org/action_mailer_basics.html#receiving-emails), but it was poorly fleshed out, lacked cohesion with the task of sending emails, and offered no help on integrating with popular inbound email processing platforms. Action Mailbox supersedes the receiving part of Action Mailer, which will be deprecated in due course. - - -## Installing - -Assumes a Rails 5.2+ application: - -1. Install the gem: - - ```ruby - # Gemfile - gem "actionmailbox", github: "rails/actionmailbox", require: "action_mailbox" - ``` - -1. Install migrations needed for InboundEmail (and ensure Active Storage is setup) - - ``` - ./bin/rails action_mailbox:install - ./bin/rails db:migrate - ``` - -## Configuring - -### Amazon SES - -1. Install the [`aws-sdk-sns`](https://rubygems.org/gems/aws-sdk-sns) gem: - - ```ruby - # Gemfile - gem "aws-sdk-sns", ">= 1.9.0", require: false - ``` - -2. Tell Action Mailbox to accept emails from SES: - - ```ruby - # config/environments/production.rb - config.action_mailbox.ingress = :amazon - ``` - -3. [Configure SES][ses-docs] to deliver emails to your application via POST requests - to `/rails/action_mailbox/amazon/inbound_emails`. If your application lived at `https://example.com`, you would specify - the fully-qualified URL `https://example.com/rails/action_mailbox/amazon/inbound_emails`. - -[ses-docs]: https://docs.aws.amazon.com/ses/latest/DeveloperGuide/receiving-email-notifications.html - -### Mailgun - -1. Give Action Mailbox your [Mailgun API key][mailgun-api-key] so it can authenticate requests to the Mailgun ingress. - - Use `rails credentials:edit` to add your API key to your application's encrypted credentials under - `action_mailbox.mailgun_api_key`, where Action Mailbox will automatically find it: - - ```yaml - action_mailbox: - mailgun_api_key: ... - ``` - - Alternatively, provide your API key in the `MAILGUN_INGRESS_API_KEY` environment variable. - -2. Tell Action Mailbox to accept emails from Mailgun: - - ```ruby - # config/environments/production.rb - config.action_mailbox.ingress = :mailgun - ``` - -3. [Configure Mailgun][mailgun-forwarding] to forward inbound emails to `/rails/action_mailbox/mailgun/inbound_emails/mime`. - If your application lived at `https://example.com`, you would specify the fully-qualified URL - `https://example.com/rails/action_mailbox/mailgun/inbound_emails/mime`. - -[mailgun-api-key]: https://help.mailgun.com/hc/en-us/articles/203380100-Where-can-I-find-my-API-key-and-SMTP-credentials- -[mailgun-forwarding]: https://documentation.mailgun.com/en/latest/user_manual.html#receiving-forwarding-and-storing-messages - -### Mandrill - -1. Give Action Mailbox your Mandrill API key so it can authenticate requests to the Mandrill ingress. - - Use `rails credentials:edit` to add your API key to your application's encrypted credentials under - `action_mailbox.mandrill_api_key`, where Action Mailbox will automatically find it: - - ```yaml - action_mailbox: - mandrill_api_key: ... - ``` - - Alternatively, provide your API key in the `MANDRILL_INGRESS_API_KEY` environment variable. - -2. Tell Action Mailbox to accept emails from Mandrill: - - ```ruby - # config/environments/production.rb - config.action_mailbox.ingress = :mandrill - ``` - -3. [Configure Mandrill][mandrill-routing] to route inbound emails to `/rails/action_mailbox/mandrill/inbound_emails`. - If your application lived at `https://example.com`, you would specify the fully-qualified URL - `https://example.com/rails/action_mailbox/mandrill/inbound_emails`. - -[mandrill-routing]: https://mandrill.zendesk.com/hc/en-us/articles/205583197-Inbound-Email-Processing-Overview - -### Postfix - -1. Tell Action Mailbox to accept emails from Postfix: - - ```ruby - # config/environments/production.rb - config.action_mailbox.ingress = :postfix - ``` - -2. Generate a strong password that Action Mailbox can use to authenticate requests to the Postfix ingress. - - Use `rails credentials:edit` to add the password to your application's encrypted credentials under - `action_mailbox.ingress_password`, where Action Mailbox will automatically find it: - - ```yaml - action_mailbox: - ingress_password: ... - ``` - - Alternatively, provide the password in the `RAILS_INBOUND_EMAIL_PASSWORD` environment variable. - -3. [Configure Postfix][postfix-config] to pipe inbound emails to `bin/rails action_mailbox:ingress:postfix`, providing - the `URL` of the Postfix ingress and the `INGRESS_PASSWORD` you previously generated. If your application lived at - `https://example.com`, the full command would look like this: - - ``` - URL=https://example.com/rails/action_mailbox/postfix/inbound_emails INGRESS_PASSWORD=... bin/rails action_mailbox:ingress:postfix - ``` - -[postfix-config]: https://serverfault.com/questions/258469/how-to-configure-postfix-to-pipe-all-incoming-email-to-a-script - -### SendGrid - -1. Tell Action Mailbox to accept emails from SendGrid: - - ```ruby - # config/environments/production.rb - config.action_mailbox.ingress = :sendgrid - ``` - -2. Generate a strong password that Action Mailbox can use to authenticate requests to the SendGrid ingress. - - Use `rails credentials:edit` to add the password to your application's encrypted credentials under - `action_mailbox.ingress_password`, where Action Mailbox will automatically find it: - - ```yaml - action_mailbox: - ingress_password: ... - ``` - - Alternatively, provide the password in the `RAILS_INBOUND_EMAIL_PASSWORD` environment variable. - -3. [Configure SendGrid Inbound Parse][sendgrid-config] to forward inbound emails to - `/rails/action_mailbox/sendgrid/inbound_emails` with the username `actionmailbox` and the password you previously - generated. If your application lived at `https://example.com`, you would configure SendGrid with the following URL: - - ``` - https://actionmailbox:PASSWORD@example.com/rails/action_mailbox/sendgrid/inbound_emails - ``` - - **⚠️ Note:** When configuring your SendGrid Inbound Parse webhook, be sure to check the box labeled **“Post the raw, - full MIME message.”** Action Mailbox needs the raw MIME message to work. - -[sendgrid-config]: https://sendgrid.com/docs/for-developers/parsing-email/setting-up-the-inbound-parse-webhook/ - - -## Examples - -Configure basic routing: - -```ruby -# app/mailboxes/application_mailbox.rb -class ApplicationMailbox < ActionMailbox::Base - routing /^save@/i => :forwards - routing /@replies\./i => :replies -end -``` - -Then setup a mailbox: - -```ruby -# Generate new mailbox -bin/rails generate mailbox forwards -``` - -```ruby -# app/mailboxes/forwards_mailbox.rb -class ForwardsMailbox < ApplicationMailbox - # Callbacks specify prerequisites to processing - before_processing :require_forward - - def process - if forwarder.buckets.one? - record_forward - else - stage_forward_and_request_more_details - end - end - - private - def require_forward - unless message.forward? - # Use Action Mailers to bounce incoming emails back to sender – this halts processing - bounce_with Forwards::BounceMailer.missing_forward( - inbound_email, forwarder: forwarder - ) - end - end - - def forwarder - @forwarder ||= Person.where(email_address: mail.from) - end - - def record_forward - forwarder.buckets.first.record \ - Forward.new forwarder: forwarder, subject: message.subject, content: mail.content - end - - def stage_forward_and_request_more_details - Forwards::RoutingMailer.choose_project(mail).deliver_now - end -end -``` - -## Incineration of InboundEmails - -By default, an InboundEmail that has been successfully processed will be incinerated after 30 days. This ensures you're not holding on to people's data willy-nilly after they may have canceled their accounts or deleted their content. The intention is that after you've processed an email, you should have extracted all the data you needed and turned it into domain models and content on your side of the application. The InboundEmail simply stays in the system for the extra time to provide debugging and forensics options. - -The actual incineration is done via the `IncinerationJob` that's scheduled to run after `config.action_mailbox.incinerate_after` time. This value is by default set to `30.days`, but you can change it in your production.rb configuration. (Note that this far-future incineration scheduling relies on your job queue being able to hold jobs for that long.) - - -## Working with Action Mailbox in development - -It's helpful to be able to test incoming emails in development without actually sending and receiving real emails. To accomplish this, there's a conductor controller mounted at `/rails/conductor/action_mailbox/inbound_emails`, which gives you an index of all the InboundEmails in the system, their state of processing, and a form to create a new InboundEmail as well. - - -## Testing mailboxes - -Example: - -```ruby -class ForwardsMailboxTest < ActionMailbox::TestCase - test "directly recording a client forward for a forwarder and forwardee corresponding to one project" do - assert_difference -> { people(:david).buckets.first.recordings.count } do - receive_inbound_email_from_mail \ - to: 'save@example.com', - from: people(:david).email_address, - subject: "Fwd: Status update?", - body: <<~BODY - --- Begin forwarded message --- - From: Frank Holland <frank@microsoft.com> - - What's the status? - BODY - end - - recording = people(:david).buckets.first.recordings.last - assert_equal people(:david), recording.creator - assert_equal "Status update?", recording.forward.subject - assert_match "What's the status?", recording.forward.content.to_s - end -end -``` - +You can read more about Action Mailbox in the [Action Mailbox Basics](https://edgeguides.rubyonrails.org/action_mailbox_basics.html) guide. ## License diff --git a/actionmailbox/actionmailbox.gemspec b/actionmailbox/actionmailbox.gemspec index 5e6ee72ff8..c38857f6d7 100644 --- a/actionmailbox/actionmailbox.gemspec +++ b/actionmailbox/actionmailbox.gemspec @@ -17,7 +17,7 @@ Gem::Specification.new do |s| s.email = ["david@loudthinking.com", "george@basecamp.com"] s.homepage = "https://rubyonrails.org" - s.files = Dir["MIT-LICENSE", "README.md", "lib/**/*", "app/**/*", "config/**/*", "db/**/*"] + s.files = Dir["CHANGELOG.md", "MIT-LICENSE", "README.md", "lib/**/*", "app/**/*", "config/**/*", "db/**/*"] s.require_path = "lib" s.metadata = { diff --git a/actionmailbox/app/controllers/action_mailbox/base_controller.rb b/actionmailbox/app/controllers/action_mailbox/base_controller.rb index cc22ea803c..f0f1f555e6 100644 --- a/actionmailbox/app/controllers/action_mailbox/base_controller.rb +++ b/actionmailbox/app/controllers/action_mailbox/base_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module ActionMailbox - # The base class for all Active Mailbox ingress controllers. + # The base class for all Action Mailbox ingress controllers. class BaseController < ActionController::Base skip_forgery_protection diff --git a/actionmailbox/app/controllers/action_mailbox/ingresses/postfix/inbound_emails_controller.rb b/actionmailbox/app/controllers/action_mailbox/ingresses/postfix/inbound_emails_controller.rb index 1871893e99..c0d5002a12 100644 --- a/actionmailbox/app/controllers/action_mailbox/ingresses/postfix/inbound_emails_controller.rb +++ b/actionmailbox/app/controllers/action_mailbox/ingresses/postfix/inbound_emails_controller.rb @@ -35,7 +35,7 @@ module ActionMailbox # # Alternatively, provide the password in the +RAILS_INBOUND_EMAIL_PASSWORD+ environment variable. # - # 3. {Configure Postfix}{https://serverfault.com/questions/258469/how-to-configure-postfix-to-pipe-all-incoming-email-to-a-script} + # 3. {Configure Postfix}[https://serverfault.com/questions/258469/how-to-configure-postfix-to-pipe-all-incoming-email-to-a-script] # to pipe inbound emails to <tt>bin/rails action_mailbox:ingress:postfix</tt>, providing the +URL+ of the Postfix # ingress and the +INGRESS_PASSWORD+ you previously generated. # diff --git a/actionmailbox/app/controllers/action_mailbox/ingresses/sendgrid/inbound_emails_controller.rb b/actionmailbox/app/controllers/action_mailbox/ingresses/sendgrid/inbound_emails_controller.rb index ec4111fefa..58da33d85e 100644 --- a/actionmailbox/app/controllers/action_mailbox/ingresses/sendgrid/inbound_emails_controller.rb +++ b/actionmailbox/app/controllers/action_mailbox/ingresses/sendgrid/inbound_emails_controller.rb @@ -35,7 +35,7 @@ module ActionMailbox # # Alternatively, provide the password in the +RAILS_INBOUND_EMAIL_PASSWORD+ environment variable. # - # 3. {Configure SendGrid Inbound Parse}{https://sendgrid.com/docs/for-developers/parsing-email/setting-up-the-inbound-parse-webhook/} + # 3. {Configure SendGrid Inbound Parse}[https://sendgrid.com/docs/for-developers/parsing-email/setting-up-the-inbound-parse-webhook/] # to forward inbound emails to +/rails/action_mailbox/sendgrid/inbound_emails+ with the username +actionmailbox+ and # the password you previously generated. If your application lived at <tt>https://example.com</tt>, you would # configure SendGrid with the following fully-qualified URL: diff --git a/actionmailbox/app/models/action_mailbox/inbound_email.rb b/actionmailbox/app/models/action_mailbox/inbound_email.rb index fbf75a6080..3a8dfd163c 100644 --- a/actionmailbox/app/models/action_mailbox/inbound_email.rb +++ b/actionmailbox/app/models/action_mailbox/inbound_email.rb @@ -45,3 +45,5 @@ module ActionMailbox end end end + +ActiveSupport.run_load_hooks :action_mailbox_inbound_email, ActionMailbox::InboundEmail diff --git a/actionmailbox/db/migrate/20180917164000_create_action_mailbox_tables.rb b/actionmailbox/db/migrate/20180917164000_create_action_mailbox_tables.rb index 85e19bc3c6..0124b4b98f 100644 --- a/actionmailbox/db/migrate/20180917164000_create_action_mailbox_tables.rb +++ b/actionmailbox/db/migrate/20180917164000_create_action_mailbox_tables.rb @@ -1,4 +1,4 @@ -class CreateActionMailboxTables < ActiveRecord::Migration[5.2] +class CreateActionMailboxTables < ActiveRecord::Migration[6.0] def change create_table :action_mailbox_inbound_emails do |t| t.integer :status, default: 0, null: false diff --git a/actionmailbox/lib/action_mailbox/base.rb b/actionmailbox/lib/action_mailbox/base.rb index 76c3a4886c..4ac594b9f8 100644 --- a/actionmailbox/lib/action_mailbox/base.rb +++ b/actionmailbox/lib/action_mailbox/base.rb @@ -77,7 +77,7 @@ module ActionMailbox @inbound_email = inbound_email end - def perform_processing + def perform_processing #:nodoc: track_status_of_inbound_email do run_callbacks :process do process @@ -92,11 +92,12 @@ module ActionMailbox # Overwrite in subclasses end - def finished_processing? + def finished_processing? #:nodoc: inbound_email.delivered? || inbound_email.bounced? end + # Enqueues the given +message+ for delivery and changes the inbound email's status to +:bounced+. def bounce_with(message) inbound_email.bounced! message.deliver_later @@ -113,3 +114,5 @@ module ActionMailbox end end end + +ActiveSupport.run_load_hooks :action_mailbox, ActionMailbox::Base diff --git a/actionmailbox/lib/action_mailbox/engine.rb b/actionmailbox/lib/action_mailbox/engine.rb index 0400469ff7..d5a07a7dce 100644 --- a/actionmailbox/lib/action_mailbox/engine.rb +++ b/actionmailbox/lib/action_mailbox/engine.rb @@ -1,6 +1,11 @@ # frozen_string_literal: true -require "rails/engine" +require "rails" +require "action_controller/railtie" +require "active_job/railtie" +require "active_record/railtie" +require "active_storage/engine" + require "action_mailbox" module ActionMailbox diff --git a/actionmailbox/lib/action_mailbox/test_case.rb b/actionmailbox/lib/action_mailbox/test_case.rb index a501e8a7ca..5e78e428d3 100644 --- a/actionmailbox/lib/action_mailbox/test_case.rb +++ b/actionmailbox/lib/action_mailbox/test_case.rb @@ -8,3 +8,5 @@ module ActionMailbox include ActionMailbox::TestHelper end end + +ActiveSupport.run_load_hooks :action_mailbox_test_case, ActionMailbox::TestCase diff --git a/actionmailbox/lib/rails/generators/mailbox/templates/application_mailbox.rb.tt b/actionmailbox/lib/rails/generators/mailbox/templates/application_mailbox.rb.tt index be51eb3639..ac22d03cd2 100644 --- a/actionmailbox/lib/rails/generators/mailbox/templates/application_mailbox.rb.tt +++ b/actionmailbox/lib/rails/generators/mailbox/templates/application_mailbox.rb.tt @@ -1,5 +1,3 @@ -# frozen_string_literal: true - class ApplicationMailbox < ActionMailbox::Base # routing /something/i => :somewhere end diff --git a/actionmailbox/lib/rails/generators/mailbox/templates/mailbox.rb.tt b/actionmailbox/lib/rails/generators/mailbox/templates/mailbox.rb.tt index 56b138e2d9..110b3b9d7e 100644 --- a/actionmailbox/lib/rails/generators/mailbox/templates/mailbox.rb.tt +++ b/actionmailbox/lib/rails/generators/mailbox/templates/mailbox.rb.tt @@ -1,5 +1,3 @@ -# frozen_string_literal: true - class <%= class_name %>Mailbox < ApplicationMailbox def process end diff --git a/actionmailbox/test/dummy/config/application.rb b/actionmailbox/test/dummy/config/application.rb index ffba407fd7..ea8ab1fc5e 100644 --- a/actionmailbox/test/dummy/config/application.rb +++ b/actionmailbox/test/dummy/config/application.rb @@ -7,7 +7,7 @@ Bundler.require(*Rails.groups) module Dummy class Application < Rails::Application # Initialize configuration defaults for originally generated Rails version. - config.load_defaults 5.2 + config.load_defaults 6.0 # Settings in config/environments/* take precedence over those specified here. # Application configuration can go into files in config/initializers diff --git a/actionmailbox/test/dummy/db/migrate/20180208205311_create_action_mailroom_tables.rb b/actionmailbox/test/dummy/db/migrate/20180208205311_create_action_mailroom_tables.rb index 85e19bc3c6..0124b4b98f 100644 --- a/actionmailbox/test/dummy/db/migrate/20180208205311_create_action_mailroom_tables.rb +++ b/actionmailbox/test/dummy/db/migrate/20180208205311_create_action_mailroom_tables.rb @@ -1,4 +1,4 @@ -class CreateActionMailboxTables < ActiveRecord::Migration[5.2] +class CreateActionMailboxTables < ActiveRecord::Migration[6.0] def change create_table :action_mailbox_inbound_emails do |t| t.integer :status, default: 0, null: false diff --git a/actionmailer/MIT-LICENSE b/actionmailer/MIT-LICENSE index 1cb3add0fc..ab7c27c209 100644 --- a/actionmailer/MIT-LICENSE +++ b/actionmailer/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2018 David Heinemeier Hansson +Copyright (c) 2004-2019 David Heinemeier Hansson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/actionmailer/lib/action_mailer.rb b/actionmailer/lib/action_mailer.rb index 48f16180be..953c5fb508 100644 --- a/actionmailer/lib/action_mailer.rb +++ b/actionmailer/lib/action_mailer.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true #-- -# Copyright (c) 2004-2018 David Heinemeier Hansson +# Copyright (c) 2004-2019 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/actionmailer/test/log_subscriber_test.rb b/actionmailer/test/log_subscriber_test.rb index e984dcdbdb..fb569ce45f 100644 --- a/actionmailer/test/log_subscriber_test.rb +++ b/actionmailer/test/log_subscriber_test.rb @@ -3,11 +3,10 @@ require "abstract_unit" require "mailers/base_mailer" require "active_support/log_subscriber/test_helper" -require "active_support/testing/stream" require "action_mailer/log_subscriber" class AMLogSubscriberTest < ActionMailer::TestCase - include ActiveSupport::LogSubscriber::TestHelper, ActiveSupport::Testing::Stream + include ActiveSupport::LogSubscriber::TestHelper def setup super @@ -54,7 +53,9 @@ class AMLogSubscriberTest < ActionMailer::TestCase def test_receive_is_notified fixture = File.read(File.expand_path("fixtures/raw_email", __dir__)) - silence_stream(STDERR) { TestMailer.receive(fixture) } + assert_deprecated do + TestMailer.receive(fixture) + end wait assert_equal(1, @logger.logged(:info).size) assert_match(/Received mail/, @logger.logged(:info).first) diff --git a/actionpack/MIT-LICENSE b/actionpack/MIT-LICENSE index 1cb3add0fc..ab7c27c209 100644 --- a/actionpack/MIT-LICENSE +++ b/actionpack/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2018 David Heinemeier Hansson +Copyright (c) 2004-2019 David Heinemeier Hansson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb index a643484d96..94ccd48203 100644 --- a/actionpack/lib/action_controller/test_case.rb +++ b/actionpack/lib/action_controller/test_case.rb @@ -276,9 +276,6 @@ module ActionController # after calling +post+. If the various assert methods are not sufficient, then you # may use this object to inspect the HTTP response in detail. # - # (Earlier versions of \Rails required each functional test to subclass - # Test::Unit::TestCase and define @controller, @request, @response in +setup+.) - # # == Controller is automatically inferred # # ActionController::TestCase will automatically infer the controller under test diff --git a/actionpack/lib/action_dispatch.rb b/actionpack/lib/action_dispatch.rb index a92d336214..8f39b88d56 100644 --- a/actionpack/lib/action_dispatch.rb +++ b/actionpack/lib/action_dispatch.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true #-- -# Copyright (c) 2004-2018 David Heinemeier Hansson +# Copyright (c) 2004-2019 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/actionpack/lib/action_pack.rb b/actionpack/lib/action_pack.rb index 3f69109633..36ee77c693 100644 --- a/actionpack/lib/action_pack.rb +++ b/actionpack/lib/action_pack.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true #-- -# Copyright (c) 2004-2018 David Heinemeier Hansson +# Copyright (c) 2004-2019 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/actionview/MIT-LICENSE b/actionview/MIT-LICENSE index 1cb3add0fc..ab7c27c209 100644 --- a/actionview/MIT-LICENSE +++ b/actionview/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2018 David Heinemeier Hansson +Copyright (c) 2004-2019 David Heinemeier Hansson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/actionview/app/assets/javascripts/MIT-LICENSE b/actionview/app/assets/javascripts/MIT-LICENSE index 28e1b12496..03319ea365 100644 --- a/actionview/app/assets/javascripts/MIT-LICENSE +++ b/actionview/app/assets/javascripts/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2007-2018 Rails Core team +Copyright (c) 2007-2019 Rails Core team Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/actionview/lib/action_view.rb b/actionview/lib/action_view.rb index c1eeda75f5..c4a614fa6d 100644 --- a/actionview/lib/action_view.rb +++ b/actionview/lib/action_view.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true #-- -# Copyright (c) 2004-2018 David Heinemeier Hansson +# Copyright (c) 2004-2019 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/activejob/MIT-LICENSE b/activejob/MIT-LICENSE index 274211f710..aedc21bca2 100644 --- a/activejob/MIT-LICENSE +++ b/activejob/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2014-2018 David Heinemeier Hansson +Copyright (c) 2014-2019 David Heinemeier Hansson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/activejob/lib/active_job.rb b/activejob/lib/active_job.rb index 01fab4d918..5f20ef9b9f 100644 --- a/activejob/lib/active_job.rb +++ b/activejob/lib/active_job.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true #-- -# Copyright (c) 2014-2018 David Heinemeier Hansson +# Copyright (c) 2014-2019 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/activemodel/MIT-LICENSE b/activemodel/MIT-LICENSE index 1cb3add0fc..ab7c27c209 100644 --- a/activemodel/MIT-LICENSE +++ b/activemodel/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2018 David Heinemeier Hansson +Copyright (c) 2004-2019 David Heinemeier Hansson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/activemodel/lib/active_model.rb b/activemodel/lib/active_model.rb index bc10d6b4b9..c9140dc582 100644 --- a/activemodel/lib/active_model.rb +++ b/activemodel/lib/active_model.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true #-- -# Copyright (c) 2004-2018 David Heinemeier Hansson +# Copyright (c) 2004-2019 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/activerecord/MIT-LICENSE b/activerecord/MIT-LICENSE index 04ba107c48..79e52c53af 100644 --- a/activerecord/MIT-LICENSE +++ b/activerecord/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2018 David Heinemeier Hansson +Copyright (c) 2004-2019 David Heinemeier Hansson Arel originally copyright (c) 2007-2016 Nick Kallen, Bryan Helmkamp, Emilio Tagua, Aaron Patterson diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index d43378c64f..de94f9693f 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true #-- -# Copyright (c) 2004-2018 David Heinemeier Hansson +# Copyright (c) 2004-2019 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index 1243236c09..346d4b067a 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -146,7 +146,7 @@ module ActiveRecord replica? || prevent_writes end - # Prevent writing to the database regardless of role. + # Prevent writing to the database regardless of role. # # In some cases you may want to prevent writes to the database # even if you are on a database that can write. `while_preventing_writes` diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb index dbc6614b93..10961ed9c8 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -97,19 +97,11 @@ module ActiveRecord end def supports_datetime_with_precision? - if mariadb? - version >= "5.3.0" - else - version >= "5.6.4" - end + mariadb? || version >= "5.6.4" end def supports_virtual_columns? - if mariadb? - version >= "5.2.0" - else - version >= "5.7.5" - end + mariadb? || version >= "5.7.5" end def supports_advisory_locks? diff --git a/activerecord/lib/active_record/connection_adapters/schema_cache.rb b/activerecord/lib/active_record/connection_adapters/schema_cache.rb index c29cf1f9a1..c10765f42d 100644 --- a/activerecord/lib/active_record/connection_adapters/schema_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/schema_cache.rb @@ -77,6 +77,11 @@ module ActiveRecord }] end + # Checks whether the columns hash is already cached for a table. + def columns_hash?(table_name) + @columns_hash.key?(table_name) + end + # Clears out internal caches def clear! @columns.clear diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index 8f4d292a4b..600825659b 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -282,6 +282,10 @@ module ActiveRecord TypeCaster::Map.new(self) end + def _internal? # :nodoc: + false + end + private def cached_find_by_statement(key, &block) @@ -350,9 +354,7 @@ module ActiveRecord # Initialize an empty model object from +attributes+. # +attributes+ should be an attributes object, and unlike the # `initialize` method, no assignment calls are made per attribute. - # - # :nodoc: - def init_with_attributes(attributes, new_record = false) + def init_with_attributes(attributes, new_record = false) # :nodoc: init_internals @new_record = new_record diff --git a/activerecord/lib/active_record/internal_metadata.rb b/activerecord/lib/active_record/internal_metadata.rb index 3626a13d7c..88b0c828ae 100644 --- a/activerecord/lib/active_record/internal_metadata.rb +++ b/activerecord/lib/active_record/internal_metadata.rb @@ -8,6 +8,10 @@ module ActiveRecord # as which environment migrations were run in. class InternalMetadata < ActiveRecord::Base # :nodoc: class << self + def _internal? + true + end + def primary_key "key" end diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 7bf8d568df..c2b60610ce 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -96,11 +96,13 @@ module ActiveRecord # When running callbacks is not needed for each record update, # it is preferred to use {update_all}[rdoc-ref:Relation#update_all] # for updating all records in a single query. - def update(id, attributes) + def update(id = :all, attributes) if id.is_a?(Array) id.map { |one_id| find(one_id) }.each_with_index { |object, idx| object.update(attributes[idx]) } + elsif id == :all + all.each { |record| record.update(attributes) } else if ActiveRecord::Base === id raise ArgumentError, diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index 538659d6bd..2ee6119158 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -140,7 +140,19 @@ end_error initializer "active_record.define_attribute_methods" do |app| config.after_initialize do ActiveSupport.on_load(:active_record) do - descendants.each(&:define_attribute_methods) if app.config.eager_load + if app.config.eager_load + descendants.each do |model| + # SchemaMigration and InternalMetadata both override `table_exists?` + # to bypass the schema cache, so skip them to avoid the extra queries. + next if model._internal? + + # If there's no connection yet, or the schema cache doesn't have the columns + # hash for the model cached, `define_attribute_methods` would trigger a query. + next unless model.connected? && model.connection.schema_cache.columns_hash?(model.table_name) + + model.define_attribute_methods + end + end end end end diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index 0fa5ba2e50..3ef6e7928f 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -245,7 +245,7 @@ module ActiveRecord if distinct && (group_values.any? || select_values.empty? && order_values.empty?) column_name = primary_key end - elsif /\s*DISTINCT[\s(]+/i.match?(column_name.to_s) + elsif column_name.is_a?(::String) && /\bDISTINCT[\s(]/i.match?(column_name) distinct = nil end end diff --git a/activerecord/lib/active_record/schema_migration.rb b/activerecord/lib/active_record/schema_migration.rb index f2d8b038fa..1fca1a18f6 100644 --- a/activerecord/lib/active_record/schema_migration.rb +++ b/activerecord/lib/active_record/schema_migration.rb @@ -10,6 +10,10 @@ module ActiveRecord # to be executed the next time. class SchemaMigration < ActiveRecord::Base # :nodoc: class << self + def _internal? + true + end + def primary_key "version" end diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb index 66e594d771..05d8aa59c4 100644 --- a/activerecord/test/cases/adapter_test.rb +++ b/activerecord/test/cases/adapter_test.rb @@ -163,6 +163,16 @@ module ActiveRecord end end + def test_preventing_writes_predicate + assert_not_predicate @connection, :preventing_writes? + + @connection.while_preventing_writes do + assert_predicate @connection, :preventing_writes? + end + + assert_not_predicate @connection, :preventing_writes? + end + def test_errors_when_an_insert_query_is_called_while_preventing_writes assert_no_queries do assert_raises(ActiveRecord::ReadOnlyError) do diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb index 4d3db912c5..5b5202d167 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -432,6 +432,8 @@ class CalculationsTest < ActiveRecord::TestCase def test_should_count_selected_field_with_include assert_equal 6, Account.includes(:firm).distinct.count assert_equal 4, Account.includes(:firm).distinct.select(:credit_limit).count + assert_equal 4, Account.includes(:firm).distinct.count("DISTINCT credit_limit") + assert_equal 4, Account.includes(:firm).distinct.count("DISTINCT(credit_limit)") end def test_should_not_perform_joined_include_by_default diff --git a/activerecord/test/cases/connection_adapters/connection_handlers_multi_db_test.rb b/activerecord/test/cases/connection_adapters/connection_handlers_multi_db_test.rb index 0b3fb82e12..865aacc1b5 100644 --- a/activerecord/test/cases/connection_adapters/connection_handlers_multi_db_test.rb +++ b/activerecord/test/cases/connection_adapters/connection_handlers_multi_db_test.rb @@ -108,6 +108,7 @@ module ActiveRecord ActiveRecord::Base.connected_to(role: :reading) do @ro_handler = ActiveRecord::Base.connection_handler assert_equal ActiveRecord::Base.connection_handler, ActiveRecord::Base.connection_handlers[:reading] + assert_equal :reading, ActiveRecord::Base.current_role assert ActiveRecord::Base.connected_to?(role: :reading) assert_not ActiveRecord::Base.connected_to?(role: :writing) end @@ -115,6 +116,7 @@ module ActiveRecord ActiveRecord::Base.connected_to(role: :writing) do assert_equal ActiveRecord::Base.connection_handler, ActiveRecord::Base.connection_handlers[:writing] assert_not_equal @ro_handler, ActiveRecord::Base.connection_handler + assert_equal :writing, ActiveRecord::Base.current_role assert ActiveRecord::Base.connected_to?(role: :writing) assert_not ActiveRecord::Base.connected_to?(role: :reading) end @@ -129,6 +131,7 @@ module ActiveRecord previous_url, ENV["DATABASE_URL"] = ENV["DATABASE_URL"], "postgres://localhost/foo" ActiveRecord::Base.connected_to(database: { writing: "postgres://localhost/bar" }) do + assert_equal :writing, ActiveRecord::Base.current_role assert ActiveRecord::Base.connected_to?(role: :writing) handler = ActiveRecord::Base.connection_handler @@ -148,6 +151,7 @@ module ActiveRecord config = { adapter: "sqlite3", database: "db/readonly.sqlite3" } ActiveRecord::Base.connected_to(database: { writing: config }) do + assert_equal :writing, ActiveRecord::Base.current_role assert ActiveRecord::Base.connected_to?(role: :writing) handler = ActiveRecord::Base.connection_handler @@ -187,6 +191,7 @@ module ActiveRecord @prev_configs, ActiveRecord::Base.configurations = ActiveRecord::Base.configurations, config ActiveRecord::Base.connected_to(database: :readonly) do + assert_equal :readonly, ActiveRecord::Base.current_role assert ActiveRecord::Base.connected_to?(role: :readonly) handler = ActiveRecord::Base.connection_handler @@ -211,6 +216,7 @@ module ActiveRecord assert_equal 1, ActiveRecord::Base.connection_handlers.size assert_equal ActiveRecord::Base.connection_handler, ActiveRecord::Base.connection_handlers[:writing] + assert_equal :writing, ActiveRecord::Base.current_role assert ActiveRecord::Base.connected_to?(role: :writing) ensure ActiveRecord::Base.configurations = @prev_configs diff --git a/activerecord/test/cases/connection_adapters/schema_cache_test.rb b/activerecord/test/cases/connection_adapters/schema_cache_test.rb index 67496381d1..727cab77f5 100644 --- a/activerecord/test/cases/connection_adapters/schema_cache_test.rb +++ b/activerecord/test/cases/connection_adapters/schema_cache_test.rb @@ -91,6 +91,22 @@ module ActiveRecord @cache.clear_data_source_cache!("posts") end + test "#columns_hash? is populated by #columns_hash" do + assert_not @cache.columns_hash?("posts") + + @cache.columns_hash("posts") + + assert @cache.columns_hash?("posts") + end + + test "#columns_hash? is not populated by #data_source_exists?" do + assert_not @cache.columns_hash?("posts") + + @cache.data_source_exists?("posts") + + assert_not @cache.columns_hash?("posts") + end + private def schema_dump_path diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb index 4830ff2b5f..d5057ad381 100644 --- a/activerecord/test/cases/persistence_test.rb +++ b/activerecord/test/cases/persistence_test.rb @@ -53,6 +53,20 @@ class PersistenceTest < ActiveRecord::TestCase assert_not_equal "2 updated", Topic.find(2).content end + def test_class_level_update_without_ids + topics = Topic.all + assert_equal 5, topics.length + topics.each do |topic| + assert_not_equal "updated", topic.content + end + + updated = Topic.update(content: "updated") + assert_equal 5, updated.length + updated.each do |topic| + assert_equal "updated", topic.content + end + end + def test_class_level_update_is_affected_by_scoping topic_data = { 1 => { "content" => "1 updated" }, 2 => { "content" => "2 updated" } } diff --git a/activerecord/test/cases/reaper_test.rb b/activerecord/test/cases/reaper_test.rb index b630f782bc..402ddcf05a 100644 --- a/activerecord/test/cases/reaper_test.rb +++ b/activerecord/test/cases/reaper_test.rb @@ -48,7 +48,7 @@ module ActiveRecord reaper = ConnectionPool::Reaper.new(fp, 0.0001) reaper.run - until fp.reaped + until fp.flushed Thread.pass end assert fp.reaped diff --git a/activestorage/CHANGELOG.md b/activestorage/CHANGELOG.md index 51890f308b..fbc78a50ab 100644 --- a/activestorage/CHANGELOG.md +++ b/activestorage/CHANGELOG.md @@ -1,3 +1,24 @@ +* Replace `config.active_storage.queue` with two options that indicate which + queues analysis and purge jobs should use, respectively: + + * `config.active_storage.queues.analysis` + * `config.active_storage.queues.purge` + + `config.active_storage.queue` is preferred over the new options when it's + set, but it is deprecated and will be removed in Rails 6.1. + + *George Claghorn* + +* Permit generating variants of TIFF images. + + *Luciano Sousa* + +* Use base36 (all lowercase) for all new Blob keys to prevent + collisions and undefined behavior with case-insensitive filesystems and + database indices. + + *Julik Tarkhanov* + * It doesn’t include an `X-CSRF-Token` header if a meta tag is not found on the page. It previously included one with a value of `undefined`. diff --git a/activestorage/MIT-LICENSE b/activestorage/MIT-LICENSE index eed89ac398..771376cc7e 100644 --- a/activestorage/MIT-LICENSE +++ b/activestorage/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2017-2018 David Heinemeier Hansson, Basecamp +Copyright (c) 2017-2019 David Heinemeier Hansson, Basecamp Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/activestorage/app/jobs/active_storage/analyze_job.rb b/activestorage/app/jobs/active_storage/analyze_job.rb index 804ee4557a..35d043d508 100644 --- a/activestorage/app/jobs/active_storage/analyze_job.rb +++ b/activestorage/app/jobs/active_storage/analyze_job.rb @@ -2,6 +2,8 @@ # Provides asynchronous analysis of ActiveStorage::Blob records via ActiveStorage::Blob#analyze_later. class ActiveStorage::AnalyzeJob < ActiveStorage::BaseJob + queue_as { ActiveStorage.queues[:analysis] } + retry_on ActiveStorage::IntegrityError, attempts: 10, wait: :exponentially_longer def perform(blob) diff --git a/activestorage/app/jobs/active_storage/base_job.rb b/activestorage/app/jobs/active_storage/base_job.rb index 6caab42a2d..7bc2064dc5 100644 --- a/activestorage/app/jobs/active_storage/base_job.rb +++ b/activestorage/app/jobs/active_storage/base_job.rb @@ -1,5 +1,4 @@ # frozen_string_literal: true class ActiveStorage::BaseJob < ActiveJob::Base - queue_as { ActiveStorage.queue } end diff --git a/activestorage/app/jobs/active_storage/purge_job.rb b/activestorage/app/jobs/active_storage/purge_job.rb index 2604977bf1..5ceb222005 100644 --- a/activestorage/app/jobs/active_storage/purge_job.rb +++ b/activestorage/app/jobs/active_storage/purge_job.rb @@ -2,6 +2,8 @@ # Provides asynchronous purging of ActiveStorage::Blob records via ActiveStorage::Blob#purge_later. class ActiveStorage::PurgeJob < ActiveStorage::BaseJob + queue_as { ActiveStorage.queues[:purge] } + discard_on ActiveRecord::RecordNotFound retry_on ActiveRecord::Deadlocked, attempts: 10, wait: :exponentially_longer diff --git a/activestorage/app/models/active_storage/blob.rb b/activestorage/app/models/active_storage/blob.rb index 04f9dbff9f..6ca7d49bc1 100644 --- a/activestorage/app/models/active_storage/blob.rb +++ b/activestorage/app/models/active_storage/blob.rb @@ -79,6 +79,15 @@ class ActiveStorage::Blob < ActiveRecord::Base def create_before_direct_upload!(filename:, byte_size:, checksum:, content_type: nil, metadata: nil) create! filename: filename, byte_size: byte_size, checksum: checksum, content_type: content_type, metadata: metadata end + + # To prevent problems with case-insensitive filesystems, especially in combination + # with databases which treat indices as case-sensitive, all blob keys generated are going + # to only contain the base-36 character alphabet and will therefore be lowercase. To maintain + # the same or higher amount of entropy as in the base-58 encoding used by `has_secure_token` + # the number of bytes used is increased to 28 from the standard 24 + def generate_unique_secure_token + SecureRandom.base36(28) + end end # Returns a signed ID for this blob that's suitable for reference on the client-side without fear of tampering. @@ -87,9 +96,10 @@ class ActiveStorage::Blob < ActiveRecord::Base ActiveStorage.verifier.generate(id, purpose: :blob_id) end - # Returns the key pointing to the file on the service that's associated with this blob. The key is in the - # standard secure-token format from Rails. So it'll look like: XTAPjJCJiuDrLk3TmwyJGpUo. This key is not intended - # to be revealed directly to the user. Always refer to blobs using the signed_id or a verified form of the key. + # Returns the key pointing to the file on the service that's associated with this blob. The key is the + # secure-token format from Rails in lower case. So it'll look like: xtapjjcjiudrlk3tmwyjgpuobabd. + # This key is not intended to be revealed directly to the user. + # Always refer to blobs using the signed_id or a verified form of the key. def key # We can't wait until the record is first saved to have a key for it self[:key] ||= self.class.generate_unique_secure_token diff --git a/activestorage/lib/active_storage.rb b/activestorage/lib/active_storage.rb index 19c7f6be2a..e542c4b2ca 100644 --- a/activestorage/lib/active_storage.rb +++ b/activestorage/lib/active_storage.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true #-- -# Copyright (c) 2017-2018 David Heinemeier Hansson, Basecamp +# Copyright (c) 2017-2019 David Heinemeier Hansson, Basecamp # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -42,7 +42,7 @@ module ActiveStorage mattr_accessor :logger mattr_accessor :verifier - mattr_accessor :queue + mattr_accessor :queues, default: {} mattr_accessor :previewers, default: [] mattr_accessor :analyzers, default: [] mattr_accessor :variant_processor, default: :mini_magick diff --git a/activestorage/lib/active_storage/engine.rb b/activestorage/lib/active_storage/engine.rb index b3bcf48d6f..384e6ebfa6 100644 --- a/activestorage/lib/active_storage/engine.rb +++ b/activestorage/lib/active_storage/engine.rb @@ -20,6 +20,7 @@ module ActiveStorage config.active_storage.previewers = [ ActiveStorage::Previewer::PopplerPDFPreviewer, ActiveStorage::Previewer::MuPDFPreviewer, ActiveStorage::Previewer::VideoPreviewer ] config.active_storage.analyzers = [ ActiveStorage::Analyzer::ImageAnalyzer, ActiveStorage::Analyzer::VideoAnalyzer ] config.active_storage.paths = ActiveSupport::OrderedOptions.new + config.active_storage.queues = ActiveSupport::OrderedOptions.new config.active_storage.variable_content_types = %w( image/png @@ -27,6 +28,7 @@ module ActiveStorage image/jpg image/jpeg image/pjpeg + image/tiff image/vnd.adobe.photoshop image/vnd.microsoft.icon ) @@ -49,6 +51,7 @@ module ActiveStorage image/gif image/jpg image/jpeg + image/tiff image/vnd.adobe.photoshop image/vnd.microsoft.icon application/pdf @@ -59,7 +62,6 @@ module ActiveStorage initializer "active_storage.configs" do config.after_initialize do |app| ActiveStorage.logger = app.config.active_storage.logger || Rails.logger - ActiveStorage.queue = app.config.active_storage.queue ActiveStorage.variant_processor = app.config.active_storage.variant_processor || :mini_magick ActiveStorage.previewers = app.config.active_storage.previewers || [] ActiveStorage.analyzers = app.config.active_storage.analyzers || [] @@ -115,6 +117,20 @@ module ActiveStorage end end + initializer "active_storage.queues" do + config.after_initialize do |app| + if queue = app.config.active_storage.queue + ActiveSupport::Deprecation.warn \ + "config.active_storage.queue is deprecated and will be removed in Rails 6.1. " \ + "Set config.active_storage.queues.purge and config.active_storage.queues.analysis instead." + + ActiveStorage.queues = { purge: queue, analysis: queue } + else + ActiveStorage.queues = app.config.active_storage.queues || {} + end + end + end + initializer "active_storage.reflection" do ActiveSupport.on_load(:active_record) do include Reflection::ActiveRecordExtensions diff --git a/activestorage/test/dummy/config/application.rb b/activestorage/test/dummy/config/application.rb index bd14ac0b1a..151c8ade4b 100644 --- a/activestorage/test/dummy/config/application.rb +++ b/activestorage/test/dummy/config/application.rb @@ -15,7 +15,7 @@ Bundler.require(*Rails.groups) module Dummy class Application < Rails::Application - config.load_defaults 5.2 + config.load_defaults 6.0 config.active_storage.service = :local end diff --git a/activestorage/test/fixtures/files/racecar.tif b/activestorage/test/fixtures/files/racecar.tif Binary files differnew file mode 100644 index 0000000000..0a11b22896 --- /dev/null +++ b/activestorage/test/fixtures/files/racecar.tif diff --git a/activestorage/test/models/blob_test.rb b/activestorage/test/models/blob_test.rb index 1503f5fc50..54cf9e2b8a 100644 --- a/activestorage/test/models/blob_test.rb +++ b/activestorage/test/models/blob_test.rb @@ -47,6 +47,10 @@ class ActiveStorage::BlobTest < ActiveSupport::TestCase assert_equal "text/csv", blob.content_type end + test "create after upload generates a 28-character base36 key" do + assert_match(/^[a-z0-9]{28}$/, create_blob.key) + end + test "image?" do blob = create_file_blob filename: "racecar.jpg" assert_predicate blob, :image? diff --git a/activestorage/test/models/variant_test.rb b/activestorage/test/models/variant_test.rb index 4f88440e54..d98935eb9f 100644 --- a/activestorage/test/models/variant_test.rb +++ b/activestorage/test/models/variant_test.rb @@ -133,6 +133,17 @@ class ActiveStorage::VariantTest < ActiveSupport::TestCase assert_equal 20, image.height end + test "resized variation of TIFF blob" do + blob = create_file_blob(filename: "racecar.tif") + variant = blob.variant(resize: "50x50").processed + assert_match(/racecar\.png/, variant.service_url) + + image = read_image(variant) + assert_equal "PNG", image.type + assert_equal 50, image.width + assert_equal 33, image.height + end + test "optimized variation of GIF blob" do blob = create_file_blob(filename: "image.gif", content_type: "image/gif") @@ -150,7 +161,7 @@ class ActiveStorage::VariantTest < ActiveSupport::TestCase test "service_url doesn't grow in length despite long variant options" do blob = create_file_blob(filename: "racecar.jpg") variant = blob.variant(font: "a" * 10_000).processed - assert_operator variant.service_url.length, :<, 726 + assert_operator variant.service_url.length, :<, 730 end test "works for vips processor" do diff --git a/activesupport/MIT-LICENSE b/activesupport/MIT-LICENSE index 8f769c0767..315a4bc7c4 100644 --- a/activesupport/MIT-LICENSE +++ b/activesupport/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2005-2018 David Heinemeier Hansson +Copyright (c) 2005-2019 David Heinemeier Hansson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/activesupport/lib/active_support.rb b/activesupport/lib/active_support.rb index a4fb697669..5589c71281 100644 --- a/activesupport/lib/active_support.rb +++ b/activesupport/lib/active_support.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true #-- -# Copyright (c) 2005-2018 David Heinemeier Hansson +# Copyright (c) 2005-2019 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/activesupport/lib/active_support/core_ext/securerandom.rb b/activesupport/lib/active_support/core_ext/securerandom.rb index b4a491f5fd..ef812f7e1a 100644 --- a/activesupport/lib/active_support/core_ext/securerandom.rb +++ b/activesupport/lib/active_support/core_ext/securerandom.rb @@ -4,17 +4,18 @@ require "securerandom" module SecureRandom BASE58_ALPHABET = ("0".."9").to_a + ("A".."Z").to_a + ("a".."z").to_a - ["0", "O", "I", "l"] + BASE36_ALPHABET = ("0".."9").to_a + ("a".."z").to_a + # SecureRandom.base58 generates a random base58 string. # - # The argument _n_ specifies the length, of the random string to be generated. + # The argument _n_ specifies the length of the random string to be generated. # # If _n_ is not specified or is +nil+, 16 is assumed. It may be larger in the future. # - # The result may contain alphanumeric characters except 0, O, I and l + # The result may contain alphanumeric characters except 0, O, I and l. # # p SecureRandom.base58 # => "4kUgL2pdQMSCQtjE" # p SecureRandom.base58(24) # => "77TMHrHJFvFDwodq8w7Ev2m7" - # def self.base58(n = 16) SecureRandom.random_bytes(n).unpack("C*").map do |byte| idx = byte % 64 @@ -22,4 +23,23 @@ module SecureRandom BASE58_ALPHABET[idx] end.join end + + # SecureRandom.base36 generates a random base36 string in lowercase. + # + # The argument _n_ specifies the length of the random string to be generated. + # + # If _n_ is not specified or is +nil+, 16 is assumed. It may be larger in the future. + # This method can be used over +base58+ if a deterministic case key is necessary. + # + # The result will contain alphanumeric characters in lowercase. + # + # p SecureRandom.base36 # => "4kugl2pdqmscqtje" + # p SecureRandom.base36(24) # => "77tmhrhjfvfdwodq8w7ev2m7" + def self.base36(n = 16) + SecureRandom.random_bytes(n).unpack("C*").map do |byte| + idx = byte % 64 + idx = SecureRandom.random_number(36) if idx >= 36 + BASE36_ALPHABET[idx] + end.join + end end diff --git a/activesupport/lib/active_support/log_subscriber.rb b/activesupport/lib/active_support/log_subscriber.rb index 0f7be06c8e..938cfdb914 100644 --- a/activesupport/lib/active_support/log_subscriber.rb +++ b/activesupport/lib/active_support/log_subscriber.rb @@ -5,8 +5,8 @@ require "active_support/core_ext/class/attribute" require "active_support/subscriber" module ActiveSupport - # ActiveSupport::LogSubscriber is an object set to consume - # ActiveSupport::Notifications with the sole purpose of logging them. + # <tt>ActiveSupport::LogSubscriber</tt> is an object set to consume + # <tt>ActiveSupport::Notifications</tt> with the sole purpose of logging them. # The log subscriber dispatches notifications to a registered object based # on its given namespace. # @@ -16,7 +16,7 @@ module ActiveSupport # module ActiveRecord # class LogSubscriber < ActiveSupport::LogSubscriber # def sql(event) - # "#{event.payload[:name]} (#{event.duration}) #{event.payload[:sql]}" + # info "#{event.payload[:name]} (#{event.duration}) #{event.payload[:sql]}" # end # end # end @@ -29,13 +29,36 @@ module ActiveSupport # subscriber, the line above should be called after your # <tt>ActiveRecord::LogSubscriber</tt> definition. # - # After configured, whenever a "sql.active_record" notification is published, - # it will properly dispatch the event (ActiveSupport::Notifications::Event) to - # the sql method. + # After configured, whenever a <tt>"sql.active_record"</tt> notification is published, + # it will properly dispatch the event + # (<tt>ActiveSupport::Notifications::Event</tt>) to the sql method. + # + # Being an <tt>ActiveSupport::Notifications</tt> consumer, + # <tt>ActiveSupport::LogSubscriber</tt> exposes a simple interface to check if + # instrumented code raises an exception. It is common to log a different + # message in case of an error, and this can be achieved by extending + # the previous example: + # + # module ActiveRecord + # class LogSubscriber < ActiveSupport::LogSubscriber + # def sql(event) + # exception = event.payload[:exception] + # + # if exception + # exception_object = event.payload[:exception_object] + # + # error "[ERROR] #{event.payload[:name]}: #{exception.join(', ')} " \ + # "(#{exception_object.backtrace.first})" + # else + # # standard logger code + # end + # end + # end + # end # # Log subscriber also has some helpers to deal with logging and automatically - # flushes all logs when the request finishes (via action_dispatch.callback - # notification) in a Rails environment. + # flushes all logs when the request finishes + # (via <tt>action_dispatch.callback</tt> notification) in a Rails environment. class LogSubscriber < Subscriber # Embed in a String to clear all previous ANSI sequences. CLEAR = "\e[0m" diff --git a/activesupport/lib/active_support/notifications.rb b/activesupport/lib/active_support/notifications.rb index 0ff32bd810..7ccc333463 100644 --- a/activesupport/lib/active_support/notifications.rb +++ b/activesupport/lib/active_support/notifications.rb @@ -67,9 +67,12 @@ module ActiveSupport # have a key <tt>:exception</tt> with an array of two elements as value: a string with # the name of the exception class, and the exception message. # The <tt>:exception_object</tt> key of the payload will have the exception - # itself as the value. + # itself as the value: # - # As the previous example depicts, the class <tt>ActiveSupport::Notifications::Event</tt> + # event.payload[:exception] # => ["ArgumentError", "Invalid value"] + # event.payload[:exception_object] # => #<ArgumentError: Invalid value> + # + # As the earlier example depicts, the class <tt>ActiveSupport::Notifications::Event</tt> # is able to take the arguments as they come and provide an object-oriented # interface to that data. # diff --git a/activesupport/test/core_ext/secure_random_test.rb b/activesupport/test/core_ext/secure_random_test.rb index 7067fb524c..4b73233971 100644 --- a/activesupport/test/core_ext/secure_random_test.rb +++ b/activesupport/test/core_ext/secure_random_test.rb @@ -19,4 +19,24 @@ class SecureRandomTest < ActiveSupport::TestCase assert_not_equal s1, s2 assert_equal 24, s1.length end + + def test_base36 + s1 = SecureRandom.base36 + s2 = SecureRandom.base36 + + assert_not_equal s1, s2 + assert_equal 16, s1.length + assert_match(/^[a-z0-9]+$/, s1) + assert_match(/^[a-z0-9]+$/, s2) + end + + def test_base36_with_length + s1 = SecureRandom.base36(24) + s2 = SecureRandom.base36(24) + + assert_not_equal s1, s2 + assert_equal 24, s1.length + assert_match(/^[a-z0-9]+$/, s1) + assert_match(/^[a-z0-9]+$/, s2) + end end diff --git a/guides/source/6_0_release_notes.md b/guides/source/6_0_release_notes.md index f3ed21dc45..9716132156 100644 --- a/guides/source/6_0_release_notes.md +++ b/guides/source/6_0_release_notes.md @@ -5,6 +5,7 @@ Ruby on Rails 6.0 Release Notes Highlights in Rails 6.0: +* Action Mailbox * Parallel Testing These release notes cover only the major changes. To learn about various bug @@ -28,6 +29,14 @@ guide. Major Features -------------- +### Action Mailbox + +[Pull Request](https://github.com/rails/rails/pull/34786) + +[Action Mailbox](https://github.com/rails/rails/tree/6-0-stable/actionmailbox) allows you +to route incoming emails to controller-like mailboxes. +You can read more about Action Mailbox in the [Action Mailbox Basics](action_mailbox_basics.html) guide. + ### Parallel Testing [Pull Request](https://github.com/rails/rails/pull/31900) diff --git a/guides/source/action_mailbox_basics.md b/guides/source/action_mailbox_basics.md new file mode 100644 index 0000000000..eb8a14b4d2 --- /dev/null +++ b/guides/source/action_mailbox_basics.md @@ -0,0 +1,302 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON https://guides.rubyonrails.org.** + +Action Mailbox Basics +===================== + +This guide provides you with all you need to get started in receiving +emails to your application. + +After reading this guide, you will know: + +* How to receive email within a Rails application. +* How to configure Action Mailbox. +* How to generate and route emails to a mailbox. +* How to test incoming emails. + +-------------------------------------------------------------------------------- + +Introduction +------------ + +Action Mailbox routes incoming emails to controller-like mailboxes for +processing in Rails. It ships with ingresses for Amazon SES, Mailgun, Mandrill, +and SendGrid. You can also handle inbound mails directly via the built-in +Postfix ingress. + +The inbound emails are turned into `InboundEmail` records using Active Record +and feature lifecycle tracking, storage of the original email on cloud storage +via Active Storage, and responsible data handling with +on-by-default incineration. + +These inbound emails are routed asynchronously using Active Job to one or +several dedicated mailboxes, which are capable of interacting directly +with the rest of your domain model. + +## Setup + +Install migrations needed for `InboundEmail` and ensure Active Storage is set up: + +```bash +$ rails action_mailbox:install +$ rails db:migrate +``` + +## Configuration + +### Amazon SES + +Install the [`aws-sdk-sns`](https://rubygems.org/gems/aws-sdk-sns) gem: + +```ruby +# Gemfile +gem "aws-sdk-sns", ">= 1.9.0", require: false +``` + +Tell Action Mailbox to accept emails from SES: + +```ruby +# config/environments/production.rb +config.action_mailbox.ingress = :amazon +``` + +[Configure SES](https://docs.aws.amazon.com/ses/latest/DeveloperGuide/receiving-email-notifications.html) +to deliver emails to your application via POST requests to +`/rails/action_mailbox/amazon/inbound_emails`. If your application lived at +`https://example.com`, you would specify the fully-qualified URL +`https://example.com/rails/action_mailbox/amazon/inbound_emails`. + +### Mailgun + +Give Action Mailbox your +[Mailgun API key](https://help.mailgun.com/hc/en-us/articles/203380100-Where-can-I-find-my-API-key-and-SMTP-credentials) +so it can authenticate requests to the Mailgun ingress. + +Use `rails credentials:edit` to add your API key to your application's +encrypted credentials under `action_mailbox.mailgun_api_key`, +where Action Mailbox will automatically find it: + +```yaml +action_mailbox: + mailgun_api_key: ... +``` + +Alternatively, provide your API key in the `MAILGUN_INGRESS_API_KEY` environment +variable. + +Tell Action Mailbox to accept emails from Mailgun: + +```ruby +# config/environments/production.rb +config.action_mailbox.ingress = :mailgun +``` + +[Configure Mailgun](https://documentation.mailgun.com/en/latest/user_manual.html#receiving-forwarding-and-storing-messages) +to forward inbound emails to `/rails/action_mailbox/mailgun/inbound_emails/mime`. +If your application lived at `https://example.com`, you would specify the +fully-qualified URL `https://example.com/rails/action_mailbox/mailgun/inbound_emails/mime`. + +### Mandrill + +Give Action Mailbox your Mandrill API key so it can authenticate requests to +the Mandrill ingress. + +Use `rails credentials:edit` to add your API key to your application's +encrypted credentials under `action_mailbox.mandrill_api_key`, +where Action Mailbox will automatically find it: + +```yaml +action_mailbox: + mandrill_api_key: ... +``` + +Alternatively, provide your API key in the `MANDRILL_INGRESS_API_KEY` +environment variable. + +Tell Action Mailbox to accept emails from Mandrill: + +```ruby +# config/environments/production.rb +config.action_mailbox.ingress = :mandrill +``` + +[Configure Mandrill](https://mandrill.zendesk.com/hc/en-us/articles/205583197-Inbound-Email-Processing-Overview) +to route inbound emails to `/rails/action_mailbox/mandrill/inbound_emails`. +If your application lived at `https://example.com`, you would specify +the fully-qualified URL `https://example.com/rails/action_mailbox/mandrill/inbound_emails`. + +### Postfix + +Tell Action Mailbox to accept emails from Postfix: + +```ruby +# config/environments/production.rb +config.action_mailbox.ingress = :postfix +``` + +Generate a strong password that Action Mailbox can use to authenticate requests to the Postfix ingress. + +Use `rails credentials:edit` to add the password to your application's encrypted credentials under +`action_mailbox.ingress_password`, where Action Mailbox will automatically find it: + +```yaml +action_mailbox: + ingress_password: ... +``` + +Alternatively, provide the password in the `RAILS_INBOUND_EMAIL_PASSWORD` environment variable. + +[Configure Postfix](https://serverfault.com/questions/258469/how-to-configure-postfix-to-pipe-all-incoming-email-to-a-script) +to pipe inbound emails to `bin/rails action_mailbox:ingress:postfix`, providing +the `URL` of the Postfix ingress and the `INGRESS_PASSWORD` you previously +generated. If your application lived at `https://example.com`, the full command +would look like this: + +```bash +$ URL=https://example.com/rails/action_mailbox/postfix/inbound_emails INGRESS_PASSWORD=... rails action_mailbox:ingress:postfix +``` + +### SendGrid + +Tell Action Mailbox to accept emails from SendGrid: + +```ruby +# config/environments/production.rb +config.action_mailbox.ingress = :sendgrid +``` + +Generate a strong password that Action Mailbox can use to authenticate +requests to the SendGrid ingress. + +Use `rails credentials:edit` to add the password to your application's +encrypted credentials under `action_mailbox.ingress_password`, +where Action Mailbox will automatically find it: + +```yaml +action_mailbox: + ingress_password: ... +``` + +Alternatively, provide the password in the `RAILS_INBOUND_EMAIL_PASSWORD` +environment variable. + +[Configure SendGrid Inbound Parse](https://sendgrid.com/docs/for-developers/parsing-email/setting-up-the-inbound-parse-webhook/) +to forward inbound emails to +`/rails/action_mailbox/sendgrid/inbound_emails` with the username `actionmailbox` +and the password you previously generated. If your application lived at `https://example.com`, +you would configure SendGrid with the following URL: + +``` +https://actionmailbox:PASSWORD@example.com/rails/action_mailbox/sendgrid/inbound_emails +``` + +NOTE: When configuring your SendGrid Inbound Parse webhook, be sure to check the box labeled **“Post the raw, full MIME message.”** Action Mailbox needs the raw MIME message to work. + +## Examples + +Configure basic routing: + +```ruby +# app/mailboxes/application_mailbox.rb +class ApplicationMailbox < ActionMailbox::Base + routing /^save@/i => :forwards + routing /@replies\./i => :replies +end +``` + +Then set up a mailbox: + +```ruby +# Generate new mailbox +$ bin/rails generate mailbox forwards +``` + +```ruby +# app/mailboxes/forwards_mailbox.rb +class ForwardsMailbox < ApplicationMailbox + # Callbacks specify prerequisites to processing + before_processing :require_forward + + def process + if forwarder.buckets.one? + record_forward + else + stage_forward_and_request_more_details + end + end + + private + def require_forward + unless message.forward? + # Use Action Mailers to bounce incoming emails back to sender – this halts processing + bounce_with Forwards::BounceMailer.missing_forward( + inbound_email, forwarder: forwarder + ) + end + end + + def forwarder + @forwarder ||= Person.where(email_address: mail.from) + end + + def record_forward + forwarder.buckets.first.record \ + Forward.new forwarder: forwarder, subject: message.subject, content: mail.content + end + + def stage_forward_and_request_more_details + Forwards::RoutingMailer.choose_project(mail).deliver_now + end +end +``` + +## Incineration of InboundEmails + +By default, an InboundEmail that has been successfully processed will be +incinerated after 30 days. This ensures you're not holding on to people's data +willy-nilly after they may have canceled their accounts or deleted their +content. The intention is that after you've processed an email, you should have +extracted all the data you needed and turned it into domain models and content +on your side of the application. The InboundEmail simply stays in the system +for the extra time to provide debugging and forensics options. + +The actual incineration is done via the `IncinerationJob` that's scheduled +to run after `config.action_mailbox.incinerate_after` time. This value is +by default set to `30.days`, but you can change it in your production.rb +configuration. (Note that this far-future incineration scheduling relies on +your job queue being able to hold jobs for that long.) + +## Working with Action Mailbox in development + +It's helpful to be able to test incoming emails in development without actually +sending and receiving real emails. To accomplish this, there's a conductor +controller mounted at `/rails/conductor/action_mailbox/inbound_emails`, +which gives you an index of all the InboundEmails in the system, their +state of processing, and a form to create a new InboundEmail as well. + +## Testing mailboxes + +Example: + +```ruby +class ForwardsMailboxTest < ActionMailbox::TestCase + test "directly recording a client forward for a forwarder and forwardee corresponding to one project" do + assert_difference -> { people(:david).buckets.first.recordings.count } do + receive_inbound_email_from_mail \ + to: 'save@example.com', + from: people(:david).email_address, + subject: "Fwd: Status update?", + body: <<~BODY + --- Begin forwarded message --- + From: Frank Holland <frank@microsoft.com> + + What's the status? + BODY + end + + recording = people(:david).buckets.first.recordings.last + assert_equal people(:david), recording.creator + assert_equal "Status update?", recording.forward.subject + assert_match "What's the status?", recording.forward.content.to_s + end +end +``` diff --git a/guides/source/action_mailer_basics.md b/guides/source/action_mailer_basics.md index 1acb993cad..16db433bd4 100644 --- a/guides/source/action_mailer_basics.md +++ b/guides/source/action_mailer_basics.md @@ -3,13 +3,13 @@ Action Mailer Basics ==================== -This guide provides you with all you need to get started in sending and -receiving emails from and to your application, and many internals of Action +This guide provides you with all you need to get started in sending +emails from and to your application, and many internals of Action Mailer. It also covers how to test your mailers. After reading this guide, you will know: -* How to send and receive email within a Rails application. +* How to send email within a Rails application. * How to generate and edit an Action Mailer class and mailer view. * How to configure Action Mailer for your environment. * How to test your Action Mailer classes. @@ -427,7 +427,7 @@ If you would like to render a template located outside of the default `app/views ```ruby class UserMailer < ApplicationMailer prepend_view_path "custom/path/to/mailer/view" - + # This will try to load "custom/path/to/mailer/view/welcome_email" template def welcome_email # ... @@ -651,48 +651,8 @@ class UserMailer < ApplicationMailer end ``` -Receiving Emails ----------------- - -Receiving and parsing emails with Action Mailer can be a rather complex -endeavor. Before your email reaches your Rails app, you would have had to -configure your system to somehow forward emails to your app, which needs to be -listening for that. So, to receive emails in your Rails app you'll need to: - -* Implement a `receive` method in your mailer. - -* Configure your email server to forward emails from the address(es) you would - like your app to receive to `/path/to/app/bin/rails runner - 'UserMailer.receive(STDIN.read)'`. - -Once a method called `receive` is defined in any mailer, Action Mailer will -parse the raw incoming email into an email object, decode it, instantiate a new -mailer, and pass the email object to the mailer `receive` instance -method. Here's an example: - -```ruby -class UserMailer < ApplicationMailer - def receive(email) - page = Page.find_by(address: email.to.first) - page.emails.create( - subject: email.subject, - body: email.body - ) - - if email.has_attachments? - email.attachments.each do |attachment| - page.attachments.create({ - file: attachment, - description: email.subject - }) - end - end - end -end -``` - Action Mailer Callbacks ---------------------------- +----------------------- Action Mailer allows for you to specify a `before_action`, `after_action` and `around_action`. @@ -882,7 +842,7 @@ class EmailDeliveryObserver end end ``` -Like interceptors, you need to register observers with the Action Mailer framework. You can do this in an initializer file +Like interceptors, you need to register observers with the Action Mailer framework. You can do this in an initializer file `config/initializers/email_delivery_observer.rb` ```ruby diff --git a/guides/source/active_support_instrumentation.md b/guides/source/active_support_instrumentation.md index 5e68b3f400..c1c3832b79 100644 --- a/guides/source/active_support_instrumentation.md +++ b/guides/source/active_support_instrumentation.md @@ -255,13 +255,16 @@ Active Record ### sql.active_record -| Key | Value | -| ---------------- | ---------------------------------------- | -| `:sql` | SQL statement | -| `:name` | Name of the operation | -| `:connection_id` | `self.object_id` | -| `:binds` | Bind parameters | -| `:cached` | `true` is added when cached queries used | +| Key | Value | +| -------------------- | ---------------------------------------- | +| `:sql` | SQL statement | +| `:name` | Name of the operation | +| `:connection_id` | Object ID of the connection object | +| `:connection` | Connection object | +| `:binds` | Bind parameters | +| `:type_casted_binds` | Typecasted bind parameters | +| `:statement_name` | SQL Statement name | +| `:cached` | `true` is added when cached queries used | INFO. The adapters will add their own data as well. @@ -270,7 +273,10 @@ INFO. The adapters will add their own data as well. sql: "SELECT \"posts\".* FROM \"posts\" ", name: "Post Load", connection_id: 70307250813140, - binds: [] + connection: #<ActiveRecord::ConnectionAdapters::SQLite3Adapter:0x00007f9f7a838850>, + binds: [#<ActiveModel::Attribute::WithCastValue:0x00007fe19d15dc00>], + type_casted_binds: [11], + statement_name: nil } ``` @@ -291,32 +297,6 @@ INFO. The adapters will add their own data as well. Action Mailer ------------- -### receive.action_mailer - -| Key | Value | -| ------------- | -------------------------------------------- | -| `:mailer` | Name of the mailer class | -| `:message_id` | ID of the message, generated by the Mail gem | -| `:subject` | Subject of the mail | -| `:to` | To address(es) of the mail | -| `:from` | From address of the mail | -| `:bcc` | BCC addresses of the mail | -| `:cc` | CC addresses of the mail | -| `:date` | Date of the mail | -| `:mail` | The encoded form of the mail | - -```ruby -{ - mailer: "Notification", - message_id: "4f5b5491f1774_181b23fc3d4434d38138e5@mba.local.mail", - subject: "Rails Guides", - to: ["users@rails.com", "dhh@rails.com"], - from: ["me@rails.com"], - date: Sat, 10 Mar 2012 14:18:09 +0100, - mail: "..." # omitted for brevity -} -``` - ### deliver.action_mailer | Key | Value | diff --git a/guides/source/asset_pipeline.md b/guides/source/asset_pipeline.md index 500e230ff9..e7faa5c330 100644 --- a/guides/source/asset_pipeline.md +++ b/guides/source/asset_pipeline.md @@ -1101,7 +1101,7 @@ Windows you have a JavaScript runtime installed in your operating system. -### Serving GZipped version of assets +### GZipping your assets By default, gzipped version of compiled assets will be generated, along with the non-gzipped version of assets. Gzipped assets help reduce the transmission @@ -1111,6 +1111,8 @@ of data over the wire. You can configure this by setting the `gzip` flag. config.assets.gzip = false # disable gzipped assets generation ``` +Refer to your web server's documentation for instructions on how to serve gzipped assets. + ### Using Your Own Compressor The compressor config settings for CSS and JavaScript also take any object. diff --git a/guides/source/configuring.md b/guides/source/configuring.md index ae1de3079f..3b21197ae4 100644 --- a/guides/source/configuring.md +++ b/guides/source/configuring.md @@ -619,6 +619,29 @@ Defaults to `'signed cookie'`. development mode, but for large test suites, disabling this option in the test environment can improve performance. This defaults to `true`. + +### Configuring Action Mailbox + +`config.action_mailbox` provides the following configuration options: + +* `config.action_mailbox.logger` contains the logger used by Action Mailbox. It accepts a logger conforming to the interface of Log4r or the default Ruby Logger class. The default is `Rails.logger`. + + ```ruby + config.action_mailbox.logger = ActiveSupport::Logger.new(STDOUT) + ``` + +* `config.action_mailbox.incinerate_after` accepts an `ActiveSupport::Duration` indicating how long after processing `ActionMailbox::InboundEmail` records should be destroyed. It defaults to `30.days`. + + ```ruby + # Incinerate inbound emails 14 days after processing. + config.action_mailbox.incinerate_after = 14.days + ``` + +* `config.action_mailbox.queues.incineration` accepts a symbol indicating the Active Job queue to use for incineration jobs. It defaults to `:action_mailbox_incineration`. + +* `config.action_mailbox.queues.routing` accepts a symbol indicating the Active Job queue to use for routing jobs. It defaults to `:action_mailbox_routing`. + + ### Configuring Action Mailer There are a number of settings available on `config.action_mailer`: @@ -815,15 +838,21 @@ normal Rails server. config.active_storage.paths[:ffprobe] = '/usr/local/bin/ffprobe' ``` -* `config.active_storage.variable_content_types` accepts an array of strings indicating the content types that Active Storage can transform through ImageMagick. The default is `%w(image/png image/gif image/jpg image/jpeg image/pjpeg image/vnd.adobe.photoshop image/vnd.microsoft.icon)`. +* `config.active_storage.variable_content_types` accepts an array of strings indicating the content types that Active Storage can transform through ImageMagick. The default is `%w(image/png image/gif image/jpg image/jpeg image/pjpeg image/tiff image/vnd.adobe.photoshop image/vnd.microsoft.icon)`. * `config.active_storage.content_types_to_serve_as_binary` accepts an array of strings indicating the content types that Active Storage will always serve as an attachment, rather than inline. The default is `%w(text/html text/javascript image/svg+xml application/postscript application/x-shockwave-flash text/xml application/xml application/xhtml+xml)`. -* `config.active_storage.queue` can be used to set the name of the Active Job queue used to perform jobs like analyzing the content of a blob or purging a blog. +* `config.active_storage.queues.analysis` accepts a symbol indicating the Active Job queue to use for analysis jobs. When this option is `nil`, analysis jobs are sent to the default Active Job queue (see `config.active_job.default_queue_name`). + + ```ruby + config.active_storage.queues.analysis = :low_priority + ``` + +* `config.active_storage.queues.purge` accepts a symbol indicating the Active Job queue to use for purge jobs. When this option is `nil`, purge jobs are sent to the default Active Job queue (see `config.active_job.default_queue_name`). ```ruby - config.active_storage.queue = :low_priority + config.active_storage.queues.purge = :low_priority ``` * `config.active_storage.logger` can be used to set the logger used by Active Storage. Accepts a logger conforming to the interface of Log4r or the default Ruby Logger class. @@ -847,6 +876,7 @@ text/javascript image/svg+xml application/postscript application/x-shockwave-fla The default is `/rails/active_storage` + ### Configuring a Database Just about every Rails application will interact with a database. You can connect to the database by setting an environment variable `ENV['DATABASE_URL']` or by using a configuration file called `config/database.yml`. diff --git a/guides/source/documents.yaml b/guides/source/documents.yaml index 25c159d471..0f836bdf48 100644 --- a/guides/source/documents.yaml +++ b/guides/source/documents.yaml @@ -74,7 +74,12 @@ - name: Action Mailer Basics url: action_mailer_basics.html - description: This guide describes how to use Action Mailer to send and receive emails. + description: This guide describes how to use Action Mailer to send emails. + - + name: Action Mailbox Basics + work_in_progress: true + url: action_mailbox_basics.html + description: This guide describes how to use Action Mailbox to receive emails. - name: Active Job Basics url: active_job_basics.html diff --git a/guides/source/engines.md b/guides/source/engines.md index 1e93a19c84..bd0542ffc1 100644 --- a/guides/source/engines.md +++ b/guides/source/engines.md @@ -1505,6 +1505,9 @@ To hook into the initialization process of one of the following classes use the | `ActionController::TestCase` | `action_controller_test_case` | | `ActionDispatch::IntegrationTest` | `action_dispatch_integration_test` | | `ActionDispatch::SystemTestCase` | `action_dispatch_system_test_case` | +| `ActionMailbox::Base` | `action_mailbox` | +| `ActionMailbox::InboundEmail` | `action_mailbox_inbound_email` | +| `ActionMailbox::TestCase` | `action_mailbox_test_case` | | `ActionMailer::Base` | `action_mailer` | | `ActionMailer::TestCase` | `action_mailer_test_case` | | `ActionView::Base` | `action_view` | @@ -1512,6 +1515,7 @@ To hook into the initialization process of one of the following classes use the | `ActiveJob::Base` | `active_job` | | `ActiveJob::TestCase` | `active_job_test_case` | | `ActiveRecord::Base` | `active_record` | +| `ActiveStorage::Blob` | `active_storage_blob` | | `ActiveSupport::TestCase` | `active_support_test_case` | | `i18n` | `i18n` | diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md index 8e358b4293..edfd5e2cd1 100644 --- a/railties/CHANGELOG.md +++ b/railties/CHANGELOG.md @@ -1,3 +1,7 @@ +* Add `rails test:mailboxes`. + + *George Claghorn* + * Introduce guard against DNS rebinding attacks The `ActionDispatch::HostAuthorization` is a new middleware that prevent diff --git a/railties/MIT-LICENSE b/railties/MIT-LICENSE index cce00cbc3a..ea8823d7ef 100644 --- a/railties/MIT-LICENSE +++ b/railties/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2018 David Heinemeier Hansson +Copyright (c) 2004-2019 David Heinemeier Hansson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/railties/lib/rails/commands/server/server_command.rb b/railties/lib/rails/commands/server/server_command.rb index 70789e0303..6c4cc3cb86 100644 --- a/railties/lib/rails/commands/server/server_command.rb +++ b/railties/lib/rails/commands/server/server_command.rb @@ -302,9 +302,10 @@ module Rails MSG else suggestion = Rails::Command::Spellchecker.suggest(server, from: RACK_SERVERS) + suggestion_msg = "Maybe you meant #{suggestion.inspect}?" if suggestion <<~MSG - Could not find server "#{server}". Maybe you meant #{suggestion.inspect}? + Could not find server "#{server}". #{suggestion_msg} Run `rails server --help` for more options. MSG end diff --git a/railties/lib/rails/generators.rb b/railties/lib/rails/generators.rb index 5e8cebc50a..caf8a33c3c 100644 --- a/railties/lib/rails/generators.rb +++ b/railties/lib/rails/generators.rb @@ -272,8 +272,10 @@ module Rails else options = sorted_groups.flat_map(&:last) suggestion = Rails::Command::Spellchecker.suggest(namespace.to_s, from: options) + suggestion_msg = "Maybe you meant #{suggestion.inspect}?" if suggestion + puts <<~MSG - Could not find generator '#{namespace}'. Maybe you meant #{suggestion.inspect}? + Could not find generator '#{namespace}'. #{suggestion_msg} Run `rails generate --help` for more options. MSG end diff --git a/railties/lib/rails/generators/app_base.rb b/railties/lib/rails/generators/app_base.rb index 1fc14d196a..7b30f8b9ca 100644 --- a/railties/lib/rails/generators/app_base.rb +++ b/railties/lib/rails/generators/app_base.rb @@ -45,7 +45,7 @@ module Rails desc: "Skip Action Mailer files" class_option :skip_action_mailbox, type: :boolean, default: false, - desc: "Skip Action Mailbox files" + desc: "Skip Action Mailbox gem" class_option :skip_active_record, type: :boolean, aliases: "-O", default: false, desc: "Skip Active Record files" @@ -234,7 +234,7 @@ module Rails end def skip_action_mailbox? # :doc: - options[:skip_action_mailbox] || options[:skip_active_record] + options[:skip_action_mailbox] || skip_active_storage? end class GemfileEntry < Struct.new(:name, :version, :comment, :options, :commented_out) diff --git a/railties/lib/rails/generators/rails/app/templates/Gemfile.tt b/railties/lib/rails/generators/rails/app/templates/Gemfile.tt index fb264935bd..1ad3a4b1f7 100644 --- a/railties/lib/rails/generators/rails/app/templates/Gemfile.tt +++ b/railties/lib/rails/generators/rails/app/templates/Gemfile.tt @@ -26,9 +26,6 @@ ruby <%= "'#{RUBY_VERSION}'" -%> # gem 'image_processing', '~> 1.2' <% end -%> -# Use Capistrano for deployment -# gem 'capistrano-rails', group: :development - <% if depend_on_bootsnap? -%> # Reduces boot times through caching; required in config/boot.rb gem 'bootsnap', '>= 1.1.0', require: false diff --git a/railties/lib/rails/test_unit/testing.rake b/railties/lib/rails/test_unit/testing.rake index 32ac27a135..ecc458b21e 100644 --- a/railties/lib/rails/test_unit/testing.rake +++ b/railties/lib/rails/test_unit/testing.rake @@ -28,7 +28,7 @@ namespace :test do desc "Run tests quickly, but also reset db" task db: %w[db:test:prepare test] - ["models", "helpers", "controllers", "mailers", "integration", "jobs"].each do |name| + ["models", "helpers", "controllers", "mailers", "integration", "jobs", "mailboxes"].each do |name| task name => "test:prepare" do $: << "test" Rails::TestUnit::Runner.rake_run(["test/#{name}"]) diff --git a/railties/test/application/configuration_test.rb b/railties/test/application/configuration_test.rb index 886fb0f843..8eaf07586e 100644 --- a/railties/test/application/configuration_test.rb +++ b/railties/test/application/configuration_test.rb @@ -331,7 +331,7 @@ module ApplicationTests assert_not_includes Post.instance_methods, :title end - test "eager loads attribute methods in production" do + test "does not eager load attribute methods in production when the schema cache is empty" do app_file "app/models/post.rb", <<-RUBY class Post < ActiveRecord::Base end @@ -354,9 +354,71 @@ module ApplicationTests app "production" + assert_not_includes Post.instance_methods, :title + end + + test "eager loads attribute methods in production when the schema cache is populated" do + app_file "app/models/post.rb", <<-RUBY + class Post < ActiveRecord::Base + end + RUBY + + app_file "config/initializers/active_record.rb", <<-RUBY + ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:") + ActiveRecord::Migration.verbose = false + ActiveRecord::Schema.define(version: 1) do + create_table :posts do |t| + t.string :title + end + end + RUBY + + add_to_config <<-RUBY + config.eager_load = true + config.cache_classes = true + RUBY + + app_file "config/initializers/schema_cache.rb", <<-RUBY + ActiveRecord::Base.connection.schema_cache.add("posts") + RUBY + + app "production" + assert_includes Post.instance_methods, :title end + test "does not attempt to eager load attribute methods for models that aren't connected" do + app_file "app/models/post.rb", <<-RUBY + class Post < ActiveRecord::Base + end + RUBY + + app_file "config/initializers/active_record.rb", <<-RUBY + ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:") + ActiveRecord::Migration.verbose = false + ActiveRecord::Schema.define(version: 1) do + create_table :posts do |t| + t.string :title + end + end + RUBY + + add_to_config <<-RUBY + config.eager_load = true + config.cache_classes = true + RUBY + + app_file "app/models/comment.rb", <<-RUBY + class Comment < ActiveRecord::Base + establish_connection(adapter: "mysql2", database: "does_not_exist") + end + RUBY + + assert_nothing_raised do + app "production" + end + end + test "initialize an eager loaded, cache classes app" do add_to_config <<-RUBY config.eager_load = true diff --git a/railties/test/application/test_runner_test.rb b/railties/test/application/test_runner_test.rb index 55ce72181f..6765eef9d0 100644 --- a/railties/test/application/test_runner_test.rb +++ b/railties/test/application/test_runner_test.rb @@ -131,6 +131,18 @@ module ApplicationTests end end + def test_run_mailboxes + create_test_file :mailboxes, "foo_mailbox" + create_test_file :mailboxes, "bar_mailbox" + create_test_file :models, "foo" + + rails("test:mailboxes").tap do |output| + assert_match "FooMailboxTest", output + assert_match "BarMailboxTest", output + assert_match "2 runs, 2 assertions, 0 failures", output + end + end + def test_run_functionals create_test_file :mailers, "foo_mailer" create_test_file :controllers, "bar_controller" @@ -155,11 +167,11 @@ module ApplicationTests end def test_run_all_suites - suites = [:models, :helpers, :unit, :controllers, :mailers, :functional, :integration, :jobs] + suites = [:models, :helpers, :unit, :controllers, :mailers, :functional, :integration, :jobs, :mailboxes] suites.each { |suite| create_test_file suite, "foo_#{suite}" } run_test_command("") .tap do |output| suites.each { |suite| assert_match "Foo#{suite.to_s.camelize}Test", output } - assert_match "8 runs, 8 assertions, 0 failures", output + assert_match "9 runs, 9 assertions, 0 failures", output end end diff --git a/railties/test/commands/server_test.rb b/railties/test/commands/server_test.rb index fbdd3f3ebb..25b89ecbd8 100644 --- a/railties/test/commands/server_test.rb +++ b/railties/test/commands/server_test.rb @@ -32,6 +32,12 @@ class Rails::Command::ServerCommandTest < ActiveSupport::TestCase assert_match(/Could not find server "tin". Maybe you meant "thin"?/, run_command("--using", "tin")) end + def test_using_server_mistype_without_suggestion + output = run_command("--using", "t") + assert_match(/Could not find server "t"/, output) + assert_no_match(/Maybe you meant/, output) + end + def test_using_positional_argument_deprecation assert_match(/DEPRECATION WARNING/, run_command("tin")) end diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb index c7a9803adf..f287827f81 100644 --- a/railties/test/generators/app_generator_test.rb +++ b/railties/test/generators/app_generator_test.rb @@ -435,7 +435,7 @@ class AppGeneratorTest < Rails::Generators::TestCase assert_no_file "#{app_root}/config/storage.yml" end - def test_generator_when_skip_action_mailbox_is_given + def test_generator_skips_action_mailbox_when_skip_action_mailbox_is_given run_generator [destination_root, "--skip-action-mailbox"] assert_file "#{application_path}/config/application.rb", /#\s+require\s+["']action_mailbox\/engine["']/ end @@ -445,6 +445,11 @@ class AppGeneratorTest < Rails::Generators::TestCase assert_file "#{application_path}/config/application.rb", /#\s+require\s+["']action_mailbox\/engine["']/ end + def test_generator_skips_action_mailbox_when_skip_active_storage_is_given + run_generator [destination_root, "--skip-active-storage"] + assert_file "#{application_path}/config/application.rb", /#\s+require\s+["']action_mailbox\/engine["']/ + end + def test_app_update_does_not_change_config_target_version run_generator diff --git a/railties/test/generators_test.rb b/railties/test/generators_test.rb index f98c1f78f7..abdc04a8d3 100644 --- a/railties/test/generators_test.rb +++ b/railties/test/generators_test.rb @@ -28,6 +28,7 @@ class GeneratorsTest < Rails::Generators::TestCase output = capture(:stdout) { Rails::Generators.invoke name } assert_match "Could not find generator '#{name}'", output assert_match "`rails generate --help`", output + assert_no_match "Maybe you meant", output end def test_generator_suggestions |