aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorKasper Timm Hansen <kaspth@gmail.com>2019-01-16 22:00:51 +0100
committerGitHub <noreply@github.com>2019-01-16 22:00:51 +0100
commitcb3f78aa7c8f14921501703ed0780f2a428bc6a1 (patch)
tree8eb4807d949160d38a3d239ea33d37318d38b5aa
parentd49899c15431104f8dad374363bac57479b4bd39 (diff)
parent7e52e3b1c004eb22521c844b6adf69a2689cc1da (diff)
downloadrails-cb3f78aa7c8f14921501703ed0780f2a428bc6a1.tar.gz
rails-cb3f78aa7c8f14921501703ed0780f2a428bc6a1.tar.bz2
rails-cb3f78aa7c8f14921501703ed0780f2a428bc6a1.zip
Merge branch 'master' into db_system_change_command
-rw-r--r--README.md6
-rw-r--r--RELEASING_RAILS.md15
-rw-r--r--actioncable/CHANGELOG.md4
-rw-r--r--actioncable/README.md545
-rw-r--r--actioncable/app/assets/javascripts/action_cable.js17
-rw-r--r--actioncable/app/javascript/action_cable/connection.js8
-rw-r--r--actioncable/app/javascript/action_cable/index.js10
-rw-r--r--actioncable/lib/action_cable/connection.rb1
-rw-r--r--actioncable/lib/action_cable/connection/test_case.rb236
-rw-r--r--actioncable/lib/rails/generators/channel/USAGE3
-rw-r--r--actioncable/lib/rails/generators/channel/channel_generator.rb2
-rw-r--r--actioncable/lib/rails/generators/channel/templates/javascript/consumer.js.tt2
-rw-r--r--actioncable/lib/rails/generators/test_unit/channel_generator.rb20
-rw-r--r--actioncable/lib/rails/generators/test_unit/templates/channel_test.rb.tt8
-rw-r--r--actioncable/package.json2
-rw-r--r--actioncable/test/connection/test_case_test.rb197
-rw-r--r--actionmailbox/README.md2
-rw-r--r--actionmailbox/app/controllers/action_mailbox/ingresses/amazon/inbound_emails_controller.rb2
-rw-r--r--actionmailbox/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb2
-rw-r--r--actionmailbox/app/controllers/action_mailbox/ingresses/postmark/inbound_emails_controller.rb62
-rw-r--r--actionmailbox/app/controllers/action_mailbox/ingresses/relay/inbound_emails_controller.rb (renamed from actionmailbox/app/controllers/action_mailbox/ingresses/postfix/inbound_emails_controller.rb)30
-rw-r--r--actionmailbox/app/jobs/action_mailbox/incineration_job.rb6
-rw-r--r--actionmailbox/app/models/action_mailbox/inbound_email.rb14
-rw-r--r--actionmailbox/app/models/action_mailbox/inbound_email/incineratable.rb6
-rw-r--r--actionmailbox/app/models/action_mailbox/inbound_email/incineratable/incineration.rb6
-rw-r--r--actionmailbox/app/models/action_mailbox/inbound_email/message_id.rb12
-rw-r--r--actionmailbox/app/models/action_mailbox/inbound_email/routable.rb10
-rw-r--r--actionmailbox/config/routes.rb3
-rw-r--r--actionmailbox/db/migrate/20180917164000_create_action_mailbox_tables.rb4
-rw-r--r--actionmailbox/lib/action_mailbox/base.rb24
-rw-r--r--actionmailbox/lib/action_mailbox/relayer.rb (renamed from actionmailbox/lib/action_mailbox/postfix_relayer.rb)28
-rw-r--r--actionmailbox/lib/action_mailbox/router/route.rb2
-rw-r--r--actionmailbox/lib/action_mailbox/routing.rb2
-rw-r--r--actionmailbox/lib/action_mailbox/test_helper.rb12
-rw-r--r--actionmailbox/lib/tasks/ingress.rake58
-rw-r--r--actionmailbox/test/controllers/ingresses/postmark/inbound_emails_controller_test.rb55
-rw-r--r--actionmailbox/test/controllers/ingresses/relay/inbound_emails_controller_test.rb (renamed from actionmailbox/test/controllers/ingresses/postfix/inbound_emails_controller_test.rb)20
-rw-r--r--actionmailbox/test/unit/relayer_test.rb (renamed from actionmailbox/test/unit/postfix_relayer_test.rb)34
-rw-r--r--actionpack/CHANGELOG.md4
-rw-r--r--actionpack/lib/action_controller/metal/mime_responds.rb8
-rw-r--r--actionpack/lib/action_dispatch/routing/route_set.rb4
-rw-r--r--actionpack/lib/action_dispatch/routing/url_for.rb1
-rw-r--r--actionpack/test/controller/base_test.rb2
-rw-r--r--actionpack/test/controller/url_for_test.rb8
-rw-r--r--actiontext/app/javascript/actiontext/attachment_upload.js2
-rw-r--r--actiontext/lib/templates/installer.rb7
-rw-r--r--actiontext/package.json6
-rw-r--r--actiontext/test/dummy/app/javascript/packs/application.js2
-rw-r--r--actiontext/yarn.lock11
-rw-r--r--actionview/app/assets/javascripts/README.md10
-rw-r--r--actionview/lib/action_view/helpers/form_options_helper.rb2
-rw-r--r--actionview/package.json2
-rw-r--r--activemodel/lib/active_model/callbacks.rb1
-rw-r--r--activerecord/CHANGELOG.md14
-rw-r--r--activerecord/lib/active_record/associations.rb6
-rw-r--r--activerecord/lib/active_record/associations/foreign_association.rb7
-rw-r--r--activerecord/lib/active_record/associations/has_many_association.rb2
-rw-r--r--activerecord/lib/active_record/associations/has_one_association.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb4
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_adapter.rb31
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb6
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb2
-rw-r--r--activerecord/lib/active_record/relation.rb5
-rw-r--r--activerecord/lib/active_record/relation/finder_methods.rb2
-rw-r--r--activerecord/lib/active_record/validations/uniqueness.rb38
-rw-r--r--activerecord/lib/arel.rb1
-rw-r--r--activerecord/lib/arel/compatibility/wheres.rb35
-rw-r--r--activerecord/lib/arel/predications.rb8
-rw-r--r--activerecord/test/cases/adapters/postgresql/case_insensitive_test.rb17
-rw-r--r--activerecord/test/cases/arel/attributes/attribute_test.rb123
-rw-r--r--activerecord/test/cases/associations/has_many_associations_test.rb16
-rw-r--r--activerecord/test/cases/associations/has_one_associations_test.rb18
-rw-r--r--activerecord/test/cases/finder_test.rb9
-rw-r--r--activerecord/test/cases/serialized_attribute_test.rb13
-rw-r--r--activerecord/test/models/drink_designer.rb6
-rw-r--r--activerecord/test/models/person.rb5
-rw-r--r--activerecord/test/support/stubs/strong_parameters.rb15
-rw-r--r--activestorage/README.md2
-rw-r--r--activestorage/package.json2
-rw-r--r--activesupport/CHANGELOG.md4
-rw-r--r--activesupport/lib/active_support/core_ext/date_and_time/calculations.rb4
-rw-r--r--activesupport/lib/active_support/inflector/methods.rb2
-rw-r--r--activesupport/test/autoloading_fixtures/raises_load_error.rb4
-rw-r--r--activesupport/test/constantize_test_cases.rb10
-rw-r--r--guides/source/6_0_release_notes.md24
-rw-r--r--guides/source/action_cable_overview.md53
-rw-r--r--guides/source/action_mailbox_basics.md110
-rw-r--r--guides/source/active_record_basics.md6
-rw-r--r--guides/source/active_storage_overview.md6
-rw-r--r--guides/source/association_basics.md4
-rw-r--r--guides/source/configuring.md3
-rw-r--r--guides/source/engines.md21
-rw-r--r--guides/source/i18n.md6
-rw-r--r--guides/source/routing.md2
-rw-r--r--guides/source/testing.md112
-rw-r--r--guides/source/upgrading_ruby_on_rails.md36
-rw-r--r--package.json1
-rw-r--r--railties/CHANGELOG.md23
-rw-r--r--railties/RDOC_MAIN.rdoc6
-rw-r--r--railties/lib/rails/application/configuration.rb16
-rw-r--r--railties/lib/rails/commands/credentials/USAGE21
-rw-r--r--railties/lib/rails/commands/credentials/credentials_command.rb56
-rw-r--r--railties/lib/rails/generators/rails/app/app_generator.rb2
-rw-r--r--railties/lib/rails/generators/rails/app/templates/app/javascript/channels/consumer.js2
-rw-r--r--railties/lib/rails/generators/rails/app/templates/app/javascript/packs/application.js.tt4
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/frontbase.yml.tt2
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/ibm_db.yml.tt2
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/jdbc.yml.tt2
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/jdbcmysql.yml.tt2
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/jdbcpostgresql.yml.tt2
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml.tt2
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/oracle.yml.tt2
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml.tt2
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/sqlserver.yml.tt2
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_6_0.rb.tt4
-rw-r--r--railties/lib/rails/generators/rails/app/templates/package.json.tt8
-rw-r--r--railties/lib/rails/generators/rails/app/templates/test/channels/application_cable/connection_test.rb.tt11
-rw-r--r--railties/lib/rails/test_unit/testing.rake2
-rw-r--r--railties/test/application/credentials_test.rb56
-rw-r--r--railties/test/application/multiple_applications_test.rb4
-rw-r--r--railties/test/application/rake/routes_test.rb3
-rw-r--r--railties/test/application/test_runner_test.rb15
-rw-r--r--railties/test/commands/routes_test.rb83
-rw-r--r--railties/test/credentials_test.rb65
-rw-r--r--railties/test/generators/app_generator_test.rb3
-rw-r--r--railties/test/generators/channel_generator_test.rb14
-rw-r--r--railties/test/generators/shared_generator_tests.rb4
-rw-r--r--tasks/release.rb47
-rw-r--r--yarn.lock5
129 files changed, 1750 insertions, 1059 deletions
diff --git a/README.md b/README.md
index 56d2b9909c..566136e2a2 100644
--- a/README.md
+++ b/README.md
@@ -44,11 +44,11 @@ or to generate the body of an email. In Rails, View generation is handled by [Ac
[Active Record](activerecord/README.rdoc), [Active Model](activemodel/README.rdoc), [Action Pack](actionpack/README.rdoc), and [Action View](actionview/README.rdoc) can each be used independently outside Rails.
In addition to that, Rails also comes with [Action Mailer](actionmailer/README.rdoc), a library
-to generate and send emails; [Active Job](activejob/README.md), a
-framework for declaring jobs and making them run on a variety of queuing
+to generate and send emails; [Action Mailbox](actionmailbox/README.md), a library to receive emails within a Rails application;
+[Active Job](activejob/README.md), a framework for declaring jobs and making them run on a variety of queuing
backends; [Action Cable](actioncable/README.md), a framework to
integrate WebSockets with a Rails application; [Active Storage](activestorage/README.md), a library to attach cloud
-and local files to Rails applications;
+and local files to Rails applications; [Action Text](actiontext/README.md), a library to handle rich text content;
and [Active Support](activesupport/README.rdoc), a collection
of utility classes and standard library extensions that are useful for Rails,
and may also be used independently outside Rails.
diff --git a/RELEASING_RAILS.md b/RELEASING_RAILS.md
index 287dd4fa12..7fa180cc43 100644
--- a/RELEASING_RAILS.md
+++ b/RELEASING_RAILS.md
@@ -109,13 +109,14 @@ browser.
This will stop you from looking silly when you push an RC to rubygems.org and
then realize it is broken.
-### Release to RubyGems and NPM.
-
-IMPORTANT: The Action Cable client and Action View's UJS adapter are released
-as NPM packages, so you must have Node.js installed, have an NPM account
-(npmjs.com), and be a package owner for `actioncable` and `rails-ujs` (you can
-check this via `npm owner ls actioncable` and `npm owner ls rails-ujs`) in
-order to do a full release. Do not release until you're set up with NPM!
+### Release to RubyGems and npm.
+
+IMPORTANT: Several gems have JavaScript components that are released as npm
+packages, so you must have Node.js installed, have an npm account (npmjs.com),
+and be a package owner for `@rails/actioncable`, `@rails/actiontext`,
+`@rails/activestorage`, and `@rails/ujs`. You can check this by making sure your
+npm user (`npm whoami`) is listed as an owner (`npm owner ls <pkg>`) of each
+package. Do not release until you're set up with npm!
The release task will sign the release tag. If you haven't got commit signing
set up, use https://git-scm.com/book/tr/v2/Git-Tools-Signing-Your-Work as a
diff --git a/actioncable/CHANGELOG.md b/actioncable/CHANGELOG.md
index 162de0df0b..f43a955a76 100644
--- a/actioncable/CHANGELOG.md
+++ b/actioncable/CHANGELOG.md
@@ -1,3 +1,7 @@
+* Merge [`action-cable-testing`](https://github.com/palkan/action-cable-testing) to Rails.
+
+ *Vladimir Dementyev*
+
* The JavaScript WebSocket client will no longer try to reconnect
when you call `reject_unauthorized_connection` on the connection.
diff --git a/actioncable/README.md b/actioncable/README.md
index a2783d6f45..60c879e1f4 100644
--- a/actioncable/README.md
+++ b/actioncable/README.md
@@ -7,550 +7,7 @@ and scalable. It's a full-stack offering that provides both a client-side
JavaScript framework and a server-side Ruby framework. You have access to your full
domain model written with Active Record or your ORM of choice.
-## Terminology
-
-A single Action Cable server can handle multiple connection instances. It has one
-connection instance per WebSocket connection. A single user may have multiple
-WebSockets open to your application if they use multiple browser tabs or devices.
-The client of a WebSocket connection is called the consumer.
-
-Each consumer can in turn subscribe to multiple cable channels. Each channel encapsulates
-a logical unit of work, similar to what a controller does in a regular MVC setup. For example,
-you could have a `ChatChannel` and an `AppearancesChannel`, and a consumer could be subscribed to either
-or to both of these channels. At the very least, a consumer should be subscribed to one channel.
-
-When the consumer is subscribed to a channel, they act as a subscriber. The connection between
-the subscriber and the channel is, surprise-surprise, called a subscription. A consumer
-can act as a subscriber to a given channel any number of times. For example, a consumer
-could subscribe to multiple chat rooms at the same time. (And remember that a physical user may
-have multiple consumers, one per tab/device open to your connection).
-
-Each channel can then again be streaming zero or more broadcastings. A broadcasting is a
-pubsub link where anything transmitted by the broadcaster is sent directly to the channel
-subscribers who are streaming that named broadcasting.
-
-As you can see, this is a fairly deep architectural stack. There's a lot of new terminology
-to identify the new pieces, and on top of that, you're dealing with both client and server side
-reflections of each unit.
-
-## Examples
-
-### A full-stack example
-
-The first thing you must do is define your `ApplicationCable::Connection` class in Ruby. This
-is the place where you authorize the incoming connection, and proceed to establish it,
-if all is well. Here's the simplest example starting with the server-side connection class:
-
-```ruby
-# app/channels/application_cable/connection.rb
-module ApplicationCable
- class Connection < ActionCable::Connection::Base
- identified_by :current_user
-
- def connect
- self.current_user = find_verified_user
- end
-
- private
- def find_verified_user
- if verified_user = User.find_by(id: cookies.encrypted[:user_id])
- verified_user
- else
- reject_unauthorized_connection
- end
- end
- end
-end
-```
-Here `identified_by` is a connection identifier that can be used to find the specific connection again or later.
-Note that anything marked as an identifier will automatically create a delegate by the same name on any channel instances created off the connection.
-
-This relies on the fact that you will already have handled authentication of the user, and
-that a successful authentication sets a signed cookie with the `user_id`. This cookie is then
-automatically sent to the connection instance when a new connection is attempted, and you
-use that to set the `current_user`. By identifying the connection by this same current_user,
-you're also ensuring that you can later retrieve all open connections by a given user (and
-potentially disconnect them all if the user is deleted or deauthorized).
-
-Next, you should define your `ApplicationCable::Channel` class in Ruby. This is the place where you put
-shared logic between your channels.
-
-```ruby
-# app/channels/application_cable/channel.rb
-module ApplicationCable
- class Channel < ActionCable::Channel::Base
- end
-end
-```
-
-The client-side needs to setup a consumer instance of this connection. That's done like so:
-
-```js
-// app/assets/javascripts/cable.js
-//= require action_cable
-//= require_self
-//= require_tree ./channels
-
-(function() {
- this.App || (this.App = {});
-
- App.cable = ActionCable.createConsumer("ws://cable.example.com");
-}).call(this);
-```
-
-The `ws://cable.example.com` address must point to your Action Cable server(s), and it
-must share a cookie namespace with the rest of the application (which may live under http://example.com).
-This ensures that the signed cookie will be correctly sent.
-
-That's all you need to establish the connection! But of course, this isn't very useful in
-itself. This just gives you the plumbing. To make stuff happen, you need content. That content
-is defined by declaring channels on the server and allowing the consumer to subscribe to them.
-
-
-### Channel example 1: User appearances
-
-Here's a simple example of a channel that tracks whether a user is online or not, and also what page they are currently on.
-(This is useful for creating presence features like showing a green dot next to a user's name if they're online).
-
-First you declare the server-side channel:
-
-```ruby
-# app/channels/appearance_channel.rb
-class AppearanceChannel < ApplicationCable::Channel
- def subscribed
- current_user.appear
- end
-
- def unsubscribed
- current_user.disappear
- end
-
- def appear(data)
- current_user.appear on: data['appearing_on']
- end
-
- def away
- current_user.away
- end
-end
-```
-
-The `#subscribed` callback is invoked when, as we'll show below, a client-side subscription is initiated. In this case,
-we take that opportunity to say "the current user has indeed appeared". That appear/disappear API could be backed by
-Redis or a database or whatever else. Here's what the client-side of that looks like:
-
-```coffeescript
-# app/assets/javascripts/cable/subscriptions/appearance.coffee
-App.cable.subscriptions.create "AppearanceChannel",
- # Called when the subscription is ready for use on the server
- connected: ->
- @install()
- @appear()
-
- # Called when the WebSocket connection is closed
- disconnected: ->
- @uninstall()
-
- # Called when the subscription is rejected by the server
- rejected: ->
- @uninstall()
-
- appear: ->
- # Calls `AppearanceChannel#appear(data)` on the server
- @perform("appear", appearing_on: $("main").data("appearing-on"))
-
- away: ->
- # Calls `AppearanceChannel#away` on the server
- @perform("away")
-
-
- buttonSelector = "[data-behavior~=appear_away]"
-
- install: ->
- $(document).on "turbolinks:load.appearance", =>
- @appear()
-
- $(document).on "click.appearance", buttonSelector, =>
- @away()
- false
-
- $(buttonSelector).show()
-
- uninstall: ->
- $(document).off(".appearance")
- $(buttonSelector).hide()
-```
-
-Simply calling `App.cable.subscriptions.create` will setup the subscription, which will call `AppearanceChannel#subscribed`,
-which in turn is linked to the original `App.cable` -> `ApplicationCable::Connection` instances.
-
-Next, we link the client-side `appear` method to `AppearanceChannel#appear(data)`. This is possible because the server-side
-channel instance will automatically expose the public methods declared on the class (minus the callbacks), so that these
-can be reached as remote procedure calls via a subscription's `perform` method.
-
-### Channel example 2: Receiving new web notifications
-
-The appearance example was all about exposing server functionality to client-side invocation over the WebSocket connection.
-But the great thing about WebSockets is that it's a two-way street. So now let's show an example where the server invokes
-an action on the client.
-
-This is a web notification channel that allows you to trigger client-side web notifications when you broadcast to the right
-streams:
-
-```ruby
-# app/channels/web_notifications_channel.rb
-class WebNotificationsChannel < ApplicationCable::Channel
- def subscribed
- stream_from "web_notifications_#{current_user.id}"
- end
-end
-```
-
-```coffeescript
-# Client-side, which assumes you've already requested the right to send web notifications
-App.cable.subscriptions.create "WebNotificationsChannel",
- received: (data) ->
- new Notification data["title"], body: data["body"]
-```
-
-```ruby
-# Somewhere in your app this is called, perhaps from a NewCommentJob
-ActionCable.server.broadcast \
- "web_notifications_#{current_user.id}", { title: 'New things!', body: 'All the news that is fit to print' }
-```
-
-The `ActionCable.server.broadcast` call places a message in the Action Cable pubsub queue under a separate broadcasting name for each user. For a user with an ID of 1, the broadcasting name would be `web_notifications_1`.
-The channel has been instructed to stream everything that arrives at `web_notifications_1` directly to the client by invoking the
-`#received(data)` callback. The data is the hash sent as the second parameter to the server-side broadcast call, JSON encoded for the trip
-across the wire, and unpacked for the data argument arriving to `#received`.
-
-
-### Passing Parameters to Channel
-
-You can pass parameters from the client side to the server side when creating a subscription. For example:
-
-```ruby
-# app/channels/chat_channel.rb
-class ChatChannel < ApplicationCable::Channel
- def subscribed
- stream_from "chat_#{params[:room]}"
- end
-end
-```
-
-If you pass an object as the first argument to `subscriptions.create`, that object will become the params hash in your cable channel. The keyword `channel` is required.
-
-```coffeescript
-# Client-side, which assumes you've already requested the right to send web notifications
-App.cable.subscriptions.create { channel: "ChatChannel", room: "Best Room" },
- received: (data) ->
- @appendLine(data)
-
- appendLine: (data) ->
- html = @createLine(data)
- $("[data-chat-room='Best Room']").append(html)
-
- createLine: (data) ->
- """
- <article class="chat-line">
- <span class="speaker">#{data["sent_by"]}</span>
- <span class="body">#{data["body"]}</span>
- </article>
- """
-```
-
-```ruby
-# Somewhere in your app this is called, perhaps from a NewCommentJob
-ActionCable.server.broadcast \
- "chat_#{room}", { sent_by: 'Paul', body: 'This is a cool chat app.' }
-```
-
-
-### Rebroadcasting message
-
-A common use case is to rebroadcast a message sent by one client to any other connected clients.
-
-```ruby
-# app/channels/chat_channel.rb
-class ChatChannel < ApplicationCable::Channel
- def subscribed
- stream_from "chat_#{params[:room]}"
- end
-
- def receive(data)
- ActionCable.server.broadcast "chat_#{params[:room]}", data
- end
-end
-```
-
-```coffeescript
-# Client-side, which assumes you've already requested the right to send web notifications
-App.chatChannel = App.cable.subscriptions.create { channel: "ChatChannel", room: "Best Room" },
- received: (data) ->
- # data => { sent_by: "Paul", body: "This is a cool chat app." }
-
-App.chatChannel.send({ sent_by: "Paul", body: "This is a cool chat app." })
-```
-
-The rebroadcast will be received by all connected clients, _including_ the client that sent the message. Note that params are the same as they were when you subscribed to the channel.
-
-
-### More complete examples
-
-See the [rails/actioncable-examples](https://github.com/rails/actioncable-examples) repository for a full example of how to setup Action Cable in a Rails app, and how to add channels.
-
-## Configuration
-
-Action Cable has three required configurations: a subscription adapter, allowed request origins, and the cable server URL (which can optionally be set on the client side).
-
-### Redis
-
-By default, `ActionCable::Server::Base` will look for a configuration file in `Rails.root.join('config/cable.yml')`.
-This file must specify an adapter and a URL for each Rails environment. It may use the following format:
-
-```yaml
-production: &production
- adapter: redis
- url: redis://10.10.3.153:6381
-development: &development
- adapter: redis
- url: redis://localhost:6379
-test: *development
-```
-
-You can also change the location of the Action Cable config file in a Rails initializer with something like:
-
-```ruby
-Rails.application.paths.add "config/cable", with: "somewhere/else/cable.yml"
-```
-
-### Allowed Request Origins
-
-Action Cable will only accept requests from specific origins.
-
-By default, only an origin matching the cable server itself will be permitted.
-Additional origins can be specified using strings or regular expressions, provided in an array.
-
-```ruby
-Rails.application.config.action_cable.allowed_request_origins = ['http://rubyonrails.com', /http:\/\/ruby.*/]
-```
-
-When running in the development environment, this defaults to "http://localhost:3000".
-
-To disable protection and allow requests from any origin:
-
-```ruby
-Rails.application.config.action_cable.disable_request_forgery_protection = true
-```
-
-To disable automatic access for same-origin requests, and strictly allow
-only the configured origins:
-
-```ruby
-Rails.application.config.action_cable.allow_same_origin_as_host = false
-```
-
-### Consumer Configuration
-
-Once you have decided how to run your cable server (see below), you must provide the server URL (or path) to your client-side setup.
-There are two ways you can do this.
-
-The first is to simply pass it in when creating your consumer. For a standalone server,
-this would be something like: `App.cable = ActionCable.createConsumer("ws://example.com:28080")`, and for an in-app server,
-something like: `App.cable = ActionCable.createConsumer("/cable")`.
-
-The second option is to pass the server URL through the `action_cable_meta_tag` in your layout.
-This uses a URL or path typically set via `config.action_cable.url` in the environment configuration files, or defaults to "/cable".
-
-This method is especially useful if your WebSocket URL might change between environments. If you host your production server via https, you will need to use the wss scheme
-for your Action Cable server, but development might remain http and use the ws scheme. You might use localhost in development and your
-domain in production.
-
-In any case, to vary the WebSocket URL between environments, add the following configuration to each environment:
-
-```ruby
-config.action_cable.url = "ws://example.com:28080"
-```
-
-Then add the following line to your layout before your JavaScript tag:
-
-```erb
-<%= action_cable_meta_tag %>
-```
-
-And finally, create your consumer like so:
-
-```coffeescript
-App.cable = ActionCable.createConsumer()
-```
-
-### Other Configurations
-
-The other common option to configure is the log tags applied to the per-connection logger. Here's an example that uses the user account id if available, else "no-account" while tagging:
-
-```ruby
-config.action_cable.log_tags = [
- -> request { request.env['user_account_id'] || "no-account" },
- :action_cable,
- -> request { request.uuid }
-]
-```
-
-For a full list of all configuration options, see the `ActionCable::Server::Configuration` class.
-
-Also note that your server must provide at least the same number of database connections as you have workers. The default worker pool is set to 4, so that means you have to make at least that available. You can change that in `config/database.yml` through the `pool` attribute.
-
-
-## Running the cable server
-
-### Standalone
-The cable server(s) is separated from your normal application server. It's still a Rack application, but it is its own Rack
-application. The recommended basic setup is as follows:
-
-```ruby
-# cable/config.ru
-require_relative '../config/environment'
-Rails.application.eager_load!
-
-run ActionCable.server
-```
-
-Then you start the server using a binstub in bin/cable ala:
-```sh
-#!/bin/bash
-bundle exec puma -p 28080 cable/config.ru
-```
-
-The above will start a cable server on port 28080.
-
-### In app
-
-If you are using a server that supports the [Rack socket hijacking API](https://www.rubydoc.info/github/rack/rack/file/SPEC#label-Hijacking), Action Cable can run alongside your Rails application. For example, to listen for WebSocket requests on `/websocket`, specify that path to `config.action_cable.mount_path`:
-
-```ruby
-# config/application.rb
-class Application < Rails::Application
- config.action_cable.mount_path = '/websocket'
-end
-```
-
-For every instance of your server you create and for every worker your server spawns, you will also have a new instance of Action Cable, but the use of Redis keeps messages synced across connections.
-
-### Notes
-
-Beware that currently, the cable server will _not_ auto-reload any changes in the framework. As we've discussed, long-running cable connections mean long-running objects. We don't yet have a way of reloading the classes of those objects in a safe manner. So when you change your channels, or the model your channels use, you must restart the cable server.
-
-We'll get all this abstracted properly when the framework is integrated into Rails.
-
-The WebSocket server doesn't have access to the session, but it has access to the cookies. This can be used when you need to handle authentication. You can see one way of doing that with Devise in this [article](https://greg.molnar.io/blog/actioncable-devise-authentication/).
-
-## Dependencies
-
-Action Cable provides a subscription adapter interface to process its pubsub internals. By default, asynchronous, inline, PostgreSQL, and Redis adapters are included. The default adapter in new Rails applications is the asynchronous (`async`) adapter. To create your own adapter, you can look at `ActionCable::SubscriptionAdapter::Base` for all methods that must be implemented, and any of the adapters included within Action Cable as example implementations.
-
-The Ruby side of things is built on top of [websocket-driver](https://github.com/faye/websocket-driver-ruby), [nio4r](https://github.com/celluloid/nio4r), and [concurrent-ruby](https://github.com/ruby-concurrency/concurrent-ruby).
-
-
-## Deployment
-
-Action Cable is powered by a combination of WebSockets and threads. All of the
-connection management is handled internally by utilizing Ruby's native thread
-support, which means you can use all your regular Rails models with no problems
-as long as you haven't committed any thread-safety sins.
-
-The Action Cable server does _not_ need to be a multi-threaded application server.
-This is because Action Cable uses the [Rack socket hijacking API](https://www.rubydoc.info/github/rack/rack/file/SPEC#label-Hijacking)
-to take over control of connections from the application server. Action Cable
-then manages connections internally, in a multithreaded manner, regardless of
-whether the application server is multi-threaded or not. So Action Cable works
-with all the popular application servers -- Unicorn, Puma and Passenger.
-
-Action Cable does not work with WEBrick, because WEBrick does not support the
-Rack socket hijacking API.
-
-## Frontend assets
-
-Action Cable's frontend assets are distributed through two channels: the
-official gem and npm package, both titled `actioncable`.
-
-### Gem usage
-
-Through the `actioncable` gem, Action Cable's frontend assets are
-available through the Rails Asset Pipeline. Create a `cable.js` or
-`cable.coffee` file (this is automatically done for you with Rails
-generators), and then simply require the assets:
-
-In JavaScript...
-
-```javascript
-//= require action_cable
-```
-
-... and in CoffeeScript:
-
-```coffeescript
-#= require action_cable
-```
-
-### npm usage
-
-In addition to being available through the `actioncable` gem, Action Cable's
-frontend JS assets are also bundled in an officially supported npm module,
-intended for usage in standalone frontend applications that communicate with a
-Rails application. A common use case for this could be if you have a decoupled
-frontend application written in React, Ember.js, etc. and want to add real-time
-WebSocket functionality.
-
-### Installation
-
-```
-npm install actioncable --save
-```
-
-### Usage
-
-The `ActionCable` constant is available as a `require`-able module, so
-you only have to require the package to gain access to the API that is
-provided.
-
-In JavaScript...
-
-```javascript
-ActionCable = require('actioncable')
-
-var cable = ActionCable.createConsumer('wss://RAILS-API-PATH.com/cable')
-
-cable.subscriptions.create('AppearanceChannel', {
- // normal channel code goes here...
-});
-```
-
-and in CoffeeScript...
-
-```coffeescript
-ActionCable = require('actioncable')
-
-cable = ActionCable.createConsumer('wss://RAILS-API-PATH.com/cable')
-
-cable.subscriptions.create 'AppearanceChannel',
- # normal channel code goes here...
-```
-
-## Download and Installation
-
-The latest version of Action Cable can be installed with [RubyGems](#gem-usage),
-or with [npm](#npm-usage).
-
-Source code can be downloaded as part of the Rails project on GitHub
-
-* https://github.com/rails/rails/tree/master/actioncable
-
-## License
-
-Action Cable is released under the MIT license:
-
-* https://opensource.org/licenses/MIT
-
+You can read more about Action Cable in the [Action Cable Overview](https://edgeguides.rubyonrails.org/action_cable_overview.html) guide.
## Support
diff --git a/actioncable/app/assets/javascripts/action_cable.js b/actioncable/app/assets/javascripts/action_cable.js
index 65e32d6c3f..a68c76f299 100644
--- a/actioncable/app/assets/javascripts/action_cable.js
+++ b/actioncable/app/assets/javascripts/action_cable.js
@@ -192,7 +192,7 @@
this.monitor.stop();
}
if (this.isActive()) {
- return this.webSocket ? this.webSocket.close() : undefined;
+ return this.webSocket.close();
}
};
Connection.prototype.reopen = function reopen() {
@@ -211,7 +211,9 @@
}
};
Connection.prototype.getProtocol = function getProtocol() {
- return this.webSocket ? this.webSocket.protocol : undefined;
+ if (this.webSocket) {
+ return this.webSocket.protocol;
+ }
};
Connection.prototype.isOpen = function isOpen() {
return this.isState("open");
@@ -452,16 +454,15 @@
};
return Consumer;
}();
- function createConsumer(url) {
- if (url == null) {
- var urlConfig = getConfig("url");
- url = urlConfig ? urlConfig : INTERNAL.default_mount_path;
- }
+ function createConsumer() {
+ var url = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : getConfig("url") || INTERNAL.default_mount_path;
return new Consumer(createWebSocketURL(url));
}
function getConfig(name) {
var element = document.head.querySelector("meta[name='action-cable-" + name + "']");
- return element ? element.getAttribute("content") : undefined;
+ if (element) {
+ return element.getAttribute("content");
+ }
}
function createWebSocketURL(url) {
if (url && !/^wss?:/i.test(url)) {
diff --git a/actioncable/app/javascript/action_cable/connection.js b/actioncable/app/javascript/action_cable/connection.js
index b2910cb2a6..96bac132c1 100644
--- a/actioncable/app/javascript/action_cable/connection.js
+++ b/actioncable/app/javascript/action_cable/connection.js
@@ -44,7 +44,9 @@ class Connection {
close({allowReconnect} = {allowReconnect: true}) {
if (!allowReconnect) { this.monitor.stop() }
- if (this.isActive()) { return (this.webSocket ? this.webSocket.close() : undefined) }
+ if (this.isActive()) {
+ return this.webSocket.close()
+ }
}
reopen() {
@@ -65,7 +67,9 @@ class Connection {
}
getProtocol() {
- return (this.webSocket ? this.webSocket.protocol : undefined)
+ if (this.webSocket) {
+ return this.webSocket.protocol
+ }
}
isOpen() {
diff --git a/actioncable/app/javascript/action_cable/index.js b/actioncable/app/javascript/action_cable/index.js
index 9f41c14e94..659418396f 100644
--- a/actioncable/app/javascript/action_cable/index.js
+++ b/actioncable/app/javascript/action_cable/index.js
@@ -18,17 +18,15 @@ export {
logger,
}
-export function createConsumer(url) {
- if (url == null) {
- const urlConfig = getConfig("url")
- url = (urlConfig ? urlConfig : INTERNAL.default_mount_path)
- }
+export function createConsumer(url = getConfig("url") || INTERNAL.default_mount_path) {
return new Consumer(createWebSocketURL(url))
}
export function getConfig(name) {
const element = document.head.querySelector(`meta[name='action-cable-${name}']`)
- return (element ? element.getAttribute("content") : undefined)
+ if (element) {
+ return element.getAttribute("content")
+ }
}
export function createWebSocketURL(url) {
diff --git a/actioncable/lib/action_cable/connection.rb b/actioncable/lib/action_cable/connection.rb
index 804b89a707..20b5dbe78d 100644
--- a/actioncable/lib/action_cable/connection.rb
+++ b/actioncable/lib/action_cable/connection.rb
@@ -15,6 +15,7 @@ module ActionCable
autoload :StreamEventLoop
autoload :Subscriptions
autoload :TaggedLoggerProxy
+ autoload :TestCase
autoload :WebSocket
end
end
diff --git a/actioncable/lib/action_cable/connection/test_case.rb b/actioncable/lib/action_cable/connection/test_case.rb
new file mode 100644
index 0000000000..26a183d1ec
--- /dev/null
+++ b/actioncable/lib/action_cable/connection/test_case.rb
@@ -0,0 +1,236 @@
+# frozen_string_literal: true
+
+require "active_support"
+require "active_support/test_case"
+require "active_support/core_ext/hash/indifferent_access"
+require "action_dispatch"
+require "action_dispatch/http/headers"
+require "action_dispatch/testing/test_request"
+
+module ActionCable
+ module Connection
+ class NonInferrableConnectionError < ::StandardError
+ def initialize(name)
+ super "Unable to determine the connection to test from #{name}. " +
+ "You'll need to specify it using `tests YourConnection` in your " +
+ "test case definition."
+ end
+ end
+
+ module Assertions
+ # Asserts that the connection is rejected (via +reject_unauthorized_connection+).
+ #
+ # # Asserts that connection without user_id fails
+ # assert_reject_connection { connect params: { user_id: '' } }
+ def assert_reject_connection(&block)
+ assert_raises(Authorization::UnauthorizedError, "Expected to reject connection but no rejection was made", &block)
+ end
+ end
+
+ # We don't want to use the whole "encryption stack" for connection
+ # unit-tests, but we want to make sure that users test against the correct types
+ # of cookies (i.e. signed or encrypted or plain)
+ class TestCookieJar < ActiveSupport::HashWithIndifferentAccess
+ def signed
+ self[:signed] ||= {}.with_indifferent_access
+ end
+
+ def encrypted
+ self[:encrypted] ||= {}.with_indifferent_access
+ end
+ end
+
+ class TestRequest < ActionDispatch::TestRequest
+ attr_accessor :session, :cookie_jar
+
+ attr_writer :cookie_jar
+ end
+
+ module TestConnection
+ attr_reader :logger, :request
+
+ def initialize(request)
+ inner_logger = ActiveSupport::Logger.new(StringIO.new)
+ tagged_logging = ActiveSupport::TaggedLogging.new(inner_logger)
+ @logger = ActionCable::Connection::TaggedLoggerProxy.new(tagged_logging, tags: [])
+ @request = request
+ @env = request.env
+ end
+ end
+
+ # Unit test Action Cable connections.
+ #
+ # Useful to check whether a connection's +identified_by+ gets assigned properly
+ # and that any improper connection requests are rejected.
+ #
+ # == Basic example
+ #
+ # Unit tests are written as follows:
+ #
+ # 1. Simulate a connection attempt by calling +connect+.
+ # 2. Assert state, e.g. identifiers, has been assigned.
+ #
+ #
+ # class ApplicationCable::ConnectionTest < ActionCable::Connection::TestCase
+ # def test_connects_with_proper_cookie
+ # # Simulate the connection request with a cookie.
+ # cookies["user_id"] = users(:john).id
+ #
+ # connect
+ #
+ # # Assert the connection identifier matches the fixture.
+ # assert_equal users(:john).id, connection.user.id
+ # end
+ #
+ # def test_rejects_connection_without_proper_cookie
+ # assert_reject_connection { connect }
+ # end
+ # end
+ #
+ # +connect+ accepts additional information the HTTP request with the
+ # +params+, +headers+, +session+ and Rack +env+ options.
+ #
+ # def test_connect_with_headers_and_query_string
+ # connect params: { user_id: 1 }, headers: { "X-API-TOKEN" => "secret-my" }
+ #
+ # assert_equal "1", connection.user.id
+ # assert_equal "secret-my", connection.token
+ # end
+ #
+ # def test_connect_with_params
+ # connect params: { user_id: 1 }
+ #
+ # assert_equal "1", connection.user.id
+ # end
+ #
+ # You can also setup the correct cookies before the connection request:
+ #
+ # def test_connect_with_cookies
+ # # Plain cookies:
+ # cookies["user_id"] = 1
+ #
+ # # Or signed/encrypted:
+ # # cookies.signed["user_id"] = 1
+ # # cookies.encrypted["user_id"] = 1
+ #
+ # connect
+ #
+ # assert_equal "1", connection.user_id
+ # end
+ #
+ # == Connection is automatically inferred
+ #
+ # ActionCable::Connection::TestCase will automatically infer the connection under test
+ # from the test class name. If the channel cannot be inferred from the test
+ # class name, you can explicitly set it with +tests+.
+ #
+ # class ConnectionTest < ActionCable::Connection::TestCase
+ # tests ApplicationCable::Connection
+ # end
+ #
+ class TestCase < ActiveSupport::TestCase
+ module Behavior
+ extend ActiveSupport::Concern
+
+ DEFAULT_PATH = "/cable"
+
+ include ActiveSupport::Testing::ConstantLookup
+ include Assertions
+
+ included do
+ class_attribute :_connection_class
+
+ attr_reader :connection
+
+ ActiveSupport.run_load_hooks(:action_cable_connection_test_case, self)
+ end
+
+ module ClassMethods
+ def tests(connection)
+ case connection
+ when String, Symbol
+ self._connection_class = connection.to_s.camelize.constantize
+ when Module
+ self._connection_class = connection
+ else
+ raise NonInferrableConnectionError.new(connection)
+ end
+ end
+
+ def connection_class
+ if connection = self._connection_class
+ connection
+ else
+ tests determine_default_connection(name)
+ end
+ end
+
+ def determine_default_connection(name)
+ connection = determine_constant_from_test_name(name) do |constant|
+ Class === constant && constant < ActionCable::Connection::Base
+ end
+ raise NonInferrableConnectionError.new(name) if connection.nil?
+ connection
+ end
+ end
+
+ # Performs connection attempt to exert #connect on the connection under test.
+ #
+ # Accepts request path as the first argument and the following request options:
+ #
+ # - params – url parameters (Hash)
+ # - headers – request headers (Hash)
+ # - session – session data (Hash)
+ # - env – additional Rack env configuration (Hash)
+ def connect(path = ActionCable.server.config.mount_path, **request_params)
+ path ||= DEFAULT_PATH
+
+ connection = self.class.connection_class.allocate
+ connection.singleton_class.include(TestConnection)
+ connection.send(:initialize, build_test_request(path, request_params))
+ connection.connect if connection.respond_to?(:connect)
+
+ # Only set instance variable if connected successfully
+ @connection = connection
+ end
+
+ # Exert #disconnect on the connection under test.
+ def disconnect
+ raise "Must be connected!" if connection.nil?
+
+ connection.disconnect if connection.respond_to?(:disconnect)
+ @connection = nil
+ end
+
+ def cookies
+ @cookie_jar ||= TestCookieJar.new
+ end
+
+ private
+ def build_test_request(path, params: nil, headers: {}, session: {}, env: {})
+ wrapped_headers = ActionDispatch::Http::Headers.from_hash(headers)
+
+ uri = URI.parse(path)
+
+ query_string = params.nil? ? uri.query : params.to_query
+
+ request_env = {
+ "QUERY_STRING" => query_string,
+ "PATH_INFO" => uri.path
+ }.merge(env)
+
+ if wrapped_headers.present?
+ ActionDispatch::Http::Headers.from_hash(request_env).merge!(wrapped_headers)
+ end
+
+ TestRequest.create(request_env).tap do |request|
+ request.session = session.with_indifferent_access
+ request.cookie_jar = cookies
+ end
+ end
+ end
+
+ include Behavior
+ end
+ end
+end
diff --git a/actioncable/lib/rails/generators/channel/USAGE b/actioncable/lib/rails/generators/channel/USAGE
index ea9662436c..bb5dd7e2db 100644
--- a/actioncable/lib/rails/generators/channel/USAGE
+++ b/actioncable/lib/rails/generators/channel/USAGE
@@ -7,6 +7,7 @@ Example:
========
rails generate channel Chat speak
- creates a Chat channel class and JavaScript asset:
+ creates a Chat channel class, test and JavaScript asset:
Channel: app/channels/chat_channel.rb
+ Test: test/channels/chat_channel_test.rb
Assets: app/javascript/channels/chat_channel.js
diff --git a/actioncable/lib/rails/generators/channel/channel_generator.rb b/actioncable/lib/rails/generators/channel/channel_generator.rb
index ef51981e89..0b80d1f96b 100644
--- a/actioncable/lib/rails/generators/channel/channel_generator.rb
+++ b/actioncable/lib/rails/generators/channel/channel_generator.rb
@@ -11,6 +11,8 @@ module Rails
check_class_collision suffix: "Channel"
+ hook_for :test_framework
+
def create_channel_file
template "channel.rb", File.join("app/channels", class_path, "#{file_name}_channel.rb")
diff --git a/actioncable/lib/rails/generators/channel/templates/javascript/consumer.js.tt b/actioncable/lib/rails/generators/channel/templates/javascript/consumer.js.tt
index 76ca3d0f2f..eec7e54b8a 100644
--- a/actioncable/lib/rails/generators/channel/templates/javascript/consumer.js.tt
+++ b/actioncable/lib/rails/generators/channel/templates/javascript/consumer.js.tt
@@ -1,6 +1,6 @@
// Action Cable provides the framework to deal with WebSockets in Rails.
// You can generate new channels where WebSocket features live using the `rails generate channel` command.
-import ActionCable from "actioncable"
+import ActionCable from "@rails/actioncable"
export default ActionCable.createConsumer()
diff --git a/actioncable/lib/rails/generators/test_unit/channel_generator.rb b/actioncable/lib/rails/generators/test_unit/channel_generator.rb
new file mode 100644
index 0000000000..7d13a12f0a
--- /dev/null
+++ b/actioncable/lib/rails/generators/test_unit/channel_generator.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module TestUnit
+ module Generators
+ class ChannelGenerator < ::Rails::Generators::NamedBase
+ source_root File.expand_path("templates", __dir__)
+
+ check_class_collision suffix: "ChannelTest"
+
+ def create_test_files
+ template "channel_test.rb", File.join("test/channels", class_path, "#{file_name}_channel_test.rb")
+ end
+
+ private
+ def file_name # :doc:
+ @_file_name ||= super.sub(/_channel\z/i, "")
+ end
+ end
+ end
+end
diff --git a/actioncable/lib/rails/generators/test_unit/templates/channel_test.rb.tt b/actioncable/lib/rails/generators/test_unit/templates/channel_test.rb.tt
new file mode 100644
index 0000000000..7307654611
--- /dev/null
+++ b/actioncable/lib/rails/generators/test_unit/templates/channel_test.rb.tt
@@ -0,0 +1,8 @@
+require "test_helper"
+
+class <%= class_name %>ChannelTest < ActionCable::Channel::TestCase
+ # test "subscribes" do
+ # subscribe
+ # assert subscription.confirmed?
+ # end
+end
diff --git a/actioncable/package.json b/actioncable/package.json
index db78c1a09a..69a97939fb 100644
--- a/actioncable/package.json
+++ b/actioncable/package.json
@@ -1,5 +1,5 @@
{
- "name": "actioncable",
+ "name": "@rails/actioncable",
"version": "6.0.0-alpha",
"description": "WebSocket framework for Ruby on Rails.",
"main": "app/assets/javascripts/action_cable.js",
diff --git a/actioncable/test/connection/test_case_test.rb b/actioncable/test/connection/test_case_test.rb
new file mode 100644
index 0000000000..3b19465d7b
--- /dev/null
+++ b/actioncable/test/connection/test_case_test.rb
@@ -0,0 +1,197 @@
+# frozen_string_literal: true
+
+require "test_helper"
+
+class SimpleConnection < ActionCable::Connection::Base
+ identified_by :user_id
+
+ class << self
+ attr_accessor :disconnected_user_id
+ end
+
+ def connect
+ self.user_id = request.params[:user_id] || cookies[:user_id]
+ end
+
+ def disconnect
+ self.class.disconnected_user_id = user_id
+ end
+end
+
+class ConnectionSimpleTest < ActionCable::Connection::TestCase
+ tests SimpleConnection
+
+ def test_connected
+ connect
+
+ assert_nil connection.user_id
+ end
+
+ def test_url_params
+ connect "/cable?user_id=323"
+
+ assert_equal "323", connection.user_id
+ end
+
+ def test_params
+ connect params: { user_id: 323 }
+
+ assert_equal "323", connection.user_id
+ end
+
+ def test_plain_cookie
+ cookies["user_id"] = "456"
+
+ connect
+
+ assert_equal "456", connection.user_id
+ end
+
+ def test_disconnect
+ cookies["user_id"] = "456"
+
+ connect
+
+ assert_equal "456", connection.user_id
+
+ disconnect
+
+ assert_equal "456", SimpleConnection.disconnected_user_id
+ end
+end
+
+class Connection < ActionCable::Connection::Base
+ identified_by :current_user_id
+ identified_by :token
+
+ class << self
+ attr_accessor :disconnected_user_id
+ end
+
+ def connect
+ self.current_user_id = verify_user
+ self.token = request.headers["X-API-TOKEN"]
+ logger.add_tags("ActionCable")
+ end
+
+ private
+ def verify_user
+ cookies.signed[:user_id].presence || reject_unauthorized_connection
+ end
+end
+
+class ConnectionTest < ActionCable::Connection::TestCase
+ def test_connected_with_signed_cookies_and_headers
+ cookies.signed["user_id"] = "456"
+
+ connect headers: { "X-API-TOKEN" => "abc" }
+
+ assert_equal "abc", connection.token
+ assert_equal "456", connection.current_user_id
+ end
+
+ def test_connected_when_no_signed_cookies_set
+ cookies["user_id"] = "456"
+
+ assert_reject_connection { connect }
+ end
+
+ def test_connection_rejected
+ assert_reject_connection { connect }
+ end
+
+ def test_connection_rejected_assertion_message
+ error = assert_raises Minitest::Assertion do
+ assert_reject_connection { "Intentionally doesn't connect." }
+ end
+
+ assert_match(/Expected to reject connection/, error.message)
+ end
+end
+
+class EncryptedCookiesConnection < ActionCable::Connection::Base
+ identified_by :user_id
+
+ def connect
+ self.user_id = verify_user
+ end
+
+ private
+ def verify_user
+ cookies.encrypted[:user_id].presence || reject_unauthorized_connection
+ end
+end
+
+class EncryptedCookiesConnectionTest < ActionCable::Connection::TestCase
+ tests EncryptedCookiesConnection
+
+ def test_connected_with_encrypted_cookies
+ cookies.encrypted["user_id"] = "456"
+
+ connect
+
+ assert_equal "456", connection.user_id
+ end
+
+ def test_connection_rejected
+ assert_reject_connection { connect }
+ end
+end
+
+class SessionConnection < ActionCable::Connection::Base
+ identified_by :user_id
+
+ def connect
+ self.user_id = verify_user
+ end
+
+ private
+ def verify_user
+ request.session[:user_id].presence || reject_unauthorized_connection
+ end
+end
+
+class SessionConnectionTest < ActionCable::Connection::TestCase
+ tests SessionConnection
+
+ def test_connected_with_encrypted_cookies
+ connect session: { user_id: "789" }
+ assert_equal "789", connection.user_id
+ end
+
+ def test_connection_rejected
+ assert_reject_connection { connect }
+ end
+end
+
+class EnvConnection < ActionCable::Connection::Base
+ identified_by :user
+
+ def connect
+ self.user = verify_user
+ end
+
+ private
+ def verify_user
+ # Warden-like authentication
+ env["authenticator"]&.user || reject_unauthorized_connection
+ end
+end
+
+class EnvConnectionTest < ActionCable::Connection::TestCase
+ tests EnvConnection
+
+ def test_connected_with_env
+ authenticator = Class.new do
+ def user; "David"; end
+ end
+
+ connect env: { "authenticator" => authenticator.new }
+
+ assert_equal "David", connection.user
+ end
+
+ def test_connection_rejected
+ assert_reject_connection { connect }
+ end
+end
diff --git a/actionmailbox/README.md b/actionmailbox/README.md
index f0bde03961..9a47223d3b 100644
--- a/actionmailbox/README.md
+++ b/actionmailbox/README.md
@@ -1,6 +1,6 @@
# Action Mailbox
-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.
+Action Mailbox routes incoming emails to controller-like mailboxes for processing in Rails. It ships with ingresses for Amazon SES, Mailgun, Mandrill, Postmark, and SendGrid. You can also handle inbound mails directly via the built-in Exim, Postfix, and Qmail ingresses.
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.
diff --git a/actionmailbox/app/controllers/action_mailbox/ingresses/amazon/inbound_emails_controller.rb b/actionmailbox/app/controllers/action_mailbox/ingresses/amazon/inbound_emails_controller.rb
index cf45ac8408..e0a187054e 100644
--- a/actionmailbox/app/controllers/action_mailbox/ingresses/amazon/inbound_emails_controller.rb
+++ b/actionmailbox/app/controllers/action_mailbox/ingresses/amazon/inbound_emails_controller.rb
@@ -37,7 +37,7 @@ module ActionMailbox
def self.prepare
self.verifier ||= begin
- require "aws-sdk-sns/message_verifier"
+ require "aws-sdk-sns"
Aws::SNS::MessageVerifier.new
end
end
diff --git a/actionmailbox/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb b/actionmailbox/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb
index ebe83a9ec0..bf0fd562fe 100644
--- a/actionmailbox/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb
+++ b/actionmailbox/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb
@@ -38,7 +38,7 @@ module ActionMailbox
# config.action_mailbox.ingress = :mailgun
#
# 3. {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`.
+ # to forward inbound emails to +/rails/action_mailbox/mailgun/inbound_emails/mime+.
#
# If your application lived at <tt>https://example.com</tt>, you would specify the fully-qualified URL
# <tt>https://example.com/rails/action_mailbox/mailgun/inbound_emails/mime</tt>.
diff --git a/actionmailbox/app/controllers/action_mailbox/ingresses/postmark/inbound_emails_controller.rb b/actionmailbox/app/controllers/action_mailbox/ingresses/postmark/inbound_emails_controller.rb
new file mode 100644
index 0000000000..309085c82a
--- /dev/null
+++ b/actionmailbox/app/controllers/action_mailbox/ingresses/postmark/inbound_emails_controller.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+module ActionMailbox
+ # Ingests inbound emails from Postmark. Requires a +RawEmail+ parameter containing a full RFC 822 message.
+ #
+ # Authenticates requests using HTTP basic access authentication. The username is always +actionmailbox+, and the
+ # password is read from the application's encrypted credentials or an environment variable. See the Usage section below.
+ #
+ # Note that basic authentication is insecure over unencrypted HTTP. An attacker that intercepts cleartext requests to
+ # the Postmark ingress can learn its password. You should only use the Postmark ingress over HTTPS.
+ #
+ # Returns:
+ #
+ # - <tt>204 No Content</tt> if an inbound email is successfully recorded and enqueued for routing to the appropriate mailbox
+ # - <tt>401 Unauthorized</tt> if the request's signature could not be validated
+ # - <tt>404 Not Found</tt> if Action Mailbox is not configured to accept inbound emails from Postmark
+ # - <tt>422 Unprocessable Entity</tt> if the request is missing the required +RawEmail+ parameter
+ # - <tt>500 Server Error</tt> if the ingress password is not configured, or if one of the Active Record database,
+ # the Active Storage service, or the Active Job backend is misconfigured or unavailable
+ #
+ # == Usage
+ #
+ # 1. Tell Action Mailbox to accept emails from Postmark:
+ #
+ # # config/environments/production.rb
+ # config.action_mailbox.ingress = :postmark
+ #
+ # 2. Generate a strong password that Action Mailbox can use to authenticate requests to the Postmark ingress.
+ #
+ # Use <tt>rails credentials:edit</tt> to add the password to your application's encrypted credentials under
+ # +action_mailbox.ingress_password+, where Action Mailbox will automatically find it:
+ #
+ # action_mailbox:
+ # ingress_password: ...
+ #
+ # Alternatively, provide the password in the +RAILS_INBOUND_EMAIL_PASSWORD+ environment variable.
+ #
+ # 3. {Configure Postmark}[https://postmarkapp.com/manual#configure-your-inbound-webhook-url] to forward inbound emails
+ # to +/rails/action_mailbox/postmark/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 your
+ # Postmark inbound webhook with the following fully-qualified URL:
+ #
+ # https://actionmailbox:PASSWORD@example.com/rails/action_mailbox/postmark/inbound_emails
+ #
+ # *NOTE:* When configuring your Postmark inbound webhook, be sure to check the box labeled *"Include raw email
+ # content in JSON payload"*. Action Mailbox needs the raw email content to work.
+ class Ingresses::Postmark::InboundEmailsController < ActionMailbox::BaseController
+ before_action :authenticate_by_password
+
+ def create
+ ActionMailbox::InboundEmail.create_and_extract_message_id! params.require("RawEmail")
+ rescue ActionController::ParameterMissing => error
+ logger.error <<~MESSAGE
+ #{error.message}
+
+ When configuring your Postmark inbound webhook, be sure to check the box
+ labeled "Include raw email content in JSON payload".
+ MESSAGE
+ head :unprocessable_entity
+ end
+ end
+end
diff --git a/actionmailbox/app/controllers/action_mailbox/ingresses/postfix/inbound_emails_controller.rb b/actionmailbox/app/controllers/action_mailbox/ingresses/relay/inbound_emails_controller.rb
index c0d5002a12..2d91c968c8 100644
--- a/actionmailbox/app/controllers/action_mailbox/ingresses/postfix/inbound_emails_controller.rb
+++ b/actionmailbox/app/controllers/action_mailbox/ingresses/relay/inbound_emails_controller.rb
@@ -1,31 +1,31 @@
# frozen_string_literal: true
module ActionMailbox
- # Ingests inbound emails relayed from Postfix.
+ # Ingests inbound emails relayed from an SMTP server.
#
# Authenticates requests using HTTP basic access authentication. The username is always +actionmailbox+, and the
# password is read from the application's encrypted credentials or an environment variable. See the Usage section below.
#
# Note that basic authentication is insecure over unencrypted HTTP. An attacker that intercepts cleartext requests to
- # the Postfix ingress can learn its password. You should only use the Postfix ingress over HTTPS.
+ # the ingress can learn its password. You should only use this ingress over HTTPS.
#
# Returns:
#
# - <tt>204 No Content</tt> if an inbound email is successfully recorded and enqueued for routing to the appropriate mailbox
# - <tt>401 Unauthorized</tt> if the request could not be authenticated
- # - <tt>404 Not Found</tt> if Action Mailbox is not configured to accept inbound emails from Postfix
+ # - <tt>404 Not Found</tt> if Action Mailbox is not configured to accept inbound emails relayed from an SMTP server
# - <tt>415 Unsupported Media Type</tt> if the request does not contain an RFC 822 message
# - <tt>500 Server Error</tt> if the ingress password is not configured, or if one of the Active Record database,
# the Active Storage service, or the Active Job backend is misconfigured or unavailable
#
# == Usage
#
- # 1. Tell Action Mailbox to accept emails from Postfix:
+ # 1. Tell Action Mailbox to accept emails from an SMTP relay:
#
# # config/environments/production.rb
- # config.action_mailbox.ingress = :postfix
+ # config.action_mailbox.ingress = :relay
#
- # 2. Generate a strong password that Action Mailbox can use to authenticate requests to the Postfix ingress.
+ # 2. Generate a strong password that Action Mailbox can use to authenticate requests to the ingress.
#
# Use <tt>rails credentials:edit</tt> to add the password to your application's encrypted credentials under
# +action_mailbox.ingress_password+, where Action Mailbox will automatically find it:
@@ -35,14 +35,20 @@ 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]
- # 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.
+ # 3. Configure your SMTP server to pipe inbound emails to the appropriate ingress command, providing the +URL+ of the
+ # relay ingress and the +INGRESS_PASSWORD+ you previously generated.
#
- # If your application lived at <tt>https://example.com</tt>, the full command would look like this:
+ # If your application lives at <tt>https://example.com</tt>, you would configure the Postfix SMTP server to pipe
+ # inbound emails to the following command:
#
- # URL=https://example.com/rails/action_mailbox/postfix/inbound_emails INGRESS_PASSWORD=... bin/rails action_mailbox:ingress:postfix
- class Ingresses::Postfix::InboundEmailsController < ActionMailbox::BaseController
+ # bin/rails action_mailbox:ingress:postfix URL=https://example.com/rails/action_mailbox/postfix/inbound_emails INGRESS_PASSWORD=...
+ #
+ # Built-in ingress commands are available for these popular SMTP servers:
+ #
+ # - Exim (<tt>bin/rails action_mailbox:ingress:exim)
+ # - Postfix (<tt>bin/rails action_mailbox:ingress:postfix)
+ # - Qmail (<tt>bin/rails action_mailbox:ingress:qmail)
+ class Ingresses::Relay::InboundEmailsController < ActionMailbox::BaseController
before_action :authenticate_by_password, :require_valid_rfc822_message
def create
diff --git a/actionmailbox/app/jobs/action_mailbox/incineration_job.rb b/actionmailbox/app/jobs/action_mailbox/incineration_job.rb
index 224c9329a5..1579a3c7c8 100644
--- a/actionmailbox/app/jobs/action_mailbox/incineration_job.rb
+++ b/actionmailbox/app/jobs/action_mailbox/incineration_job.rb
@@ -1,10 +1,10 @@
# frozen_string_literal: true
module ActionMailbox
- # You can configure when this `IncinerationJob` will be run as a time-after-processing using the
- # `config.action_mailbox.incinerate_after` or `ActionMailbox.incinerate_after` setting.
+ # You can configure when this +IncinerationJob+ will be run as a time-after-processing using the
+ # +config.action_mailbox.incinerate_after+ or +ActionMailbox.incinerate_after+ setting.
#
- # Since this incineration is set for the future, it'll automatically ignore any `InboundEmail`s
+ # Since this incineration is set for the future, it'll automatically ignore any <tt>InboundEmail</tt>s
# that have already been deleted and discard itself if so.
class IncinerationJob < ActiveJob::Base
queue_as { ActionMailbox.queues[:incineration] }
diff --git a/actionmailbox/app/models/action_mailbox/inbound_email.rb b/actionmailbox/app/models/action_mailbox/inbound_email.rb
index 3a8dfd163c..023de19ccc 100644
--- a/actionmailbox/app/models/action_mailbox/inbound_email.rb
+++ b/actionmailbox/app/models/action_mailbox/inbound_email.rb
@@ -3,22 +3,22 @@
require "mail"
module ActionMailbox
- # The `InboundEmail` is an Active Record that keeps a reference to the raw email stored in Active Storage
+ # The +InboundEmail+ is an Active Record that keeps a reference to the raw email stored in Active Storage
# and tracks the status of processing. By default, incoming emails will go through the following lifecycle:
#
# * Pending: Just received by one of the ingress controllers and scheduled for routing.
# * Processing: During active processing, while a specific mailbox is running its #process method.
# * Delivered: Successfully processed by the specific mailbox.
- # * Failed: An exception was raised during the specific mailbox's execution of the `#process` method.
+ # * Failed: An exception was raised during the specific mailbox's execution of the +#process+ method.
# * Bounced: Rejected processing by the specific mailbox and bounced to sender.
#
- # Once the `InboundEmail` has reached the status of being either `delivered`, `failed`, or `bounced`,
- # it'll count as having been `#processed?`. Once processed, the `InboundEmail` will be scheduled for
+ # Once the +InboundEmail+ has reached the status of being either +delivered+, +failed+, or +bounced+,
+ # it'll count as having been +#processed?+. Once processed, the +InboundEmail+ will be scheduled for
# automatic incineration at a later point.
#
- # When working with an `InboundEmail`, you'll usually interact with the parsed version of the source,
- # which is available as a `Mail` object from `#mail`. But you can also access the raw source directly
- # using the `#source` method.
+ # When working with an +InboundEmail+, you'll usually interact with the parsed version of the source,
+ # which is available as a +Mail+ object from +#mail+. But you can also access the raw source directly
+ # using the +#source+ method.
#
# Examples:
#
diff --git a/actionmailbox/app/models/action_mailbox/inbound_email/incineratable.rb b/actionmailbox/app/models/action_mailbox/inbound_email/incineratable.rb
index 825e300648..697331ede4 100644
--- a/actionmailbox/app/models/action_mailbox/inbound_email/incineratable.rb
+++ b/actionmailbox/app/models/action_mailbox/inbound_email/incineratable.rb
@@ -1,8 +1,8 @@
# frozen_string_literal: true
-# Ensure that the `InboundEmail` is automatically scheduled for later incineration if the status has been
-# changed to `processed`. The later incineration will be invoked at the time specified by the
-# `ActionMailbox.incinerate_after` time using the `IncinerationJob`.
+# Ensure that the +InboundEmail+ is automatically scheduled for later incineration if the status has been
+# changed to +processed+. The later incineration will be invoked at the time specified by the
+# +ActionMailbox.incinerate_after+ time using the +IncinerationJob+.
module ActionMailbox::InboundEmail::Incineratable
extend ActiveSupport::Concern
diff --git a/actionmailbox/app/models/action_mailbox/inbound_email/incineratable/incineration.rb b/actionmailbox/app/models/action_mailbox/inbound_email/incineratable/incineration.rb
index 685f7fceb6..dabc83fae6 100644
--- a/actionmailbox/app/models/action_mailbox/inbound_email/incineratable/incineration.rb
+++ b/actionmailbox/app/models/action_mailbox/inbound_email/incineratable/incineration.rb
@@ -1,10 +1,10 @@
# frozen_string_literal: true
module ActionMailbox
- # Command class for carrying out the actual incineration of the `InboundMail` that's been scheduled
- # for removal. Before the incineration – which really is just a call to `#destroy!` – is run, we verify
+ # Command class for carrying out the actual incineration of the +InboundMail+ that's been scheduled
+ # for removal. Before the incineration – which really is just a call to +#destroy!+ – is run, we verify
# that it's both eligible (by virtue of having already been processed) and time to do so (that is,
- # the `InboundEmail` was processed after the `incinerate_after` time).
+ # the +InboundEmail+ was processed after the +incinerate_after+ time).
class InboundEmail::Incineratable::Incineration
def initialize(inbound_email)
@inbound_email = inbound_email
diff --git a/actionmailbox/app/models/action_mailbox/inbound_email/message_id.rb b/actionmailbox/app/models/action_mailbox/inbound_email/message_id.rb
index 2ad4525929..57b4a2445d 100644
--- a/actionmailbox/app/models/action_mailbox/inbound_email/message_id.rb
+++ b/actionmailbox/app/models/action_mailbox/inbound_email/message_id.rb
@@ -1,11 +1,11 @@
# frozen_string_literal: true
-# The `Message-ID` as specified by rfc822 is supposed to be a unique identifier for that individual email.
-# That makes it an ideal tracking token for debugging and forensics, just like `X-Request-Id` does for
+# The +Message-ID+ as specified by rfc822 is supposed to be a unique identifier for that individual email.
+# That makes it an ideal tracking token for debugging and forensics, just like +X-Request-Id+ does for
# web request.
#
# If an inbound email does not, against the rfc822 mandate, specify a Message-ID, one will be generated
-# using the approach from `Mail::MessageIdField`.
+# using the approach from <tt>Mail::MessageIdField</tt>.
module ActionMailbox::InboundEmail::MessageId
extend ActiveSupport::Concern
@@ -14,9 +14,9 @@ module ActionMailbox::InboundEmail::MessageId
end
class_methods do
- # Create a new `InboundEmail` from the raw `source` of the email, which be uploaded as a Active Storage
- # attachment called `raw_email`. Before the upload, extract the Message-ID from the `source` and set
- # it as an attribute on the new `InboundEmail`.
+ # Create a new +InboundEmail+ from the raw +source+ of the email, which be uploaded as a Active Storage
+ # attachment called +raw_email+. Before the upload, extract the Message-ID from the +source+ and set
+ # it as an attribute on the new +InboundEmail+.
def create_and_extract_message_id!(source, **options)
create! options.merge(message_id: extract_message_id(source)) do |inbound_email|
inbound_email.raw_email.attach io: StringIO.new(source), filename: "message.eml", content_type: "message/rfc822"
diff --git a/actionmailbox/app/models/action_mailbox/inbound_email/routable.rb b/actionmailbox/app/models/action_mailbox/inbound_email/routable.rb
index 58d67eb20c..39565df166 100644
--- a/actionmailbox/app/models/action_mailbox/inbound_email/routable.rb
+++ b/actionmailbox/app/models/action_mailbox/inbound_email/routable.rb
@@ -1,9 +1,9 @@
# frozen_string_literal: true
-# A newly received `InboundEmail` will not be routed synchronously as part of ingress controller's receival.
-# Instead, the routing will be done asynchronously, using a `RoutingJob`, to ensure maximum parallel capacity.
+# A newly received +InboundEmail+ will not be routed synchronously as part of ingress controller's receival.
+# Instead, the routing will be done asynchronously, using a +RoutingJob+, to ensure maximum parallel capacity.
#
-# By default, all newly created `InboundEmail` records that have the status of `pending`, which is the default,
+# By default, all newly created +InboundEmail+ records that have the status of +pending+, which is the default,
# will be scheduled for automatic, deferred routing.
module ActionMailbox::InboundEmail::Routable
extend ActiveSupport::Concern
@@ -12,12 +12,12 @@ module ActionMailbox::InboundEmail::Routable
after_create_commit :route_later, if: :pending?
end
- # Enqueue a `RoutingJob` for this `InboundEmail`.
+ # Enqueue a +RoutingJob+ for this +InboundEmail+.
def route_later
ActionMailbox::RoutingJob.perform_later self
end
- # Route this `InboundEmail` using the routing rules declared on the `ApplicationMailbox`.
+ # Route this +InboundEmail+ using the routing rules declared on the +ApplicationMailbox+.
def route
ApplicationMailbox.route self
end
diff --git a/actionmailbox/config/routes.rb b/actionmailbox/config/routes.rb
index f1bc9847f5..517d2835af 100644
--- a/actionmailbox/config/routes.rb
+++ b/actionmailbox/config/routes.rb
@@ -4,7 +4,8 @@ Rails.application.routes.draw do
scope "/rails/action_mailbox", module: "action_mailbox/ingresses" do
post "/amazon/inbound_emails" => "amazon/inbound_emails#create", as: :rails_amazon_inbound_emails
post "/mandrill/inbound_emails" => "mandrill/inbound_emails#create", as: :rails_mandrill_inbound_emails
- post "/postfix/inbound_emails" => "postfix/inbound_emails#create", as: :rails_postfix_inbound_emails
+ post "/postmark/inbound_emails" => "postmark/inbound_emails#create", as: :rails_postmark_inbound_emails
+ post "/relay/inbound_emails" => "relay/inbound_emails#create", as: :rails_relay_inbound_emails
post "/sendgrid/inbound_emails" => "sendgrid/inbound_emails#create", as: :rails_sendgrid_inbound_emails
# Mailgun requires that a webhook's URL end in 'mime' for it to receive the raw contents of emails.
diff --git a/actionmailbox/db/migrate/20180917164000_create_action_mailbox_tables.rb b/actionmailbox/db/migrate/20180917164000_create_action_mailbox_tables.rb
index 0124b4b98f..8cf621d7e3 100644
--- a/actionmailbox/db/migrate/20180917164000_create_action_mailbox_tables.rb
+++ b/actionmailbox/db/migrate/20180917164000_create_action_mailbox_tables.rb
@@ -4,8 +4,8 @@ class CreateActionMailboxTables < ActiveRecord::Migration[6.0]
t.integer :status, default: 0, null: false
t.string :message_id
- t.datetime :created_at, precision: 6
- t.datetime :updated_at, precision: 6
+ t.datetime :created_at, precision: 6, null: false
+ t.datetime :updated_at, precision: 6, null: false
end
end
end
diff --git a/actionmailbox/lib/action_mailbox/base.rb b/actionmailbox/lib/action_mailbox/base.rb
index 4ac594b9f8..ff8587acd1 100644
--- a/actionmailbox/lib/action_mailbox/base.rb
+++ b/actionmailbox/lib/action_mailbox/base.rb
@@ -7,7 +7,7 @@ require "action_mailbox/routing"
module ActionMailbox
# The base class for all application mailboxes. Not intended to be inherited from directly. Inherit from
- # `ApplicationMailbox` instead, as that's where the app-specific routing is configured. This routing
+ # +ApplicationMailbox+ instead, as that's where the app-specific routing is configured. This routing
# is specified in the following ways:
#
# class ApplicationMailbox < ActionMailbox::Base
@@ -27,15 +27,15 @@ module ActionMailbox
# routing :all => :backstop
# end
#
- # Application mailboxes need to overwrite the `#process` method, which is invoked by the framework after
- # callbacks have been run. The callbacks available are: `before_processing`, `after_processing`, and
- # `around_processing`. The primary use case is ensure certain preconditions to processing are fulfilled
- # using `before_processing` callbacks.
+ # Application mailboxes need to overwrite the +#process+ method, which is invoked by the framework after
+ # callbacks have been run. The callbacks available are: +before_processing+, +after_processing+, and
+ # +around_processing+. The primary use case is ensure certain preconditions to processing are fulfilled
+ # using +before_processing+ callbacks.
#
- # If a precondition fails to be met, you can halt the processing using the `#bounced!` method,
+ # If a precondition fails to be met, you can halt the processing using the +#bounced!+ method,
# which will silently prevent any further processing, but not actually send out any bounce notice. You
# can also pair this behavior with the invocation of an Action Mailer class responsible for sending out
- # an actual bounce email. This is done using the `#bounce_with` method, which takes the mail object returned
+ # an actual bounce email. This is done using the +#bounce_with+ method, which takes the mail object returned
# by an Action Mailer method, like so:
#
# class ForwardsMailbox < ApplicationMailbox
@@ -50,12 +50,12 @@ module ActionMailbox
# end
#
# During the processing of the inbound email, the status will be tracked. Before processing begins,
- # the email will normally have the `pending` status. Once processing begins, just before callbacks
- # and the `#process` method is called, the status is changed to `processing`. If processing is allowed to
- # complete, the status is changed to `delivered`. If a bounce is triggered, then `bounced`. If an unhandled
- # exception is bubbled up, then `failed`.
+ # the email will normally have the +pending+ status. Once processing begins, just before callbacks
+ # and the +#process+ method is called, the status is changed to +processing+. If processing is allowed to
+ # complete, the status is changed to +delivered+. If a bounce is triggered, then +bounced+. If an unhandled
+ # exception is bubbled up, then +failed+.
#
- # Exceptions can be handled at the class level using the familiar `Rescuable` approach:
+ # Exceptions can be handled at the class level using the familiar +Rescuable+ approach:
#
# class ForwardsMailbox < ApplicationMailbox
# rescue_from(ApplicationSpecificVerificationError) { bounced! }
diff --git a/actionmailbox/lib/action_mailbox/postfix_relayer.rb b/actionmailbox/lib/action_mailbox/relayer.rb
index d43c56ed2b..e2890acb60 100644
--- a/actionmailbox/lib/action_mailbox/postfix_relayer.rb
+++ b/actionmailbox/lib/action_mailbox/relayer.rb
@@ -5,19 +5,27 @@ require "net/http"
require "uri"
module ActionMailbox
- class PostfixRelayer
- class Result < Struct.new(:output)
+ class Relayer
+ class Result < Struct.new(:status_code, :message)
def success?
!failure?
end
def failure?
- output.match?(/\A[45]\.\d{1,3}\.\d{1,3}(\s|\z)/)
+ transient_failure? || permanent_failure?
+ end
+
+ def transient_failure?
+ status_code.start_with?("4.")
+ end
+
+ def permanent_failure?
+ status_code.start_with?("5.")
end
end
CONTENT_TYPE = "message/rfc822"
- USER_AGENT = "Action Mailbox Postfix relayer v#{ActionMailbox.version}"
+ USER_AGENT = "Action Mailbox relayer v#{ActionMailbox.version}"
attr_reader :uri, :username, :password
@@ -28,18 +36,18 @@ module ActionMailbox
def relay(source)
case response = post(source)
when Net::HTTPSuccess
- Result.new "2.0.0 Successfully relayed message to Postfix ingress"
+ Result.new "2.0.0", "Successfully relayed message to ingress"
when Net::HTTPUnauthorized
- Result.new "4.7.0 Invalid credentials for Postfix ingress"
+ Result.new "4.7.0", "Invalid credentials for ingress"
else
- Result.new "4.0.0 HTTP #{response.code}"
+ Result.new "4.0.0", "HTTP #{response.code}"
end
rescue IOError, SocketError, SystemCallError => error
- Result.new "4.4.2 Network error relaying to Postfix ingress: #{error.message}"
+ Result.new "4.4.2", "Network error relaying to ingress: #{error.message}"
rescue Timeout::Error
- Result.new "4.4.2 Timed out relaying to Postfix ingress"
+ Result.new "4.4.2", "Timed out relaying to ingress"
rescue => error
- Result.new "4.0.0 Error relaying to Postfix ingress: #{error.message}"
+ Result.new "4.0.0", "Error relaying to ingress: #{error.message}"
end
private
diff --git a/actionmailbox/lib/action_mailbox/router/route.rb b/actionmailbox/lib/action_mailbox/router/route.rb
index b681eb7ea8..7e98e83382 100644
--- a/actionmailbox/lib/action_mailbox/router/route.rb
+++ b/actionmailbox/lib/action_mailbox/router/route.rb
@@ -2,7 +2,7 @@
module ActionMailbox
# Encapsulates a route, which can then be matched against an inbound_email and provide a lookup of the matching
- # mailbox class. See examples for the different route addresses and how to use them in the `ActionMailbox::Base`
+ # mailbox class. See examples for the different route addresses and how to use them in the +ActionMailbox::Base+
# documentation.
class Router::Route
attr_reader :address, :mailbox_name
diff --git a/actionmailbox/lib/action_mailbox/routing.rb b/actionmailbox/lib/action_mailbox/routing.rb
index 1ea96c8a9d..58462a44c6 100644
--- a/actionmailbox/lib/action_mailbox/routing.rb
+++ b/actionmailbox/lib/action_mailbox/routing.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module ActionMailbox
- # See `ActionMailbox::Base` for how to specify routing.
+ # See +ActionMailbox::Base+ for how to specify routing.
module Routing
extend ActiveSupport::Concern
diff --git a/actionmailbox/lib/action_mailbox/test_helper.rb b/actionmailbox/lib/action_mailbox/test_helper.rb
index 02c52fb779..0ec9152844 100644
--- a/actionmailbox/lib/action_mailbox/test_helper.rb
+++ b/actionmailbox/lib/action_mailbox/test_helper.rb
@@ -4,38 +4,38 @@ require "mail"
module ActionMailbox
module TestHelper
- # Create an `InboundEmail` record using an eml fixture in the format of message/rfc822
+ # Create an +InboundEmail+ record using an eml fixture in the format of message/rfc822
# referenced with +fixture_name+ located in +test/fixtures/files/fixture_name+.
def create_inbound_email_from_fixture(fixture_name, status: :processing)
create_inbound_email_from_source file_fixture(fixture_name).read, status: status
end
- # Create an `InboundEmail` by specifying it using `Mail.new` options. Example:
+ # Create an +InboundEmail+ by specifying it using +Mail.new+ options. Example:
#
# create_inbound_email_from_mail(from: "david@loudthinking.com", subject: "Hello!")
def create_inbound_email_from_mail(status: :processing, **mail_options)
create_inbound_email_from_source Mail.new(mail_options).to_s, status: status
end
- # Create an `InboundEmail` using the raw rfc822 `source` as text.
+ # Create an +InboundEmail+ using the raw rfc822 +source+ as text.
def create_inbound_email_from_source(source, status: :processing)
ActionMailbox::InboundEmail.create_and_extract_message_id! source, status: status
end
- # Create an `InboundEmail` from fixture using the same arguments as `create_inbound_email_from_fixture`
+ # Create an +InboundEmail+ from fixture using the same arguments as +create_inbound_email_from_fixture+
# and immediately route it to processing.
def receive_inbound_email_from_fixture(*args)
create_inbound_email_from_fixture(*args).tap(&:route)
end
- # Create an `InboundEmail` from fixture using the same arguments as `create_inbound_email_from_mail`
+ # Create an +InboundEmail+ from fixture using the same arguments as +create_inbound_email_from_mail+
# and immediately route it to processing.
def receive_inbound_email_from_mail(**kwargs)
create_inbound_email_from_mail(**kwargs).tap(&:route)
end
- # Create an `InboundEmail` from fixture using the same arguments as `create_inbound_email_from_source`
+ # Create an +InboundEmail+ from fixture using the same arguments as +create_inbound_email_from_source+
# and immediately route it to processing.
def receive_inbound_email_from_source(**kwargs)
create_inbound_email_from_source(**kwargs).tap(&:route)
diff --git a/actionmailbox/lib/tasks/ingress.rake b/actionmailbox/lib/tasks/ingress.rake
index f775bbdfd7..43b613ea12 100644
--- a/actionmailbox/lib/tasks/ingress.rake
+++ b/actionmailbox/lib/tasks/ingress.rake
@@ -2,12 +2,37 @@
namespace :action_mailbox do
namespace :ingress do
- desc "Pipe an inbound email from STDIN to the Postfix ingress (URL and INGRESS_PASSWORD required)"
- task :postfix do
+ task :environment do
require "active_support"
require "active_support/core_ext/object/blank"
- require "action_mailbox/postfix_relayer"
+ require "action_mailbox/relayer"
+ end
+
+ desc "Relay an inbound email from Exim to Action Mailbox (URL and INGRESS_PASSWORD required)"
+ task exim: "action_mailbox:ingress:environment" do
+ url, password = ENV.values_at("URL", "INGRESS_PASSWORD")
+
+ if url.blank? || password.blank?
+ print "URL and INGRESS_PASSWORD are required"
+ exit 64 # EX_USAGE
+ end
+
+ ActionMailbox::Relayer.new(url: url, password: password).relay(STDIN.read).tap do |result|
+ print result.message
+
+ case
+ when result.success?
+ exit 0
+ when result.transient_failure?
+ exit 75 # EX_TEMPFAIL
+ else
+ exit 69 # EX_UNAVAILABLE
+ end
+ end
+ end
+ desc "Relay an inbound email from Postfix to Action Mailbox (URL and INGRESS_PASSWORD required)"
+ task postfix: "action_mailbox:ingress:environment" do
url, password = ENV.values_at("URL", "INGRESS_PASSWORD")
if url.blank? || password.blank?
@@ -15,10 +40,33 @@ namespace :action_mailbox do
exit 1
end
- ActionMailbox::PostfixRelayer.new(url: url, password: password).relay(STDIN.read).tap do |result|
- print result.output
+ ActionMailbox::Relayer.new(url: url, password: password).relay(STDIN.read).tap do |result|
+ print "#{result.status_code} #{result.message}"
exit result.success?
end
end
+
+ desc "Relay an inbound email from Qmail to Action Mailbox (URL and INGRESS_PASSWORD required)"
+ task qmail: "action_mailbox:ingress:environment" do
+ url, password = ENV.values_at("URL", "INGRESS_PASSWORD")
+
+ if url.blank? || password.blank?
+ print "URL and INGRESS_PASSWORD are required"
+ exit 111
+ end
+
+ ActionMailbox::Relayer.new(url: url, password: password).relay(STDIN.read).tap do |result|
+ print result.message
+
+ case
+ when result.success?
+ exit 0
+ when result.transient_failure?
+ exit 111
+ else
+ exit 100
+ end
+ end
+ end
end
end
diff --git a/actionmailbox/test/controllers/ingresses/postmark/inbound_emails_controller_test.rb b/actionmailbox/test/controllers/ingresses/postmark/inbound_emails_controller_test.rb
new file mode 100644
index 0000000000..11b579b39c
--- /dev/null
+++ b/actionmailbox/test/controllers/ingresses/postmark/inbound_emails_controller_test.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+require "test_helper"
+
+class ActionMailbox::Ingresses::Postmark::InboundEmailsControllerTest < ActionDispatch::IntegrationTest
+ setup { ActionMailbox.ingress = :postmark }
+
+ test "receiving an inbound email from Postmark" do
+ assert_difference -> { ActionMailbox::InboundEmail.count }, +1 do
+ post rails_postmark_inbound_emails_url,
+ headers: { authorization: credentials }, params: { RawEmail: file_fixture("../files/welcome.eml").read }
+ end
+
+ assert_response :no_content
+
+ inbound_email = ActionMailbox::InboundEmail.last
+ assert_equal file_fixture("../files/welcome.eml").read, inbound_email.raw_email.download
+ assert_equal "0CB459E0-0336-41DA-BC88-E6E28C697DDB@37signals.com", inbound_email.message_id
+ end
+
+ test "rejecting when RawEmail param is missing" do
+ assert_no_difference -> { ActionMailbox::InboundEmail.count } do
+ post rails_postmark_inbound_emails_url,
+ headers: { authorization: credentials }, params: { From: "someone@example.com" }
+ end
+
+ assert_response :unprocessable_entity
+ end
+
+ test "rejecting an unauthorized inbound email from Postmark" do
+ assert_no_difference -> { ActionMailbox::InboundEmail.count } do
+ post rails_postmark_inbound_emails_url, params: { RawEmail: file_fixture("../files/welcome.eml").read }
+ end
+
+ assert_response :unauthorized
+ end
+
+ test "raising when the configured password is nil" do
+ switch_password_to nil do
+ assert_raises ArgumentError do
+ post rails_postmark_inbound_emails_url,
+ headers: { authorization: credentials }, params: { RawEmail: file_fixture("../files/welcome.eml").read }
+ end
+ end
+ end
+
+ test "raising when the configured password is blank" do
+ switch_password_to "" do
+ assert_raises ArgumentError do
+ post rails_postmark_inbound_emails_url,
+ headers: { authorization: credentials }, params: { RawEmail: file_fixture("../files/welcome.eml").read }
+ end
+ end
+ end
+end
diff --git a/actionmailbox/test/controllers/ingresses/postfix/inbound_emails_controller_test.rb b/actionmailbox/test/controllers/ingresses/relay/inbound_emails_controller_test.rb
index d646f5e859..67c5993f7f 100644
--- a/actionmailbox/test/controllers/ingresses/postfix/inbound_emails_controller_test.rb
+++ b/actionmailbox/test/controllers/ingresses/relay/inbound_emails_controller_test.rb
@@ -2,12 +2,12 @@
require "test_helper"
-class ActionMailbox::Ingresses::Postfix::InboundEmailsControllerTest < ActionDispatch::IntegrationTest
- setup { ActionMailbox.ingress = :postfix }
+class ActionMailbox::Ingresses::Relay::InboundEmailsControllerTest < ActionDispatch::IntegrationTest
+ setup { ActionMailbox.ingress = :relay }
- test "receiving an inbound email from Postfix" do
+ test "receiving an inbound email relayed from an SMTP server" do
assert_difference -> { ActionMailbox::InboundEmail.count }, +1 do
- post rails_postfix_inbound_emails_url, headers: { "Authorization" => credentials, "Content-Type" => "message/rfc822" },
+ post rails_relay_inbound_emails_url, headers: { "Authorization" => credentials, "Content-Type" => "message/rfc822" },
params: file_fixture("../files/welcome.eml").read
end
@@ -18,18 +18,18 @@ class ActionMailbox::Ingresses::Postfix::InboundEmailsControllerTest < ActionDis
assert_equal "0CB459E0-0336-41DA-BC88-E6E28C697DDB@37signals.com", inbound_email.message_id
end
- test "rejecting an unauthorized inbound email from Postfix" do
+ test "rejecting an unauthorized inbound email" do
assert_no_difference -> { ActionMailbox::InboundEmail.count } do
- post rails_postfix_inbound_emails_url, headers: { "Content-Type" => "message/rfc822" },
+ post rails_relay_inbound_emails_url, headers: { "Content-Type" => "message/rfc822" },
params: file_fixture("../files/welcome.eml").read
end
assert_response :unauthorized
end
- test "rejecting an inbound email of an unsupported media type from Postfix" do
+ test "rejecting an inbound email of an unsupported media type" do
assert_no_difference -> { ActionMailbox::InboundEmail.count } do
- post rails_postfix_inbound_emails_url, headers: { "Authorization" => credentials, "Content-Type" => "text/plain" },
+ post rails_relay_inbound_emails_url, headers: { "Authorization" => credentials, "Content-Type" => "text/plain" },
params: file_fixture("../files/welcome.eml").read
end
@@ -39,7 +39,7 @@ class ActionMailbox::Ingresses::Postfix::InboundEmailsControllerTest < ActionDis
test "raising when the configured password is nil" do
switch_password_to nil do
assert_raises ArgumentError do
- post rails_postfix_inbound_emails_url, headers: { "Authorization" => credentials, "Content-Type" => "message/rfc822" },
+ post rails_relay_inbound_emails_url, headers: { "Authorization" => credentials, "Content-Type" => "message/rfc822" },
params: file_fixture("../files/welcome.eml").read
end
end
@@ -48,7 +48,7 @@ class ActionMailbox::Ingresses::Postfix::InboundEmailsControllerTest < ActionDis
test "raising when the configured password is blank" do
switch_password_to "" do
assert_raises ArgumentError do
- post rails_postfix_inbound_emails_url, headers: { "Authorization" => credentials, "Content-Type" => "message/rfc822" },
+ post rails_relay_inbound_emails_url, headers: { "Authorization" => credentials, "Content-Type" => "message/rfc822" },
params: file_fixture("../files/welcome.eml").read
end
end
diff --git a/actionmailbox/test/unit/postfix_relayer_test.rb b/actionmailbox/test/unit/relayer_test.rb
index 5f7496ec3f..fb2b48ea16 100644
--- a/actionmailbox/test/unit/postfix_relayer_test.rb
+++ b/actionmailbox/test/unit/relayer_test.rb
@@ -2,35 +2,37 @@
require_relative "../test_helper"
-require "action_mailbox/postfix_relayer"
+require "action_mailbox/relayer"
module ActionMailbox
- class PostfixRelayerTest < ActiveSupport::TestCase
- URL = "https://example.com/rails/action_mailbox/postfix/inbound_emails"
+ class RelayerTest < ActiveSupport::TestCase
+ URL = "https://example.com/rails/action_mailbox/relay/inbound_emails"
INGRESS_PASSWORD = "secret"
setup do
- @relayer = ActionMailbox::PostfixRelayer.new(url: URL, password: INGRESS_PASSWORD)
+ @relayer = ActionMailbox::Relayer.new(url: URL, password: INGRESS_PASSWORD)
end
test "successfully relaying an email" do
stub_request(:post, URL).to_return status: 204
result = @relayer.relay(file_fixture("welcome.eml").read)
- assert_equal "2.0.0 Successfully relayed message to Postfix ingress", result.output
+ assert_equal "2.0.0", result.status_code
+ assert_equal "Successfully relayed message to ingress", result.message
assert result.success?
assert_not result.failure?
assert_requested :post, URL, body: file_fixture("welcome.eml").read,
basic_auth: [ "actionmailbox", INGRESS_PASSWORD ],
- headers: { "Content-Type" => "message/rfc822", "User-Agent" => /\AAction Mailbox Postfix relayer v\d+\./ }
+ headers: { "Content-Type" => "message/rfc822", "User-Agent" => /\AAction Mailbox relayer v\d+\./ }
end
test "unsuccessfully relaying with invalid credentials" do
stub_request(:post, URL).to_return status: 401
result = @relayer.relay(file_fixture("welcome.eml").read)
- assert_equal "4.7.0 Invalid credentials for Postfix ingress", result.output
+ assert_equal "4.7.0", result.status_code
+ assert_equal "Invalid credentials for ingress", result.message
assert_not result.success?
assert result.failure?
end
@@ -39,7 +41,8 @@ module ActionMailbox
stub_request(:post, URL).to_return status: 500
result = @relayer.relay(file_fixture("welcome.eml").read)
- assert_equal "4.0.0 HTTP 500", result.output
+ assert_equal "4.0.0", result.status_code
+ assert_equal "HTTP 500", result.message
assert_not result.success?
assert result.failure?
end
@@ -48,7 +51,8 @@ module ActionMailbox
stub_request(:post, URL).to_return status: 504
result = @relayer.relay(file_fixture("welcome.eml").read)
- assert_equal "4.0.0 HTTP 504", result.output
+ assert_equal "4.0.0", result.status_code
+ assert_equal "HTTP 504", result.message
assert_not result.success?
assert result.failure?
end
@@ -57,7 +61,8 @@ module ActionMailbox
stub_request(:post, URL).to_raise Errno::ECONNRESET.new
result = @relayer.relay(file_fixture("welcome.eml").read)
- assert_equal "4.4.2 Network error relaying to Postfix ingress: Connection reset by peer", result.output
+ assert_equal "4.4.2", result.status_code
+ assert_equal "Network error relaying to ingress: Connection reset by peer", result.message
assert_not result.success?
assert result.failure?
end
@@ -66,7 +71,8 @@ module ActionMailbox
stub_request(:post, URL).to_raise SocketError.new("Failed to open TCP connection to example.com:443")
result = @relayer.relay(file_fixture("welcome.eml").read)
- assert_equal "4.4.2 Network error relaying to Postfix ingress: Failed to open TCP connection to example.com:443", result.output
+ assert_equal "4.4.2", result.status_code
+ assert_equal "Network error relaying to ingress: Failed to open TCP connection to example.com:443", result.message
assert_not result.success?
assert result.failure?
end
@@ -75,7 +81,8 @@ module ActionMailbox
stub_request(:post, URL).to_timeout
result = @relayer.relay(file_fixture("welcome.eml").read)
- assert_equal "4.4.2 Timed out relaying to Postfix ingress", result.output
+ assert_equal "4.4.2", result.status_code
+ assert_equal "Timed out relaying to ingress", result.message
assert_not result.success?
assert result.failure?
end
@@ -84,7 +91,8 @@ module ActionMailbox
stub_request(:post, URL).to_raise StandardError.new("Something went wrong")
result = @relayer.relay(file_fixture("welcome.eml").read)
- assert_equal "4.0.0 Error relaying to Postfix ingress: Something went wrong", result.output
+ assert_equal "4.0.0", result.status_code
+ assert_equal "Error relaying to ingress: Something went wrong", result.message
assert_not result.success?
assert result.failure?
end
diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md
index 95da4265a4..1457794354 100644
--- a/actionpack/CHANGELOG.md
+++ b/actionpack/CHANGELOG.md
@@ -85,10 +85,6 @@
*Yoshiyuki Kinjo*
-* Remove undocumented `params` option from `url_for` helper.
-
- *Ilkka Oksanen*
-
* Encode Content-Disposition filenames on `send_data` and `send_file`.
Previously, `send_data 'data', filename: "\u{3042}.txt"` sends
`"filename=\"\u{3042}.txt\""` as Content-Disposition and it can be
diff --git a/actionpack/lib/action_controller/metal/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb
index 118da11990..bf5e7a433f 100644
--- a/actionpack/lib/action_controller/metal/mime_responds.rb
+++ b/actionpack/lib/action_controller/metal/mime_responds.rb
@@ -124,6 +124,14 @@ module ActionController #:nodoc:
#
# render json: @people
#
+ # +any+ can also be used with no arguments, in which case it will be used for any format requested by
+ # the user:
+ #
+ # respond_to do |format|
+ # format.html
+ # format.any { redirect_to support_path }
+ # end
+ #
# Formats can have different variants.
#
# The request variant is a specialization of the request format, like <tt>:tablet</tt>,
diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb
index 2966c969f6..972953d4f3 100644
--- a/actionpack/lib/action_dispatch/routing/route_set.rb
+++ b/actionpack/lib/action_dispatch/routing/route_set.rb
@@ -820,6 +820,10 @@ module ActionDispatch
path, params = generate(route_name, path_options, recall)
+ if options.key? :params
+ params.merge! options[:params]
+ end
+
options[:path] = path
options[:script_name] = script_name
options[:params] = params
diff --git a/actionpack/lib/action_dispatch/routing/url_for.rb b/actionpack/lib/action_dispatch/routing/url_for.rb
index 1a31c7dbb8..fcb8ae296b 100644
--- a/actionpack/lib/action_dispatch/routing/url_for.rb
+++ b/actionpack/lib/action_dispatch/routing/url_for.rb
@@ -133,6 +133,7 @@ module ActionDispatch
# <tt>ActionDispatch::Http::URL.tld_length</tt>, which in turn defaults to 1.
# * <tt>:port</tt> - Optionally specify the port to connect to.
# * <tt>:anchor</tt> - An anchor name to be appended to the path.
+ # * <tt>:params</tt> - The query parameters to be appended to the path.
# * <tt>:trailing_slash</tt> - If true, adds a trailing slash, as in "/archive/2009/"
# * <tt>:script_name</tt> - Specifies application path relative to domain root. If provided, prepends application path.
#
diff --git a/actionpack/test/controller/base_test.rb b/actionpack/test/controller/base_test.rb
index 558e710df9..d8cea10153 100644
--- a/actionpack/test/controller/base_test.rb
+++ b/actionpack/test/controller/base_test.rb
@@ -193,7 +193,7 @@ class UrlOptionsTest < ActionController::TestCase
action: "home",
controller: "pages",
only_path: true,
- token: "secret"
+ params: { "token" => "secret" }
}
assert_equal "/home?token=secret", rs.url_for(options)
diff --git a/actionpack/test/controller/url_for_test.rb b/actionpack/test/controller/url_for_test.rb
index e381abee36..9222250b9c 100644
--- a/actionpack/test/controller/url_for_test.rb
+++ b/actionpack/test/controller/url_for_test.rb
@@ -354,6 +354,14 @@ module AbstractController
assert_equal({ p2: "Y2" }.to_query, params[1])
end
+ def test_params_option
+ url = W.new.url_for(only_path: true, controller: "c", action: "a", params: { domain: "foo", id: "1" })
+ params = extract_params(url)
+ assert_equal("/c/a?domain=foo&id=1", url)
+ assert_equal({ domain: "foo" }.to_query, params[0])
+ assert_equal({ id: "1" }.to_query, params[1])
+ end
+
def test_hash_parameter
url = W.new.url_for(only_path: true, controller: "c", action: "a", query: { name: "Bob", category: "prof" })
params = extract_params(url)
diff --git a/actiontext/app/javascript/actiontext/attachment_upload.js b/actiontext/app/javascript/actiontext/attachment_upload.js
index a716f1f589..77fbc97df6 100644
--- a/actiontext/app/javascript/actiontext/attachment_upload.js
+++ b/actiontext/app/javascript/actiontext/attachment_upload.js
@@ -1,4 +1,4 @@
-import { DirectUpload } from "activestorage"
+import { DirectUpload } from "@rails/activestorage"
export class AttachmentUpload {
constructor(attachment, element) {
diff --git a/actiontext/lib/templates/installer.rb b/actiontext/lib/templates/installer.rb
index dc549a8af3..e7c6c2623e 100644
--- a/actiontext/lib/templates/installer.rb
+++ b/actiontext/lib/templates/installer.rb
@@ -8,15 +8,14 @@ say "Copying blob rendering partial to app/views/active_storage/blobs/_blob.html
copy_file "#{__dir__}/../../app/views/active_storage/blobs/_blob.html.erb",
"app/views/active_storage/blobs/_blob.html.erb"
-# FIXME: Replace with release version on release
say "Installing JavaScript dependency"
-run "yarn add https://github.com/rails/actiontext"
+run "yarn add @rails/actiontext"
APPLICATION_PACK_PATH = "app/javascript/packs/application.js"
-if File.exist?(APPLICATION_PACK_PATH) && File.read(APPLICATION_PACK_PATH) !~ /import "actiontext"/
+if File.exist?(APPLICATION_PACK_PATH) && File.read(APPLICATION_PACK_PATH) !~ /import "@rails\/actiontext"/
say "Adding import to default JavaScript pack"
append_to_file APPLICATION_PACK_PATH, <<-EOS
-import "actiontext"
+import "@rails/actiontext"
EOS
end
diff --git a/actiontext/package.json b/actiontext/package.json
index b42838b1df..ec8f35fd3c 100644
--- a/actiontext/package.json
+++ b/actiontext/package.json
@@ -1,5 +1,5 @@
{
- "name": "actiontext",
+ "name": "@rails/actiontext",
"version": "6.0.0-alpha",
"description": "Edit and display rich text in Rails applications",
"main": "app/javascript/actiontext/index.js",
@@ -21,7 +21,7 @@
],
"license": "MIT",
"dependencies": {
- "trix": ">=1.0.0",
- "activestorage": "6.0.0-alpha"
+ "trix": "^1.0.0",
+ "@rails/activestorage": "^6.0.0-alpha"
}
}
diff --git a/actiontext/test/dummy/app/javascript/packs/application.js b/actiontext/test/dummy/app/javascript/packs/application.js
index 90eb1a1841..13ac17ed58 100644
--- a/actiontext/test/dummy/app/javascript/packs/application.js
+++ b/actiontext/test/dummy/app/javascript/packs/application.js
@@ -1 +1 @@
-import "actiontext"
+import "@rails/actiontext"
diff --git a/actiontext/yarn.lock b/actiontext/yarn.lock
deleted file mode 100644
index f98622eb64..0000000000
--- a/actiontext/yarn.lock
+++ /dev/null
@@ -1,11 +0,0 @@
-# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
-# yarn lockfile v1
-
-
-"activestorage@>= 5.2.0-rc1":
- version "5.2.0-rc1"
- resolved "https://registry.yarnpkg.com/activestorage/-/activestorage-5.2.0-rc1.tgz#79898996eceb0f13575eff41fb109051fbfa49b0"
-
-trix@^0.11.1:
- version "0.11.1"
- resolved "https://registry.yarnpkg.com/trix/-/trix-0.11.1.tgz#ffe54f2757c2c2385b8424fd5c5d2ab712a09acc"
diff --git a/actionview/app/assets/javascripts/README.md b/actionview/app/assets/javascripts/README.md
index b74fa1afad..2b110e604f 100644
--- a/actionview/app/assets/javascripts/README.md
+++ b/actionview/app/assets/javascripts/README.md
@@ -17,11 +17,11 @@ Note that the `data` attributes this library adds are a feature of HTML5. If you
### NPM
- npm install rails-ujs --save
-
+ npm install @rails/ujs --save
+
### Yarn
-
- yarn add rails-ujs
+
+ yarn add @rails/ujs
Ensure that `.yarnclean` does not include `assets` if you use [yarn autoclean](https://yarnpkg.com/lang/en/docs/cli/autoclean/).
@@ -40,7 +40,7 @@ In a conventional Rails application that uses the asset pipeline, require `rails
If you're using the Webpacker gem or some other JavaScript bundler, add the following to your main JS file:
```javascript
-import Rails from 'rails-ujs';
+import Rails from "@rails/ujs"
Rails.start()
```
diff --git a/actionview/lib/action_view/helpers/form_options_helper.rb b/actionview/lib/action_view/helpers/form_options_helper.rb
index ebdd96f570..a7747456a4 100644
--- a/actionview/lib/action_view/helpers/form_options_helper.rb
+++ b/actionview/lib/action_view/helpers/form_options_helper.rb
@@ -654,7 +654,7 @@ module ActionView
#
# ==== Gotcha
#
- # The HTML specification says when nothing is select on a collection of radio buttons
+ # The HTML specification says when nothing is selected on a collection of radio buttons
# web browsers do not send any value to server.
# Unfortunately this introduces a gotcha:
# if a +User+ model has a +category_id+ field and in the form no category is selected, no +category_id+ parameter is sent. So,
diff --git a/actionview/package.json b/actionview/package.json
index 1f74df79d3..d6cf412c6d 100644
--- a/actionview/package.json
+++ b/actionview/package.json
@@ -1,5 +1,5 @@
{
- "name": "rails-ujs",
+ "name": "@rails/ujs",
"version": "6.0.0-alpha",
"description": "Ruby on Rails unobtrusive scripting adapter",
"main": "lib/assets/compiled/rails-ujs.js",
diff --git a/activemodel/lib/active_model/callbacks.rb b/activemodel/lib/active_model/callbacks.rb
index fde3381df2..ea2ed7dff7 100644
--- a/activemodel/lib/active_model/callbacks.rb
+++ b/activemodel/lib/active_model/callbacks.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
require "active_support/core_ext/array/extract_options"
+require "active_support/core_ext/hash/keys"
module ActiveModel
# == Active \Model \Callbacks
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md
index ca072be5e1..e987a0e279 100644
--- a/activerecord/CHANGELOG.md
+++ b/activerecord/CHANGELOG.md
@@ -1,3 +1,17 @@
+* Set polymorphic type column to NULL on `dependent: :nullify` strategy.
+
+ On polymorphic associations both the foreign key and the foreign type columns will be set to NULL.
+
+ *Laerti Papa*
+
+* Allow `ActionController::Params` as argument of `ActiveRecord::Base#exists?`.
+
+ *Gannon McGibbon*
+
+* Add support for endless ranges introduces in Ruby 2.6.
+
+ *Greg Navis*
+
* Deprecate passing `migrations_paths` to `connection.assume_migrated_upto_version`.
*Ryuta Kamizono*
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb
index fb1df00dc8..7bdbd8ce69 100644
--- a/activerecord/lib/active_record/associations.rb
+++ b/activerecord/lib/active_record/associations.rb
@@ -1293,7 +1293,8 @@ module ActiveRecord
#
# * <tt>:destroy</tt> causes all the associated objects to also be destroyed.
# * <tt>:delete_all</tt> causes all the associated objects to be deleted directly from the database (so callbacks will not be executed).
- # * <tt>:nullify</tt> causes the foreign keys to be set to +NULL+. Callbacks are not executed.
+ # * <tt>:nullify</tt> causes the foreign keys to be set to +NULL+. Polymorphic type will also be nullified
+ # on polymorphic associations. Callbacks are not executed.
# * <tt>:restrict_with_exception</tt> causes an <tt>ActiveRecord::DeleteRestrictionError</tt> exception to be raised if there are any associated records.
# * <tt>:restrict_with_error</tt> causes an error to be added to the owner if there are any associated objects.
#
@@ -1436,7 +1437,8 @@ module ActiveRecord
#
# * <tt>:destroy</tt> causes the associated object to also be destroyed
# * <tt>:delete</tt> causes the associated object to be deleted directly from the database (so callbacks will not execute)
- # * <tt>:nullify</tt> causes the foreign key to be set to +NULL+. Callbacks are not executed.
+ # * <tt>:nullify</tt> causes the foreign key to be set to +NULL+. Polymorphic type column is also nullified
+ # on polymorphic associations. Callbacks are not executed.
# * <tt>:restrict_with_exception</tt> causes an <tt>ActiveRecord::DeleteRestrictionError</tt> exception to be raised if there is an associated record
# * <tt>:restrict_with_error</tt> causes an error to be added to the owner if there is an associated object
#
diff --git a/activerecord/lib/active_record/associations/foreign_association.rb b/activerecord/lib/active_record/associations/foreign_association.rb
index 40010cde03..59af6f54c3 100644
--- a/activerecord/lib/active_record/associations/foreign_association.rb
+++ b/activerecord/lib/active_record/associations/foreign_association.rb
@@ -9,5 +9,12 @@ module ActiveRecord::Associations
false
end
end
+
+ def nullified_owner_attributes
+ Hash.new.tap do |attrs|
+ attrs[reflection.foreign_key] = nil
+ attrs[reflection.type] = nil if reflection.type.present?
+ end
+ end
end
end
diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb
index f6fdbcde54..eb22db838c 100644
--- a/activerecord/lib/active_record/associations/has_many_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_association.rb
@@ -92,7 +92,7 @@ module ActiveRecord
if method == :delete_all
scope.delete_all
else
- scope.update_all(reflection.foreign_key => nil)
+ scope.update_all(nullified_owner_attributes)
end
end
diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb
index 390bfd8b08..99971286a3 100644
--- a/activerecord/lib/active_record/associations/has_one_association.rb
+++ b/activerecord/lib/active_record/associations/has_one_association.rb
@@ -33,7 +33,7 @@ module ActiveRecord
target.destroy
throw(:abort) unless target.destroyed?
when :nullify
- target.update_columns(reflection.foreign_key => nil) if target.persisted?
+ target.update_columns(nullified_owner_attributes) if target.persisted?
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
index 2299fc0214..79d71bfb5d 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
@@ -123,7 +123,7 @@ module ActiveRecord
# +binds+ as the bind substitutes. +name+ is logged along with
# the executed +sql+ statement.
def exec_insert(sql, name = nil, binds = [], pk = nil, sequence_name = nil)
- sql, binds = sql_for_insert(sql, pk, nil, sequence_name, binds)
+ sql, binds = sql_for_insert(sql, pk, sequence_name, binds)
exec_query(sql, name, binds)
end
@@ -464,7 +464,7 @@ module ActiveRecord
exec_query(sql, name, binds, prepare: true)
end
- def sql_for_insert(sql, pk, id_value, sequence_name, binds)
+ def sql_for_insert(sql, pk, sequence_name, binds)
[sql, binds]
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
index 346d4b067a..d1ff32df3f 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
@@ -6,6 +6,7 @@ require "active_record/connection_adapters/sql_type_metadata"
require "active_record/connection_adapters/abstract/schema_dumper"
require "active_record/connection_adapters/abstract/schema_creation"
require "active_support/concurrency/load_interlock_aware_monitor"
+require "active_support/deprecation"
require "arel/collectors/bind"
require "arel/collectors/composite"
require "arel/collectors/sql_string"
@@ -76,8 +77,11 @@ module ActiveRecord
SIMPLE_INT = /\A\d+\z/
- attr_accessor :visitor, :pool, :prevent_writes
- attr_reader :schema_cache, :owner, :logger, :prepared_statements, :lock
+ attr_writer :visitor
+ deprecate :visitor=
+
+ attr_accessor :pool
+ attr_reader :schema_cache, :visitor, :owner, :logger, :lock, :prepared_statements, :prevent_writes
alias :in_use? :owner
set_callback :checkin, :after, :enable_lazy_transactions!
@@ -117,6 +121,7 @@ module ActiveRecord
@idle_since = Concurrent.monotonic_time
@schema_cache = SchemaCache.new self
@quoted_column_names, @quoted_table_names = {}, {}
+ @prevent_writes = false
@visitor = arel_visitor
@lock = ActiveSupport::Concurrency::LoadInterlockAwareMonitor.new
@@ -152,11 +157,10 @@ module ActiveRecord
# even if you are on a database that can write. `while_preventing_writes`
# will prevent writes to the database for the duration of the block.
def while_preventing_writes
- original = self.prevent_writes
- self.prevent_writes = true
+ original, @prevent_writes = @prevent_writes, true
yield
ensure
- self.prevent_writes = original
+ @prevent_writes = original
end
def migrations_paths # :nodoc:
@@ -504,15 +508,17 @@ module ActiveRecord
@connection
end
- def case_sensitive_comparison(table, attribute, column, value) # :nodoc:
- table[attribute].eq(value)
+ def case_sensitive_comparison(attribute, value) # :nodoc:
+ attribute.eq(value)
end
- def case_insensitive_comparison(table, attribute, column, value) # :nodoc:
+ def case_insensitive_comparison(attribute, value) # :nodoc:
+ column = column_for_attribute(attribute)
+
if can_perform_case_insensitive_comparison_for?(column)
- table[attribute].lower.eq(table.lower(value))
+ attribute.lower.eq(attribute.relation.lower(value))
else
- table[attribute].eq(value)
+ attribute.eq(value)
end
end
@@ -659,6 +665,11 @@ module ActiveRecord
raise(ActiveRecordError, "No such column: #{table_name}.#{column_name}")
end
+ def column_for_attribute(attribute)
+ table_name = attribute.relation.name
+ schema_cache.columns_hash(table_name)[attribute.name.to_s]
+ end
+
def collector
if prepared_statements
Arel::Collectors::Composite.new(
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 cccd6e2210..70d281b62b 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
@@ -476,9 +476,11 @@ module ActiveRecord
SQL
end
- def case_sensitive_comparison(table, attribute, column, value) # :nodoc:
+ def case_sensitive_comparison(attribute, value) # :nodoc:
+ column = column_for_attribute(attribute)
+
if column.collation && !column.case_sensitive?
- table[attribute].eq(Arel::Nodes::Bin.new(value))
+ attribute.eq(Arel::Nodes::Bin.new(value))
else
super
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb
index c70a4fa875..41633872e2 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb
@@ -110,7 +110,7 @@ module ActiveRecord
end
alias :exec_update :exec_delete
- def sql_for_insert(sql, pk, id_value, sequence_name, binds) # :nodoc:
+ def sql_for_insert(sql, pk, sequence_name, binds) # :nodoc:
if pk.nil?
# Extract the table from the insert sql. Yuck.
table_ref = extract_table_ref_from_insert_sql(sql)
diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb
index ba221a333b..a863227276 100644
--- a/activerecord/lib/active_record/relation.rb
+++ b/activerecord/lib/active_record/relation.rb
@@ -44,6 +44,11 @@ module ActiveRecord
end
def bind_attribute(name, value) # :nodoc:
+ if reflection = klass._reflect_on_association(name)
+ name = reflection.foreign_key
+ value = value.read_attribute(reflection.klass.primary_key) unless value.nil?
+ end
+
attr = arel_attribute(name)
bind = predicate_builder.build_bind_attribute(attr.name, value)
yield attr, bind
diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb
index dc03b196f4..fd84f9c46b 100644
--- a/activerecord/lib/active_record/relation/finder_methods.rb
+++ b/activerecord/lib/active_record/relation/finder_methods.rb
@@ -312,6 +312,8 @@ module ActiveRecord
return false if !conditions || limit_value == 0
+ conditions = sanitize_forbidden_attributes(conditions)
+
if eager_loading?
relation = apply_join_dependency(eager_loading: false)
return relation.exists?(conditions)
diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb
index 5a1dbc8e53..111b6c9a64 100644
--- a/activerecord/lib/active_record/validations/uniqueness.rb
+++ b/activerecord/lib/active_record/validations/uniqueness.rb
@@ -56,33 +56,21 @@ module ActiveRecord
end
def build_relation(klass, attribute, value)
- if reflection = klass._reflect_on_association(attribute)
- attribute = reflection.foreign_key
- value = value.attributes[reflection.klass.primary_key] unless value.nil?
- end
-
- if value.nil?
- return klass.unscoped.where!(attribute => value)
- end
-
- # the attribute may be an aliased attribute
- if klass.attribute_alias?(attribute)
- attribute = klass.attribute_alias(attribute)
+ relation = klass.unscoped
+ comparison = relation.bind_attribute(attribute, value) do |attr, bind|
+ return relation.none! unless bind.boundable?
+
+ if bind.nil?
+ attr.eq(bind)
+ elsif options[:case_sensitive]
+ klass.connection.case_sensitive_comparison(attr, bind)
+ else
+ # will use SQL LOWER function before comparison, unless it detects a case insensitive collation
+ klass.connection.case_insensitive_comparison(attr, bind)
+ end
end
- attribute_name = attribute.to_s
- value = klass.predicate_builder.build_bind_attribute(attribute_name, value)
-
- table = klass.arel_table
- column = klass.columns_hash[attribute_name]
-
- comparison = if !options[:case_sensitive]
- # will use SQL LOWER function before comparison, unless it detects a case insensitive collation
- klass.connection.case_insensitive_comparison(table, attribute, column, value)
- else
- klass.connection.case_sensitive_comparison(table, attribute, column, value)
- end
- klass.unscoped.where!(comparison)
+ relation.where!(comparison)
end
def scope_relation(record, relation)
diff --git a/activerecord/lib/arel.rb b/activerecord/lib/arel.rb
index dab785738e..7411b5c41b 100644
--- a/activerecord/lib/arel.rb
+++ b/activerecord/lib/arel.rb
@@ -13,7 +13,6 @@ require "arel/alias_predication"
require "arel/order_predications"
require "arel/table"
require "arel/attributes"
-require "arel/compatibility/wheres"
require "arel/visitors"
require "arel/collectors/sql_string"
diff --git a/activerecord/lib/arel/compatibility/wheres.rb b/activerecord/lib/arel/compatibility/wheres.rb
deleted file mode 100644
index c8a73f0dae..0000000000
--- a/activerecord/lib/arel/compatibility/wheres.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-# frozen_string_literal: true
-
-module Arel # :nodoc: all
- module Compatibility # :nodoc:
- class Wheres # :nodoc:
- include Enumerable
-
- module Value # :nodoc:
- attr_accessor :visitor
- def value
- visitor.accept self
- end
-
- def name
- super.to_sym
- end
- end
-
- def initialize(engine, collection)
- @engine = engine
- @collection = collection
- end
-
- def each
- to_sql = Visitors::ToSql.new @engine
-
- @collection.each { |c|
- c.extend(Value)
- c.visitor = to_sql
- yield c
- }
- end
- end
- end
-end
diff --git a/activerecord/lib/arel/predications.rb b/activerecord/lib/arel/predications.rb
index 28679ae892..2a62c53aa3 100644
--- a/activerecord/lib/arel/predications.rb
+++ b/activerecord/lib/arel/predications.rb
@@ -36,14 +36,14 @@ module Arel # :nodoc: all
def between(other)
if infinity?(other.begin)
- if infinity?(other.end)
+ if other.end.nil? || infinity?(other.end)
not_in([])
elsif other.exclude_end?
lt(other.end)
else
lteq(other.end)
end
- elsif infinity?(other.end)
+ elsif other.end.nil? || infinity?(other.end)
gteq(other.begin)
elsif other.exclude_end?
gteq(other.begin).and(lt(other.end))
@@ -82,14 +82,14 @@ Passing a range to `#in` is deprecated. Call `#between`, instead.
def not_between(other)
if infinity?(other.begin)
- if infinity?(other.end)
+ if other.end.nil? || infinity?(other.end)
self.in([])
elsif other.exclude_end?
gteq(other.end)
else
gt(other.end)
end
- elsif infinity?(other.end)
+ elsif other.end.nil? || infinity?(other.end)
lt(other.begin)
else
left = lt(other.begin)
diff --git a/activerecord/test/cases/adapters/postgresql/case_insensitive_test.rb b/activerecord/test/cases/adapters/postgresql/case_insensitive_test.rb
index 305e033642..79e9efcf06 100644
--- a/activerecord/test/cases/adapters/postgresql/case_insensitive_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/case_insensitive_test.rb
@@ -7,22 +7,21 @@ class PostgresqlCaseInsensitiveTest < ActiveRecord::PostgreSQLTestCase
def test_case_insensitiveness
connection = ActiveRecord::Base.connection
- table = Default.arel_table
- column = Default.columns_hash["char1"]
- comparison = connection.case_insensitive_comparison table, :char1, column, nil
+ attr = Default.arel_attribute(:char1)
+ comparison = connection.case_insensitive_comparison(attr, nil)
assert_match(/lower/i, comparison.to_sql)
- column = Default.columns_hash["char2"]
- comparison = connection.case_insensitive_comparison table, :char2, column, nil
+ attr = Default.arel_attribute(:char2)
+ comparison = connection.case_insensitive_comparison(attr, nil)
assert_match(/lower/i, comparison.to_sql)
- column = Default.columns_hash["char3"]
- comparison = connection.case_insensitive_comparison table, :char3, column, nil
+ attr = Default.arel_attribute(:char3)
+ comparison = connection.case_insensitive_comparison(attr, nil)
assert_match(/lower/i, comparison.to_sql)
- column = Default.columns_hash["multiline_default"]
- comparison = connection.case_insensitive_comparison table, :multiline_default, column, nil
+ attr = Default.arel_attribute(:multiline_default)
+ comparison = connection.case_insensitive_comparison(attr, nil)
assert_match(/lower/i, comparison.to_sql)
end
end
diff --git a/activerecord/test/cases/arel/attributes/attribute_test.rb b/activerecord/test/cases/arel/attributes/attribute_test.rb
index 671e273543..c7bd0a053b 100644
--- a/activerecord/test/cases/arel/attributes/attribute_test.rb
+++ b/activerecord/test/cases/arel/attributes/attribute_test.rb
@@ -560,7 +560,7 @@ module Arel
end
end
- describe "with a range" do
+ describe "#between" do
it "can be constructed with a standard range" do
attribute = Attribute.new nil, nil
node = attribute.between(1..3)
@@ -628,7 +628,6 @@ module Arel
node.must_equal Nodes::NotIn.new(attribute, [])
end
-
it "can be constructed with a range ending at Infinity" do
attribute = Attribute.new nil, nil
node = attribute.between(0..::Float::INFINITY)
@@ -639,6 +638,18 @@ module Arel
)
end
+ if Gem::Version.new("2.6.0") <= Gem::Version.new(RUBY_VERSION)
+ it "can be constructed with a range implicitly ending at Infinity" do
+ attribute = Attribute.new nil, nil
+ node = attribute.between(eval("0..")) # Use eval for compatibility with Ruby < 2.6 parser
+
+ node.must_equal Nodes::GreaterThanOrEqual.new(
+ attribute,
+ Nodes::Casted.new(0, attribute)
+ )
+ end
+ end
+
it "can be constructed with a quoted range ending at Infinity" do
attribute = Attribute.new nil, nil
node = attribute.between(quoted_range(0, ::Float::INFINITY, false))
@@ -664,14 +675,6 @@ module Arel
)
])
end
-
- def quoted_range(begin_val, end_val, exclude)
- OpenStruct.new(
- begin: Nodes::Quoted.new(begin_val),
- end: Nodes::Quoted.new(end_val),
- exclude_end?: exclude,
- )
- end
end
describe "#in" do
@@ -753,21 +756,23 @@ module Arel
end
end
- describe "with a range" do
+ describe "#not_between" do
it "can be constructed with a standard range" do
attribute = Attribute.new nil, nil
node = attribute.not_between(1..3)
- node.must_equal Nodes::Grouping.new(Nodes::Or.new(
- Nodes::LessThan.new(
- attribute,
- Nodes::Casted.new(1, attribute)
- ),
- Nodes::GreaterThan.new(
- attribute,
- Nodes::Casted.new(3, attribute)
+ node.must_equal Nodes::Grouping.new(
+ Nodes::Or.new(
+ Nodes::LessThan.new(
+ attribute,
+ Nodes::Casted.new(1, attribute)
+ ),
+ Nodes::GreaterThan.new(
+ attribute,
+ Nodes::Casted.new(3, attribute)
+ )
)
- ))
+ )
end
it "can be constructed with a range starting from -Infinity" do
@@ -780,6 +785,16 @@ module Arel
)
end
+ it "can be constructed with a quoted range starting from -Infinity" do
+ attribute = Attribute.new nil, nil
+ node = attribute.not_between(quoted_range(-::Float::INFINITY, 3, false))
+
+ node.must_equal Nodes::GreaterThan.new(
+ attribute,
+ Nodes::Quoted.new(3)
+ )
+ end
+
it "can be constructed with an exclusive range starting from -Infinity" do
attribute = Attribute.new nil, nil
node = attribute.not_between(-::Float::INFINITY...3)
@@ -790,6 +805,16 @@ module Arel
)
end
+ it "can be constructed with a quoted exclusive range starting from -Infinity" do
+ attribute = Attribute.new nil, nil
+ node = attribute.not_between(quoted_range(-::Float::INFINITY, 3, true))
+
+ node.must_equal Nodes::GreaterThanOrEqual.new(
+ attribute,
+ Nodes::Quoted.new(3)
+ )
+ end
+
it "can be constructed with an infinite range" do
attribute = Attribute.new nil, nil
node = attribute.not_between(-::Float::INFINITY..::Float::INFINITY)
@@ -797,6 +822,13 @@ module Arel
node.must_equal Nodes::In.new(attribute, [])
end
+ it "can be constructed with a quoted infinite range" do
+ attribute = Attribute.new nil, nil
+ node = attribute.not_between(quoted_range(-::Float::INFINITY, ::Float::INFINITY, false))
+
+ node.must_equal Nodes::In.new(attribute, [])
+ end
+
it "can be constructed with a range ending at Infinity" do
attribute = Attribute.new nil, nil
node = attribute.not_between(0..::Float::INFINITY)
@@ -807,20 +839,44 @@ module Arel
)
end
+ if Gem::Version.new("2.6.0") <= Gem::Version.new(RUBY_VERSION)
+ it "can be constructed with a range implicitly ending at Infinity" do
+ attribute = Attribute.new nil, nil
+ node = attribute.not_between(eval("0..")) # Use eval for compatibility with Ruby < 2.6 parser
+
+ node.must_equal Nodes::LessThan.new(
+ attribute,
+ Nodes::Casted.new(0, attribute)
+ )
+ end
+ end
+
+ it "can be constructed with a quoted range ending at Infinity" do
+ attribute = Attribute.new nil, nil
+ node = attribute.not_between(quoted_range(0, ::Float::INFINITY, false))
+
+ node.must_equal Nodes::LessThan.new(
+ attribute,
+ Nodes::Quoted.new(0)
+ )
+ end
+
it "can be constructed with an exclusive range" do
attribute = Attribute.new nil, nil
node = attribute.not_between(0...3)
- node.must_equal Nodes::Grouping.new(Nodes::Or.new(
- Nodes::LessThan.new(
- attribute,
- Nodes::Casted.new(0, attribute)
- ),
- Nodes::GreaterThanOrEqual.new(
- attribute,
- Nodes::Casted.new(3, attribute)
+ node.must_equal Nodes::Grouping.new(
+ Nodes::Or.new(
+ Nodes::LessThan.new(
+ attribute,
+ Nodes::Casted.new(0, attribute)
+ ),
+ Nodes::GreaterThanOrEqual.new(
+ attribute,
+ Nodes::Casted.new(3, attribute)
+ )
)
- ))
+ )
end
end
@@ -1010,6 +1066,15 @@ module Arel
condition.to_sql.must_equal %("foo"."id" = (select 1))
end
end
+
+ private
+ def quoted_range(begin_val, end_val, exclude)
+ OpenStruct.new(
+ begin: Nodes::Quoted.new(begin_val),
+ end: Nodes::Quoted.new(end_val),
+ exclude_end?: exclude,
+ )
+ end
end
end
end
diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb
index 5921193374..4b9b55f822 100644
--- a/activerecord/test/cases/associations/has_many_associations_test.rb
+++ b/activerecord/test/cases/associations/has_many_associations_test.rb
@@ -1815,6 +1815,22 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal num_accounts, Account.count
end
+ def test_depends_and_nullify_on_polymorphic_assoc
+ author = PersonWithPolymorphicDependentNullifyComments.create!(first_name: "Laertis")
+ comment = posts(:welcome).comments.first
+ comment.author = author
+ comment.save!
+
+ assert_equal comment.author_id, author.id
+ assert_equal comment.author_type, author.class.name
+
+ author.destroy
+ comment.reload
+
+ assert_nil comment.author_id
+ assert_nil comment.author_type
+ end
+
def test_restrict_with_exception
firm = RestrictedWithExceptionFirm.create!(name: "restrict")
firm.companies.create(name: "child")
diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb
index bf574f6637..3e5b5c1275 100644
--- a/activerecord/test/cases/associations/has_one_associations_test.rb
+++ b/activerecord/test/cases/associations/has_one_associations_test.rb
@@ -12,6 +12,9 @@ require "models/bulb"
require "models/author"
require "models/image"
require "models/post"
+require "models/drink_designer"
+require "models/chef"
+require "models/department"
class HasOneAssociationsTest < ActiveRecord::TestCase
self.use_transactional_tests = false unless supports_savepoints?
@@ -114,6 +117,21 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
assert_nil Account.find(old_account_id).firm_id
end
+ def test_nullify_on_polymorphic_association
+ department = Department.create!
+ designer = DrinkDesignerWithPolymorphicDependentNullifyChef.create!
+ chef = department.chefs.create!(employable: designer)
+
+ assert_equal chef.employable_id, designer.id
+ assert_equal chef.employable_type, designer.class.name
+
+ designer.destroy!
+ chef.reload
+
+ assert_nil chef.employable_id
+ assert_nil chef.employable_type
+ end
+
def test_nullification_on_destroyed_association
developer = Developer.create!(name: "Someone")
ship = Ship.create!(name: "Planet Caravan", developer: developer)
diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb
index 961ae03a4c..1c53362bac 100644
--- a/activerecord/test/cases/finder_test.rb
+++ b/activerecord/test/cases/finder_test.rb
@@ -21,6 +21,7 @@ require "models/dog"
require "models/car"
require "models/tyre"
require "models/subscriber"
+require "support/stubs/strong_parameters"
class FinderTest < ActiveRecord::TestCase
fixtures :companies, :topics, :entrants, :developers, :developers_projects, :posts, :comments, :accounts, :authors, :author_addresses, :customers, :categories, :categorizations, :cars
@@ -224,6 +225,14 @@ class FinderTest < ActiveRecord::TestCase
assert_equal true, Subscriber.exists?(" ")
end
+ def test_exists_with_strong_parameters
+ assert_equal false, Subscriber.exists?(Parameters.new(nick: "foo"))
+
+ Subscriber.create!(nick: "foo")
+
+ assert_equal true, Subscriber.exists?(Parameters.new(nick: "foo"))
+ end
+
def test_exists_passing_active_record_object_is_not_permitted
assert_raises(ArgumentError) do
Topic.exists?(Topic.new)
diff --git a/activerecord/test/cases/serialized_attribute_test.rb b/activerecord/test/cases/serialized_attribute_test.rb
index f6cd4f85ee..fa136fe8da 100644
--- a/activerecord/test/cases/serialized_attribute_test.rb
+++ b/activerecord/test/cases/serialized_attribute_test.rb
@@ -22,7 +22,7 @@ class SerializedAttributeTest < ActiveRecord::TestCase
end
def test_serialize_does_not_eagerly_load_columns
- Topic.reset_column_information
+ reset_column_information_of(Topic)
assert_no_queries do
Topic.serialize(:content)
end
@@ -377,7 +377,8 @@ class SerializedAttributeTest < ActiveRecord::TestCase
topic.update group: "1"
model.serialize :group, JSON
- model.reset_column_information
+
+ reset_column_information_of(model)
# This isn't strictly necessary for the test, but a little bit of
# knowledge of internals allows us to make failures far more likely.
@@ -397,4 +398,12 @@ class SerializedAttributeTest < ActiveRecord::TestCase
# raw string ("1"), or raise an exception.
assert_equal [1] * threads.size, threads.map(&:value)
end
+
+ private
+
+ def reset_column_information_of(topic_class)
+ topic_class.reset_column_information
+ # reset original topic to undefine attribute methods
+ ::Topic.reset_column_information
+ end
end
diff --git a/activerecord/test/models/drink_designer.rb b/activerecord/test/models/drink_designer.rb
index eb6701b84e..8258408f35 100644
--- a/activerecord/test/models/drink_designer.rb
+++ b/activerecord/test/models/drink_designer.rb
@@ -4,5 +4,11 @@ class DrinkDesigner < ActiveRecord::Base
has_one :chef, as: :employable
end
+class DrinkDesignerWithPolymorphicDependentNullifyChef < ActiveRecord::Base
+ self.table_name = "drink_designers"
+
+ has_one :chef, as: :employable, dependent: :nullify
+end
+
class MocktailDesigner < DrinkDesigner
end
diff --git a/activerecord/test/models/person.rb b/activerecord/test/models/person.rb
index 5cba1e440e..c3d15a571a 100644
--- a/activerecord/test/models/person.rb
+++ b/activerecord/test/models/person.rb
@@ -62,6 +62,11 @@ class PersonWithDependentNullifyJobs < ActiveRecord::Base
has_many :jobs, source: :job, through: :references, dependent: :nullify
end
+class PersonWithPolymorphicDependentNullifyComments < ActiveRecord::Base
+ self.table_name = "people"
+ has_many :comments, as: :author, dependent: :nullify
+end
+
class LoosePerson < ActiveRecord::Base
self.table_name = "people"
self.abstract_class = true
diff --git a/activerecord/test/support/stubs/strong_parameters.rb b/activerecord/test/support/stubs/strong_parameters.rb
new file mode 100644
index 0000000000..acba3a4504
--- /dev/null
+++ b/activerecord/test/support/stubs/strong_parameters.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class Parameters
+ def initialize(parameters = {})
+ @parameters = parameters.with_indifferent_access
+ end
+
+ def permitted?
+ true
+ end
+
+ def to_h
+ @parameters.to_h
+ end
+end
diff --git a/activestorage/README.md b/activestorage/README.md
index bd31f0ea58..4a683dd8cd 100644
--- a/activestorage/README.md
+++ b/activestorage/README.md
@@ -118,7 +118,7 @@ Active Storage, with its included JavaScript library, supports uploading directl
```
Using the npm package:
```js
- import * as ActiveStorage from "activestorage"
+ import * as ActiveStorage from "@rails/activestorage"
ActiveStorage.start()
```
2. Annotate file inputs with the direct upload URL.
diff --git a/activestorage/package.json b/activestorage/package.json
index 00876985cf..37706efe37 100644
--- a/activestorage/package.json
+++ b/activestorage/package.json
@@ -1,5 +1,5 @@
{
- "name": "activestorage",
+ "name": "@rails/activestorage",
"version": "6.0.0-alpha",
"description": "Attach cloud and local files in Rails applications",
"main": "app/assets/javascripts/activestorage.js",
diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md
index 360cef2b41..d4eaee9f6d 100644
--- a/activesupport/CHANGELOG.md
+++ b/activesupport/CHANGELOG.md
@@ -1,3 +1,7 @@
+* Fix `String#safe_constantize` throwing a `LoadError` for incorrectly cased constant references.
+
+ *Keenan Brock*
+
* Preserve key order passed to `ActiveSupport::CacheStore#fetch_multi`.
`fetch_multi(*names)` now returns its results in the same order as the `*names` requested, rather than returning cache hits followed by cache misses.
diff --git a/activesupport/lib/active_support/core_ext/date_and_time/calculations.rb b/activesupport/lib/active_support/core_ext/date_and_time/calculations.rb
index 05abd83221..e2e11545e2 100644
--- a/activesupport/lib/active_support/core_ext/date_and_time/calculations.rb
+++ b/activesupport/lib/active_support/core_ext/date_and_time/calculations.rb
@@ -134,7 +134,7 @@ module DateAndTime
# now = DateTime.current # => Fri, 10 Jul 2015 18:41:29 +0000
# now.beginning_of_quarter # => Wed, 01 Jul 2015 00:00:00 +0000
def beginning_of_quarter
- first_quarter_month = [10, 7, 4, 1].detect { |m| m <= month }
+ first_quarter_month = month - (2 + month) % 3
beginning_of_month.change(month: first_quarter_month)
end
alias :at_beginning_of_quarter :beginning_of_quarter
@@ -149,7 +149,7 @@ module DateAndTime
# now = DateTime.current # => Fri, 10 Jul 2015 18:41:29 +0000
# now.end_of_quarter # => Wed, 30 Sep 2015 23:59:59 +0000
def end_of_quarter
- last_quarter_month = [3, 6, 9, 12].detect { |m| m >= month }
+ last_quarter_month = month + (12 - month) % 3
beginning_of_month.change(month: last_quarter_month).end_of_month
end
alias :at_end_of_quarter :end_of_quarter
diff --git a/activesupport/lib/active_support/inflector/methods.rb b/activesupport/lib/active_support/inflector/methods.rb
index 1af9833d46..ee193add6f 100644
--- a/activesupport/lib/active_support/inflector/methods.rb
+++ b/activesupport/lib/active_support/inflector/methods.rb
@@ -328,6 +328,8 @@ module ActiveSupport
e.name.to_s == camel_cased_word.to_s)
rescue ArgumentError => e
raise unless /not missing constant #{const_regexp(camel_cased_word)}!$/.match?(e.message)
+ rescue LoadError => e
+ raise unless /Unable to autoload constant #{const_regexp(camel_cased_word)}/.match?(e.message)
end
# Returns the suffix that should be added to a number to denote the position
diff --git a/activesupport/test/autoloading_fixtures/raises_load_error.rb b/activesupport/test/autoloading_fixtures/raises_load_error.rb
new file mode 100644
index 0000000000..f97be29b71
--- /dev/null
+++ b/activesupport/test/autoloading_fixtures/raises_load_error.rb
@@ -0,0 +1,4 @@
+# frozen_string_literal: true
+
+# raises a load error typical of the dynamic code that manually raises load errors
+raise LoadError, "required gem not present kind of error"
diff --git a/activesupport/test/constantize_test_cases.rb b/activesupport/test/constantize_test_cases.rb
index 2c6145940b..cdb8441b81 100644
--- a/activesupport/test/constantize_test_cases.rb
+++ b/activesupport/test/constantize_test_cases.rb
@@ -112,6 +112,16 @@ module ConstantizeTestCases
assert_nil yield("A::Object::B")
assert_nil yield("A::Object::Object::Object::B")
+ with_autoloading_fixtures do
+ assert_nil yield("Em")
+ end
+
+ assert_raises(LoadError) do
+ with_autoloading_fixtures do
+ yield("RaisesLoadError")
+ end
+ end
+
assert_raises(NameError) do
with_autoloading_fixtures do
yield("RaisesNameError")
diff --git a/guides/source/6_0_release_notes.md b/guides/source/6_0_release_notes.md
index 6f1db126c3..e6fb15c09c 100644
--- a/guides/source/6_0_release_notes.md
+++ b/guides/source/6_0_release_notes.md
@@ -8,6 +8,7 @@ Highlights in Rails 6.0:
* Action Mailbox
* Action Text
* Parallel Testing
+* Action Cable Testing
These release notes cover only the major changes. To learn about various bug
fixes and changes, please refer to the change logs or check out the [list of
@@ -62,6 +63,13 @@ test suite. While forking processes is the default method, threading is
supported as well. Running tests in parallel reduces the time it takes
your entire test suite to run.
+### Action Cable Testing
+
+[Pull Request](https://github.com/rails/rails/pull/33659)
+
+[Action Cable testing tools](testing.html#testing-action-cable) allow you to test your
+Action Cable functionality at any level: connections, channels, broadcasts.
+
Railties
--------
@@ -84,6 +92,22 @@ Please refer to the [Changelog][action-cable] for detailed changes.
### Notable changes
+* The ActionCable javascript package has been converted from CoffeeScript
+ to ES2015, and we now publish the source code in the npm distribution.
+
+ This allows ActionCable users to depend on the javascript source code
+ rather than the compiled code, which can produce smaller javascript bundles.
+
+ This change includes some breaking changes to optional parts of the
+ ActionCable javascript API:
+
+ - Configuration of the WebSocket adapter and logger adapter have been moved
+ from properties of `ActionCable` to properties of `ActionCable.adapters`.
+
+ - The `ActionCable.startDebugging()` and `ActionCable.stopDebugging()`
+ methods have been removed and replaced with the property
+ `ActionCable.logger.enabled`.
+
Action Pack
-----------
diff --git a/guides/source/action_cable_overview.md b/guides/source/action_cable_overview.md
index 7809607574..df02d5bd91 100644
--- a/guides/source/action_cable_overview.md
+++ b/guides/source/action_cable_overview.md
@@ -27,6 +27,36 @@ client-side JavaScript framework and a server-side Ruby framework. You have
access to your full domain model written with Active Record or your ORM of
choice.
+Terminology
+-----------
+
+A single Action Cable server can handle multiple connection instances. It has one
+connection instance per WebSocket connection. A single user may have multiple
+WebSockets open to your application if they use multiple browser tabs or devices.
+The client of a WebSocket connection is called the consumer.
+
+Each consumer can in turn subscribe to multiple cable channels. Each channel
+encapsulates a logical unit of work, similar to what a controller does in
+a regular MVC setup. For example, you could have a `ChatChannel` and
+an `AppearancesChannel`, and a consumer could be subscribed to either
+or to both of these channels. At the very least, a consumer should be subscribed
+to one channel.
+
+When the consumer is subscribed to a channel, they act as a subscriber.
+The connection between the subscriber and the channel is, surprise-surprise,
+called a subscription. A consumer can act as a subscriber to a given channel
+any number of times. For example, a consumer could subscribe to multiple chat rooms
+at the same time. (And remember that a physical user may have multiple consumers,
+one per tab/device open to your connection).
+
+Each channel can then again be streaming zero or more broadcastings.
+A broadcasting is a pubsub link where anything transmitted by the broadcaster is
+sent directly to the channel subscribers who are streaming that named broadcasting.
+
+As you can see, this is a fairly deep architectural stack. There's a lot of new
+terminology to identify the new pieces, and on top of that, you're dealing
+with both client and server side reflections of each unit.
+
What is Pub/Sub
---------------
@@ -151,7 +181,7 @@ established using the following JavaScript, which is generated by default by Rai
// Action Cable provides the framework to deal with WebSockets in Rails.
// You can generate new channels where WebSocket features live using the `rails generate channel` command.
-import ActionCable from "actioncable"
+import ActionCable from "@rails/actioncable"
export default ActionCable.createConsumer()
```
@@ -165,12 +195,12 @@ you're interested in having.
A consumer becomes a subscriber by creating a subscription to a given channel:
```js
-// app/javascript/cable/subscriptions/chat_channel.js
+// app/javascript/channels/chat_channel.js
import consumer from "./consumer"
consumer.subscriptions.create({ channel: "ChatChannel", room: "Best Room" })
-// app/javascript/cable/subscriptions/appearance_channel.js
+// app/javascript/channels/appearance_channel.js
import consumer from "./consumer"
consumer.subscriptions.create({ channel: "AppearanceChannel" })
@@ -183,7 +213,7 @@ A consumer can act as a subscriber to a given channel any number of times. For
example, a consumer could subscribe to multiple chat rooms at the same time:
```js
-// app/javascript/cable/subscriptions/chat_channel.js
+// app/javascript/channels/chat_channel.js
import consumer from "./consumer"
consumer.subscriptions.create({ channel: "ChatChannel", room: "1st Room" })
@@ -261,7 +291,7 @@ connection is called a subscription. Incoming messages are then routed to
these channel subscriptions based on an identifier sent by the cable consumer.
```js
-// app/javascript/cable/subscriptions/chat_channel.js
+// app/javascript/channels/chat_channel.js
// Assumes you've already requested the right to send web notifications
import consumer from "./consumer"
@@ -305,7 +335,7 @@ An object passed as the first argument to `subscriptions.create` becomes the
params hash in the cable channel. The keyword `channel` is required:
```js
-// app/javascript/cable/subscriptions/chat_channel.js
+// app/javascript/channels/chat_channel.js
import consumer from "./consumer"
consumer.subscriptions.create({ channel: "ChatChannel", room: "Best Room" }, {
@@ -359,7 +389,7 @@ end
```
```js
-// app/javascript/cable/subscriptions/chat_channel.js
+// app/javascript/channels/chat_channel.js
import consumer from "./consumer"
const chatChannel = consumer.subscriptions.create({ channel: "ChatChannel", room: "Best Room" }, {
@@ -419,7 +449,7 @@ appear/disappear API could be backed by Redis, a database, or whatever else.
Create the client-side appearance channel subscription:
```js
-// app/javascript/cable/subscriptions/appearance_channel.js
+// app/javascript/channels/appearance_channel.js
import consumer from "./consumer"
consumer.subscriptions.create("AppearanceChannel", {
@@ -534,7 +564,7 @@ end
Create the client-side web notifications channel subscription:
```js
-// app/javascript/cable/subscriptions/web_notifications_channel.js
+// app/javascript/channels/web_notifications_channel.js
// Client-side which assumes you've already requested
// the right to send web notifications.
import consumer from "./consumer"
@@ -738,3 +768,8 @@ internally, irrespective of whether the application server is multi-threaded or
Accordingly, Action Cable works with popular servers like Unicorn, Puma, and
Passenger.
+
+## Testing
+
+You can find detailed instructions on how to test your Action Cable functionality in the
+[testing guide](testing.html#testing-action-cable).
diff --git a/guides/source/action_mailbox_basics.md b/guides/source/action_mailbox_basics.md
index eb8a14b4d2..c90892d456 100644
--- a/guides/source/action_mailbox_basics.md
+++ b/guides/source/action_mailbox_basics.md
@@ -20,8 +20,8 @@ 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.
+Postmark, and SendGrid. You can also handle inbound mails directly via the
+built-in Exim, Postfix, and Qmail ingresses.
The inbound emails are turned into `InboundEmail` records using Active Record
and feature lifecycle tracking, storage of the original email on cloud storage
@@ -65,6 +65,36 @@ to deliver emails to your application via POST requests to
`https://example.com`, you would specify the fully-qualified URL
`https://example.com/rails/action_mailbox/amazon/inbound_emails`.
+### Exim
+
+Tell Action Mailbox to accept emails from an SMTP relay:
+
+```ruby
+# config/environments/production.rb
+config.action_mailbox.ingress = :relay
+```
+
+Generate a strong password that Action Mailbox can use to authenticate requests to the relay 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 Exim to pipe inbound emails to `bin/rails action_mailbox:ingress:exim`,
+providing the `URL` of the relay ingress and the `INGRESS_PASSWORD` you
+previously generated. If your application lived at `https://example.com`, the
+full command would look like this:
+
+```shell
+bin/rails action_mailbox:ingress:exim URL=https://example.com/rails/action_mailbox/relay/inbound_emails INGRESS_PASSWORD=...
+```
+
### Mailgun
Give Action Mailbox your
@@ -126,14 +156,14 @@ the fully-qualified URL `https://example.com/rails/action_mailbox/mandrill/inbou
### Postfix
-Tell Action Mailbox to accept emails from Postfix:
+Tell Action Mailbox to accept emails from an SMTP relay:
```ruby
# config/environments/production.rb
-config.action_mailbox.ingress = :postfix
+config.action_mailbox.ingress = :relay
```
-Generate a strong password that Action Mailbox can use to authenticate requests to the Postfix ingress.
+Generate a strong password that Action Mailbox can use to authenticate requests to the relay 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:
@@ -151,8 +181,74 @@ 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
+```shell
+$ bin/rails action_mailbox:ingress:postfix URL=https://example.com/rails/action_mailbox/relay/inbound_emails INGRESS_PASSWORD=...
+```
+
+### Postmark
+
+Tell Action Mailbox to accept emails from Postmark:
+
+```ruby
+# config/environments/production.rb
+config.action_mailbox.ingress = :postmark
+```
+
+Generate a strong password that Action Mailbox can use to authenticate
+requests to the Postmark 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 Postmark inbound webhook](https://postmarkapp.com/manual#configure-your-inbound-webhook-url)
+to forward inbound emails to `/rails/action_mailbox/postmark/inbound_emails` with the username `actionmailbox`
+and the password you previously generated. If your application lived at `https://example.com`, you would
+configure Postmark with the following fully-qualified URL:
+
+```
+https://actionmailbox:PASSWORD@example.com/rails/action_mailbox/postmark/inbound_emails
+```
+
+NOTE: When configuring your Postmark inbound webhook, be sure to check the box labeled **"Include raw email content in JSON payload"**.
+Action Mailbox needs the raw email content to work.
+
+### Qmail
+
+Tell Action Mailbox to accept emails from an SMTP relay:
+
+```ruby
+# config/environments/production.rb
+config.action_mailbox.ingress = :relay
+```
+
+Generate a strong password that Action Mailbox can use to authenticate requests to the relay 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 Qmail to pipe inbound emails to `bin/rails action_mailbox:ingress:qmail`,
+providing the `URL` of the relay ingress and the `INGRESS_PASSWORD` you
+previously generated. If your application lived at `https://example.com`, the
+full command would look like this:
+
+```shell
+bin/rails action_mailbox:ingress:qmail URL=https://example.com/rails/action_mailbox/relay/inbound_emails INGRESS_PASSWORD=...
```
### SendGrid
diff --git a/guides/source/active_record_basics.md b/guides/source/active_record_basics.md
index a67e2924d7..b0d4bbd2c0 100644
--- a/guides/source/active_record_basics.md
+++ b/guides/source/active_record_basics.md
@@ -105,9 +105,9 @@ depending on the purpose of these columns.
fields that Active Record will look for when you create associations between
your models.
* **Primary keys** - By default, Active Record will use an integer column named
- `id` as the table's primary key. When using [Active Record
- Migrations](active_record_migrations.html) to create your tables, this column will be
- automatically created.
+ `id` as the table's primary key (`bigint` for Postgres and MYSQL, `integer`
+ for SQLite). When using [Active Record Migrations](active_record_migrations.html)
+ to create your tables, this column will be automatically created.
There are also some optional column names that will add additional features
to Active Record instances:
diff --git a/guides/source/active_storage_overview.md b/guides/source/active_storage_overview.md
index 51f50e8931..6d07d34dd7 100644
--- a/guides/source/active_storage_overview.md
+++ b/guides/source/active_storage_overview.md
@@ -489,7 +489,7 @@ directly from the client to the cloud.
Using the npm package:
```js
- import * as ActiveStorage from "activestorage"
+ import * as ActiveStorage from "@rails/activestorage"
ActiveStorage.start()
```
@@ -616,7 +616,7 @@ of choice, instantiate a DirectUpload and call its create method. Create takes
a callback to invoke when the upload completes.
```js
-import { DirectUpload } from "activestorage"
+import { DirectUpload } from "@rails/activestorage"
const input = document.querySelector('input[type=file]')
@@ -664,7 +664,7 @@ will call the object's `directUploadWillStoreFileWithXHR` method. You can then
bind your own progress handler on the XHR.
```js
-import { DirectUpload } from "activestorage"
+import { DirectUpload } from "@rails/activestorage"
class Uploader {
constructor(file, url) {
diff --git a/guides/source/association_basics.md b/guides/source/association_basics.md
index 4f3e8b2cff..38e46e4bf8 100644
--- a/guides/source/association_basics.md
+++ b/guides/source/association_basics.md
@@ -1257,7 +1257,7 @@ Controls what happens to the associated object when its owner is destroyed:
* `:destroy` causes the associated object to also be destroyed
* `:delete` causes the associated object to be deleted directly from the database (so callbacks will not execute)
-* `:nullify` causes the foreign key to be set to `NULL`. Callbacks are not executed.
+* `:nullify` causes the foreign key to be set to `NULL`. Polymorphic type column is also nullified on polymorphic associations. Callbacks are not executed.
* `:restrict_with_exception` causes an `ActiveRecord::DeleteRestrictionError` exception to be raised if there is an associated record
* `:restrict_with_error` causes an error to be added to the owner if there is an associated object
@@ -1658,7 +1658,7 @@ Controls what happens to the associated objects when their owner is destroyed:
* `:destroy` causes all the associated objects to also be destroyed
* `:delete_all` causes all the associated objects to be deleted directly from the database (so callbacks will not execute)
-* `:nullify` causes the foreign keys to be set to `NULL`. Callbacks are not executed.
+* `:nullify` causes the foreign key to be set to `NULL`. Polymorphic type column is also nullified on polymorphic associations. Callbacks are not executed.
* `:restrict_with_exception` causes an `ActiveRecord::DeleteRestrictionError` exception to be raised if there are any associated records
* `:restrict_with_error` causes an error to be added to the owner if there are any associated objects
diff --git a/guides/source/configuring.md b/guides/source/configuring.md
index 3a7baf84a9..32682fb91f 100644
--- a/guides/source/configuring.md
+++ b/guides/source/configuring.md
@@ -721,6 +721,8 @@ There are a number of settings available on `config.action_mailer`:
* `config.action_mailer.perform_caching` specifies whether the mailer templates should perform fragment caching or not. By default this is `false` in all environments.
+* `config.action_mailer.delivery_job` specifies delivery job for mail. Defaults to `ActionMailer::DeliveryJob`.
+
### Configuring Active Support
@@ -905,6 +907,7 @@ text/javascript image/svg+xml application/postscript application/x-shockwave-fla
- `config.action_view.default_enforce_utf8`: `false`
- `config.action_dispatch.use_cookies_with_metadata`: `true`
+- `config.action_mailer.delivery_job`: `"ActionMailer::MailDeliveryJob"`
- `config.active_job.return_false_on_aborted_enqueue`: `true`
- `config.active_storage.queues.analysis`: `:active_storage_analysis`
- `config.active_storage.queues.purge`: `:active_storage_purge`
diff --git a/guides/source/engines.md b/guides/source/engines.md
index c4829299ca..f15383e3f1 100644
--- a/guides/source/engines.md
+++ b/guides/source/engines.md
@@ -1091,16 +1091,15 @@ main Rails application.
Engine model and controller classes can be extended by open classing them in the
main Rails application (since model and controller classes are just Ruby classes
that inherit Rails specific functionality). Open classing an Engine class
-redefines it for use in the main application. This is usually implemented by
-using the decorator pattern.
+redefines it for use in the main application.
For simple class modifications, use `Class#class_eval`. For complex class
modifications, consider using `ActiveSupport::Concern`.
-#### A note on Decorators and Loading Code
+#### A note on Overriding and Loading Code
-Because these decorators are not referenced by your Rails application itself,
-Rails' autoloading system will not kick in and load your decorators. This means
+Because these overrides are not referenced by your Rails application itself,
+Rails' autoloading system will not kick in and load your overrides. This means
that you need to require them yourself.
Here is some sample code to do this:
@@ -1112,7 +1111,7 @@ module Blorgh
isolate_namespace Blorgh
config.to_prepare do
- Dir.glob(Rails.root + "app/decorators/**/*_decorator*.rb").each do |c|
+ Dir.glob(Rails.root + "app/overrides/**/*_override*.rb").each do |c|
require_dependency(c)
end
end
@@ -1120,15 +1119,15 @@ module Blorgh
end
```
-This doesn't apply to just Decorators, but anything that you add in an engine
+This doesn't apply to just overrides, but anything that you add in an engine
that isn't referenced by your main application.
-#### Implementing Decorator Pattern Using Class#class_eval
+#### Reopening existing classes using Class#class_eval
**Adding** `Article#time_since_created`:
```ruby
-# MyApp/app/decorators/models/blorgh/article_decorator.rb
+# MyApp/app/overrides/models/blorgh/article_override.rb
Blorgh::Article.class_eval do
def time_since_created
@@ -1149,7 +1148,7 @@ end
**Overriding** `Article#summary`:
```ruby
-# MyApp/app/decorators/models/blorgh/article_decorator.rb
+# MyApp/app/overrides/models/blorgh/article_override.rb
Blorgh::Article.class_eval do
def summary
@@ -1169,7 +1168,7 @@ class Article < ApplicationRecord
end
```
-#### Implementing Decorator Pattern Using ActiveSupport::Concern
+#### Reopening existing classes using ActiveSupport::Concern
Using `Class#class_eval` is great for simple adjustments, but for more complex
class modifications, you might want to consider using [`ActiveSupport::Concern`]
diff --git a/guides/source/i18n.md b/guides/source/i18n.md
index 08cad375ef..d146685675 100644
--- a/guides/source/i18n.md
+++ b/guides/source/i18n.md
@@ -139,10 +139,12 @@ Note that appending directly to `I18n.load_paths` instead of to the application'
### Managing the Locale across Requests
-The default locale is used for all translations unless `I18n.locale` is explicitly set.
-
A localized application will likely need to provide support for multiple locales. To accomplish this, the locale should be set at the beginning of each request so that all strings are translated using the desired locale during the lifetime of that request.
+The default locale is used for all translations unless `I18n.locale=` or `I18n.with_locale` is used.
+
+`I18n.locale` can leak into subsequent requests served by the same thread/process if it is not consistently set in every controller. For example executing `I18n.locale = :es` in one POST requests will have effects for all later requests to controllers that don't set the locale, but only in that particular thread/process. For that reason, instead of `I18n.locale =` you can use `I18n.with_locale` which does not have this leak issue.
+
The locale can be set in an `around_action` in the `ApplicationController`:
```ruby
diff --git a/guides/source/routing.md b/guides/source/routing.md
index 0a0f1b6754..a33ac6a589 100644
--- a/guides/source/routing.md
+++ b/guides/source/routing.md
@@ -519,7 +519,7 @@ resources :photos do
end
```
-You can leave out the `:on` option, this will create the same member route except that the resource id value will be available in `params[:photo_id]` instead of `params[:id]`.
+You can leave out the `:on` option, this will create the same member route except that the resource id value will be available in `params[:photo_id]` instead of `params[:id]`. Route helpers will also be renamed from `preview_photo_url` and `preview_photo_path` to `photo_preview_url` and `photo_preview_path`.
#### Adding Collection Routes
diff --git a/guides/source/testing.md b/guides/source/testing.md
index f34f9d95f4..1a2f480407 100644
--- a/guides/source/testing.md
+++ b/guides/source/testing.md
@@ -33,11 +33,11 @@ Rails creates a `test` directory for you as soon as you create a Rails project u
```bash
$ ls -F test
-application_system_test_case.rb fixtures/ integration/ models/ test_helper.rb
-controllers/ helpers/ mailers/ system/
+application_system_test_case.rb controllers/ helpers/ mailers/ system/
+channels/ fixtures/ integration/ models/ test_helper.rb
```
-The `helpers`, `mailers`, and `models` directories are meant to hold tests for view helpers, mailers, and models, respectively. The `controllers` directory is meant to hold tests for controllers, routes, and views. The `integration` directory is meant to hold tests for interactions between controllers.
+The `helpers`, `mailers`, and `models` directories are meant to hold tests for view helpers, mailers, and models, respectively. The `channels` directory is meant to hold tests for Action Cable connection and channels. The `controllers` directory is meant to hold tests for controllers, routes, and views. The `integration` directory is meant to hold tests for interactions between controllers.
The system test directory holds system tests, which are used for full browser
testing of your application. System tests allow you to test your application
@@ -1731,6 +1731,112 @@ class ProductTest < ActiveJob::TestCase
end
```
+Testing Action Cable
+--------------------
+
+Since Action Cable is used at different levels inside your application,
+you'll need to test both the channels, connection classes themselves, and that other
+entities broadcast correct messages.
+
+### Connection Test Case
+
+By default, when you generate new Rails application with Action Cable, a test for the base connection class (`ApplicationCable::Connection`) is generated as well under `test/channels/application_cable` directory.
+
+Connection tests aim to check whether a connection's identifiers get assigned properly
+or that any improper connection requests are rejected. Here is an example:
+
+```ruby
+class ApplicationCable::ConnectionTest < ActionCable::Connection::TestCase
+ test "connects with params" do
+ # Simulate a connection opening by calling the `connect` method
+ connect params: { user_id: 42 }
+
+ # You can access the Connection object via `connection` in tests
+ assert_equal connection.user_id, "42"
+ end
+
+ test "rejects connection without params" do
+ # Use `assert_reject_connection` matcher to verify that
+ # connection is rejected
+ assert_reject_connection { connect }
+ end
+end
+```
+
+You can also specify request cookies the same way you do in integration tests:
+
+```ruby
+test "connects with cookies" do
+ cookies.signed[:user_id] = "42"
+
+ connect
+
+ assert_equal connection.user_id, "42"
+end
+```
+
+See the API documentation for [`AcionCable::Connection::TestCase`](http://api.rubyonrails.org/classes/ActionCable/Connection/TestCase.html) for more information.
+
+### Channel Test Case
+
+By default, when you generate a channel, an associated test will be generated as well
+under the `test/channels` directory. Here's an example test with a chat channel:
+
+```ruby
+require "test_helper"
+
+class ChatChannelTest < ActionCable::Channel::TestCase
+ test "subscribes and stream for room" do
+ # Simulate a subscription creation by calling `subscribe`
+ subscribe room: "15"
+
+ # You can access the Channel object via `subscription` in tests
+ assert subscription.confirmed?
+ assert_has_stream "chat_15"
+ end
+end
+```
+
+This test is pretty simple and only asserts that the channel subscribes the connection to a particular stream.
+
+You can also specify the underlying connection identifiers. Here's an example test with a web notifications channel:
+
+```ruby
+require "test_helper"
+
+class WebNotificationsChannelTest < ActionCable::Channel::TestCase
+ test "subscribes and stream for user" do
+ stub_connection current_user: users[:john]
+
+ subscribe
+
+ assert_has_stream_for users[:john]
+ end
+end
+```
+
+See the API documentation for [`AcionCable::Channel::TestCase`](http://api.rubyonrails.org/classes/ActionCable/Channel/TestCase.html) for more information.
+
+### Custom Assertions And Testing Broadcasts Inside Other Components
+
+Action Cable ships with a bunch of custom assertions that can be used to lessen the verbosity of tests. For a full list of available assertions, see the API documentation for [`ActionCable::TestHelper`](http://api.rubyonrails.org/classes/ActionCable/TestHelper.html).
+
+It's a good practice to ensure that the correct message has been broadcasted inside other components (e.g. inside your controllers). This is precisely where
+the custom assertions provided by Action Cable are pretty useful. For instance,
+within a model:
+
+```ruby
+require 'test_helper'
+
+class ProductTest < ActionCable::TestCase
+ test "broadcast status after charge" do
+ assert_broadcast_on("products:#{product.id}", type: "charged") do
+ product.charge(account)
+ end
+ end
+end
+```
+
Additional Testing Resources
----------------------------
diff --git a/guides/source/upgrading_ruby_on_rails.md b/guides/source/upgrading_ruby_on_rails.md
index 2682c6ffd7..1f30ce1971 100644
--- a/guides/source/upgrading_ruby_on_rails.md
+++ b/guides/source/upgrading_ruby_on_rails.md
@@ -97,6 +97,42 @@ If you require your cookies to be read by 5.2 and older, or you are still valida
to allow you to rollback set
`Rails.application.config.action_dispatch.use_cookies_with_metadata` to `false`.
+### ActionCable javascript API Changes
+
+The ActionCable javascript package has been converted from CoffeeScript
+to ES2015, and we now publish the source code in the npm distribution.
+
+This change includes some breaking changes to optional parts of the
+ActionCable javascript API:
+
+- Configuration of the WebSocket adapter and logger adapter have been moved
+ from properties of `ActionCable` to properties of `ActionCable.adapters`.
+ If you are currently configuring these adapters you will need to make
+ these changes when upgrading:
+
+ ```diff
+ - ActionCable.WebSocket = MyWebSocket
+ + ActionCable.adapters.WebSocket = MyWebSocket
+ ```
+ ```diff
+ - ActionCable.logger = myLogger
+ + ActionCable.adapters.logger = myLogger
+ ```
+
+- The `ActionCable.startDebugging()` and `ActionCable.stopDebugging()`
+ methods have been removed and replaced with the property
+ `ActionCable.logger.enabled`. If you are currently using these methods you
+ will need to make these changes when upgrading:
+
+ ```diff
+ - ActionCable.startDebugging()
+ + ActionCable.logger.enabled = true
+ ```
+ ```diff
+ - ActionCable.stopDebugging()
+ + ActionCable.logger.enabled = false
+ ```
+
Upgrading from Rails 5.1 to Rails 5.2
-------------------------------------
diff --git a/package.json b/package.json
index 88029de141..43055fa6bc 100644
--- a/package.json
+++ b/package.json
@@ -2,6 +2,7 @@
"private": true,
"workspaces": [
"actioncable",
+ "actiontext",
"activestorage",
"actionview",
"tmp/templates/app_template",
diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md
index c4123886bf..9c7e958b7c 100644
--- a/railties/CHANGELOG.md
+++ b/railties/CHANGELOG.md
@@ -10,6 +10,11 @@
*Gannon McGibbon*
+* Add `rails test:channels`.
+
+ *bogdanvlviv*
+
+
* Use original `bundler` environment variables during the process of generating a new rails project.
*Marco Costa*
@@ -128,12 +133,20 @@
*Richard Schneeman*
-* Support environment specific credentials file.
+* Support environment specific credentials overrides.
+
+ So any environment will look for `config/credentials/#{Rails.env}.yml.enc` and fall back
+ to `config/credentials.yml.enc`.
+
+ The encryption key can be in `ENV["RAILS_MASTER_KEY"]` or `config/credentials/production.key`.
+
+ Environment credentials overrides can be edited with `rails credentials:edit --environment production`.
+ If no override is setup for the passed environment, it will be created.
+
+ Additionally, the default lookup paths can be overwritten with these configs:
- For `production` environment look first for `config/credentials/production.yml.enc` file that can be decrypted by
- `ENV["RAILS_MASTER_KEY"]` or `config/credentials/production.key` master key.
- Edit given environment credentials file by command `rails credentials:edit --environment production`.
- Default paths can be overwritten by setting `config.credentials.content_path` and `config.credentials.key_path`.
+ - `config.credentials.content_path`
+ - `config.credentials.key_path`
*Wojciech Wnętrzak*
diff --git a/railties/RDOC_MAIN.rdoc b/railties/RDOC_MAIN.rdoc
index 89fc6bcbce..1c4252edd0 100644
--- a/railties/RDOC_MAIN.rdoc
+++ b/railties/RDOC_MAIN.rdoc
@@ -44,11 +44,11 @@ or to generate the body of an email. In \Rails, View generation is handled by {A
{Active Record}[link:files/activerecord/README_rdoc.html], {Active Model}[link:files/activemodel/README_rdoc.html],
{Action Pack}[link:files/actionpack/README_rdoc.html], and {Action View}[link:files/actionview/README_rdoc.html] can each be used independently outside \Rails.
In addition to that, \Rails also comes with {Action Mailer}[link:files/actionmailer/README_rdoc.html], a library
-to generate and send emails; {Active Job}[link:files/activejob/README_md.html], a
-framework for declaring jobs and making them run on a variety of queueing
+to generate and send emails; {Action Mailbox}[link:files/actionmailbox/README_md.html], a library to receive emails within a Rails application;
+{Active Job}[link:files/activejob/README_md.html], a framework for declaring jobs and making them run on a variety of queueing
backends; {Action Cable}[link:files/actioncable/README_md.html], a framework to
integrate WebSockets with a \Rails application; {Active Storage}[link:files/activestorage/README_md.html],
-a library to attach cloud and local files to \Rails applications;
+a library to attach cloud and local files to \Rails applications; {Action Text}[link:files/actiontext/README_md.html], a library to handle rich text content;
and {Active Support}[link:files/activesupport/README_rdoc.html], a collection
of utility classes and standard library extensions that are useful for \Rails,
and may also be used independently outside \Rails.
diff --git a/railties/lib/rails/application/configuration.rb b/railties/lib/rails/application/configuration.rb
index 3595f60bf8..c2403c57a7 100644
--- a/railties/lib/rails/application/configuration.rb
+++ b/railties/lib/rails/application/configuration.rb
@@ -293,25 +293,25 @@ module Rails
end
private
- def credentials_available_for_current_env?
- File.exist?("#{root}/config/credentials/#{Rails.env}.yml.enc")
- end
-
def default_credentials_content_path
if credentials_available_for_current_env?
- File.join(root, "config", "credentials", "#{Rails.env}.yml.enc")
+ root.join("config", "credentials", "#{Rails.env}.yml.enc")
else
- File.join(root, "config", "credentials.yml.enc")
+ root.join("config", "credentials.yml.enc")
end
end
def default_credentials_key_path
if credentials_available_for_current_env?
- File.join(root, "config", "credentials", "#{Rails.env}.key")
+ root.join("config", "credentials", "#{Rails.env}.key")
else
- File.join(root, "config", "master.key")
+ root.join("config", "master.key")
end
end
+
+ def credentials_available_for_current_env?
+ File.exist?(root.join("config", "credentials", "#{Rails.env}.yml.enc"))
+ end
end
end
end
diff --git a/railties/lib/rails/commands/credentials/USAGE b/railties/lib/rails/commands/credentials/USAGE
index 6b33d1ab74..d235592f46 100644
--- a/railties/lib/rails/commands/credentials/USAGE
+++ b/railties/lib/rails/commands/credentials/USAGE
@@ -41,9 +41,18 @@ from leaking.
=== Environment Specific Credentials
-It is possible to have credentials for each environment. If the file for current environment exists it will take
-precedence over `config/credentials.yml.enc`, thus for `production` environment first look for
-`config/credentials/production.yml.enc` that can be decrypted using master key taken from `ENV["RAILS_MASTER_KEY"]`
-or stored in `config/credentials/production.key`.
-To edit given file use command `rails credentials:edit --environment production`
-Default paths can be overwritten by setting `config.credentials.content_path` and `config.credentials.key_path`.
+The `credentials` command supports passing an `--environment` option to create an
+environment specific override. That override will takes precedence over the
+global `config/credentials.yml.enc` file when running in that environment. So:
+
+ rails credentials:edit --environment development
+
+will create `config/credentials/development.yml.enc` with the corresponding
+encryption key in `config/credentials/development.key` if the credentials file
+doesn't exist.
+
+The encryption key can also be put in `ENV["RAILS_MASTER_KEY"]`, which takes
+precedence over the file encryption key.
+
+In addition to that, the default credentials lookup paths can be overriden through
+`config.credentials.content_path` and `config.credentials.key_path`.
diff --git a/railties/lib/rails/commands/credentials/credentials_command.rb b/railties/lib/rails/commands/credentials/credentials_command.rb
index 4b30d208e0..852cd401d7 100644
--- a/railties/lib/rails/commands/credentials/credentials_command.rb
+++ b/railties/lib/rails/commands/credentials/credentials_command.rb
@@ -24,13 +24,11 @@ module Rails
ensure_editor_available(command: "bin/rails credentials:edit") || (return)
- encrypted = Rails.application.encrypted(content_path, key_path: key_path)
-
- ensure_encryption_key_has_been_added(key_path) if encrypted.key.nil?
- ensure_encrypted_file_has_been_added(content_path, key_path)
+ ensure_encryption_key_has_been_added if credentials.key.nil?
+ ensure_credentials_have_been_added
catch_editing_exceptions do
- change_encrypted_file_in_system_editor(content_path, key_path)
+ change_credentials_in_system_editor
end
say "File encrypted and saved."
@@ -41,36 +39,46 @@ module Rails
def show
require_application_and_environment!
- encrypted = Rails.application.encrypted(content_path, key_path: key_path)
-
- say encrypted.read.presence || missing_encrypted_message(key: encrypted.key, key_path: key_path, file_path: content_path)
+ say credentials.read.presence || missing_credentials_message
end
private
- def content_path
- options[:environment] ? "config/credentials/#{options[:environment]}.yml.enc" : "config/credentials.yml.enc"
- end
-
- def key_path
- options[:environment] ? "config/credentials/#{options[:environment]}.key" : "config/master.key"
+ def credentials
+ Rails.application.encrypted(content_path, key_path: key_path)
end
-
- def ensure_encryption_key_has_been_added(key_path)
+ def ensure_encryption_key_has_been_added
encryption_key_file_generator.add_key_file(key_path)
encryption_key_file_generator.ignore_key_file(key_path)
end
- def ensure_encrypted_file_has_been_added(file_path, key_path)
- encrypted_file_generator.add_encrypted_file_silently(file_path, key_path)
+ def ensure_credentials_have_been_added
+ encrypted_file_generator.add_encrypted_file_silently(content_path, key_path)
end
- def change_encrypted_file_in_system_editor(file_path, key_path)
- Rails.application.encrypted(file_path, key_path: key_path).change do |tmp_path|
+ def change_credentials_in_system_editor
+ credentials.change do |tmp_path|
system("#{ENV["EDITOR"]} #{tmp_path}")
end
end
+ def missing_credentials_message
+ if credentials.key.nil?
+ "Missing '#{key_path}' to decrypt credentials. See `rails credentials:help`"
+ else
+ "File '#{content_path}' does not exist. Use `rails credentials:edit` to change that."
+ end
+ end
+
+
+ def content_path
+ options[:environment] ? "config/credentials/#{options[:environment]}.yml.enc" : "config/credentials.yml.enc"
+ end
+
+ def key_path
+ options[:environment] ? "config/credentials/#{options[:environment]}.key" : "config/master.key"
+ end
+
def encryption_key_file_generator
require "rails/generators"
@@ -85,14 +93,6 @@ module Rails
Rails::Generators::EncryptedFileGenerator.new
end
-
- def missing_encrypted_message(key:, key_path:, file_path:)
- if key.nil?
- "Missing '#{key_path}' to decrypt credentials. See `rails credentials:help`"
- else
- "File '#{file_path}' does not exist. Use `rails credentials:edit` to change that."
- end
- end
end
end
end
diff --git a/railties/lib/rails/generators/rails/app/app_generator.rb b/railties/lib/rails/generators/rails/app/app_generator.rb
index 261a2ccb7b..5f6e817bf6 100644
--- a/railties/lib/rails/generators/rails/app/app_generator.rb
+++ b/railties/lib/rails/generators/rails/app/app_generator.rb
@@ -213,6 +213,7 @@ module Rails
empty_directory_with_keep_file "test/helpers"
empty_directory_with_keep_file "test/integration"
+ template "test/channels/application_cable/connection_test.rb"
template "test/test_helper.rb"
end
@@ -439,6 +440,7 @@ module Rails
if options[:skip_action_cable]
remove_dir "app/javascript/channels"
remove_dir "app/channels"
+ remove_dir "test/channels"
end
end
diff --git a/railties/lib/rails/generators/rails/app/templates/app/javascript/channels/consumer.js b/railties/lib/rails/generators/rails/app/templates/app/javascript/channels/consumer.js
index 76ca3d0f2f..eec7e54b8a 100644
--- a/railties/lib/rails/generators/rails/app/templates/app/javascript/channels/consumer.js
+++ b/railties/lib/rails/generators/rails/app/templates/app/javascript/channels/consumer.js
@@ -1,6 +1,6 @@
// Action Cable provides the framework to deal with WebSockets in Rails.
// You can generate new channels where WebSocket features live using the `rails generate channel` command.
-import ActionCable from "actioncable"
+import ActionCable from "@rails/actioncable"
export default ActionCable.createConsumer()
diff --git a/railties/lib/rails/generators/rails/app/templates/app/javascript/packs/application.js.tt b/railties/lib/rails/generators/rails/app/templates/app/javascript/packs/application.js.tt
index 4d7a145cd6..de91713546 100644
--- a/railties/lib/rails/generators/rails/app/templates/app/javascript/packs/application.js.tt
+++ b/railties/lib/rails/generators/rails/app/templates/app/javascript/packs/application.js.tt
@@ -3,7 +3,7 @@
// a relevant structure within app/javascript and only use these pack files to reference
// that code so it'll be compiled.
-import Rails from "rails-ujs"
+import Rails from "@rails/ujs"
Rails.start()
<%- unless options[:skip_turbolinks] -%>
@@ -12,7 +12,7 @@ Turbolinks.start()
<%- end -%>
<%- unless skip_active_storage? -%>
-import * as ActiveStorage from "activestorage"
+import * as ActiveStorage from "@rails/activestorage"
ActiveStorage.start()
<%- end -%>
<%- unless options[:skip_action_cable] -%>
diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/frontbase.yml.tt b/railties/lib/rails/generators/rails/app/templates/config/databases/frontbase.yml.tt
index 33f422c622..6ab4a26084 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/databases/frontbase.yml.tt
+++ b/railties/lib/rails/generators/rails/app/templates/config/databases/frontbase.yml.tt
@@ -24,7 +24,7 @@ test:
<<: *default
database: <%= app_name %>_test
-# As with config/secrets.yml, you never want to store sensitive information,
+# As with config/credentials.yml, you never want to store sensitive information,
# like your database password, in your source code. If your source code is
# ever seen by anyone, they now have access to your database.
#
diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/ibm_db.yml.tt b/railties/lib/rails/generators/rails/app/templates/config/databases/ibm_db.yml.tt
index 681c765e93..e422aa31fc 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/databases/ibm_db.yml.tt
+++ b/railties/lib/rails/generators/rails/app/templates/config/databases/ibm_db.yml.tt
@@ -60,7 +60,7 @@ test:
<<: *default
database: <%= app_name[0,4] %>_tst
-# As with config/secrets.yml, you never want to store sensitive information,
+# As with config/credentials.yml, you never want to store sensitive information,
# like your database password, in your source code. If your source code is
# ever seen by anyone, they now have access to your database.
#
diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbc.yml.tt b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbc.yml.tt
index af69f12059..678455c622 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbc.yml.tt
+++ b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbc.yml.tt
@@ -54,7 +54,7 @@ test:
<<: *default
url: jdbc:db://localhost/<%= app_name %>_test
-# As with config/secrets.yml, you never want to store sensitive information,
+# As with config/credentials.yml, you never want to store sensitive information,
# like your database password, in your source code. If your source code is
# ever seen by anyone, they now have access to your database.
#
diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcmysql.yml.tt b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcmysql.yml.tt
index f39593372c..b5a0efef47 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcmysql.yml.tt
+++ b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcmysql.yml.tt
@@ -27,7 +27,7 @@ test:
<<: *default
database: <%= app_name %>_test
-# As with config/secrets.yml, you never want to store sensitive information,
+# As with config/credentials.yml, you never want to store sensitive information,
# like your database password, in your source code. If your source code is
# ever seen by anyone, they now have access to your database.
#
diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcpostgresql.yml.tt b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcpostgresql.yml.tt
index 2383fe97d3..009a81a6b8 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcpostgresql.yml.tt
+++ b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcpostgresql.yml.tt
@@ -43,7 +43,7 @@ test:
<<: *default
database: <%= app_name %>_test
-# As with config/secrets.yml, you never want to store sensitive information,
+# As with config/credentials.yml, you never want to store sensitive information,
# like your database password, in your source code. If your source code is
# ever seen by anyone, they now have access to your database.
#
diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml.tt b/railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml.tt
index b6c2e7448a..386eb511e5 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml.tt
+++ b/railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml.tt
@@ -32,7 +32,7 @@ test:
<<: *default
database: <%= app_name %>_test
-# As with config/secrets.yml, you never want to store sensitive information,
+# As with config/credentials.yml, you never want to store sensitive information,
# like your database password, in your source code. If your source code is
# ever seen by anyone, they now have access to your database.
#
diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/oracle.yml.tt b/railties/lib/rails/generators/rails/app/templates/config/databases/oracle.yml.tt
index 8d9d33ba6c..f7b6dfafab 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/databases/oracle.yml.tt
+++ b/railties/lib/rails/generators/rails/app/templates/config/databases/oracle.yml.tt
@@ -33,7 +33,7 @@ test:
<<: *default
database: <%= app_name %>_test
-# As with config/secrets.yml, you never want to store sensitive information,
+# As with config/credentials.yml, you never want to store sensitive information,
# like your database password, in your source code. If your source code is
# ever seen by anyone, they now have access to your database.
#
diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml.tt b/railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml.tt
index 2f51030756..44dafbd0c0 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml.tt
+++ b/railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml.tt
@@ -59,7 +59,7 @@ test:
<<: *default
database: <%= app_name %>_test
-# As with config/secrets.yml, you never want to store sensitive information,
+# As with config/credentials.yml, you never want to store sensitive information,
# like your database password, in your source code. If your source code is
# ever seen by anyone, they now have access to your database.
#
diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/sqlserver.yml.tt b/railties/lib/rails/generators/rails/app/templates/config/databases/sqlserver.yml.tt
index 0246fb0d02..27e8f2f35d 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/databases/sqlserver.yml.tt
+++ b/railties/lib/rails/generators/rails/app/templates/config/databases/sqlserver.yml.tt
@@ -26,7 +26,7 @@ test:
<<: *default
database: <%= app_name %>_test
-# As with config/secrets.yml, you never want to store sensitive information,
+# As with config/credentials.yml, you never want to store sensitive information,
# like your database password, in your source code. If your source code is
# ever seen by anyone, they now have access to your database.
#
diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_6_0.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_6_0.rb.tt
index a3aca27500..d25552e923 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_6_0.rb.tt
+++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_6_0.rb.tt
@@ -25,8 +25,8 @@
# Use ActionMailer::MailDeliveryJob for sending parameterized and normal mail.
#
-# The default delivery job (ActionMailer::DeliveryJob), will be removed in Rails 6.1.
-# This setting is not backwards compatible with earlier Rails versions.
+# The default delivery jobs (ActionMailer::Parameterized::DeliveryJob, ActionMailer::DeliveryJob),
+# will be removed in Rails 6.1. This setting is not backwards compatible with earlier Rails versions.
# If you send mail in the background, job workers need to have a copy of
# MailDeliveryJob to ensure all delivery jobs are processed properly.
# Make sure your entire app is migrated and stable on 6.0 before using this setting.
diff --git a/railties/lib/rails/generators/rails/app/templates/package.json.tt b/railties/lib/rails/generators/rails/app/templates/package.json.tt
index 7174116989..07207e1747 100644
--- a/railties/lib/rails/generators/rails/app/templates/package.json.tt
+++ b/railties/lib/rails/generators/rails/app/templates/package.json.tt
@@ -2,10 +2,10 @@
"name": "<%= app_name %>",
"private": true,
"dependencies": {
- "rails-ujs": ">=5.2.1"<% unless options[:skip_turbolinks] %>,
- "turbolinks": "5.1.1"<% end -%><% unless skip_active_storage? %>,
- "activestorage": ">=5.2.1"<% end -%><% unless options[:skip_action_cable] %>,
- "actioncable": ">=5.2.1"<% end %>
+ "@rails/ujs": "^6.0.0-alpha"<% unless options[:skip_turbolinks] %>,
+ "turbolinks": "^5.2.0"<% end -%><% unless skip_active_storage? %>,
+ "@rails/activestorage": "^6.0.0-alpha"<% end -%><% unless options[:skip_action_cable] %>,
+ "@rails/actioncable": "^6.0.0-alpha"<% end %>
},
"version": "0.1.0"
}
diff --git a/railties/lib/rails/generators/rails/app/templates/test/channels/application_cable/connection_test.rb.tt b/railties/lib/rails/generators/rails/app/templates/test/channels/application_cable/connection_test.rb.tt
new file mode 100644
index 0000000000..800405f15e
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/test/channels/application_cable/connection_test.rb.tt
@@ -0,0 +1,11 @@
+require "test_helper"
+
+class ApplicationCable::ConnectionTest < ActionCable::Connection::TestCase
+ # test "connects with cookies" do
+ # cookies.signed[:user_id] = 42
+ #
+ # connect
+ #
+ # assert_equal connection.user_id, "42"
+ # end
+end
diff --git a/railties/lib/rails/test_unit/testing.rake b/railties/lib/rails/test_unit/testing.rake
index ecc458b21e..3a1b62d9d1 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", "mailboxes"].each do |name|
+ ["models", "helpers", "channels", "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/credentials_test.rb b/railties/test/application/credentials_test.rb
new file mode 100644
index 0000000000..2f6b109b50
--- /dev/null
+++ b/railties/test/application/credentials_test.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+require "isolation/abstract_unit"
+require "env_helpers"
+
+class Rails::CredentialsTest < ActiveSupport::TestCase
+ include ActiveSupport::Testing::Isolation, EnvHelpers
+
+ setup :build_app
+ teardown :teardown_app
+
+ test "reads credentials from environment specific path" do
+ write_credentials_override(:production)
+
+ app("production")
+
+ assert_equal "revealed", Rails.application.credentials.mystery
+ end
+
+ test "reads credentials from customized path and key" do
+ write_credentials_override(:staging)
+ add_to_env_config("production", "config.credentials.content_path = config.root.join('config/credentials/staging.yml.enc')")
+ add_to_env_config("production", "config.credentials.key_path = config.root.join('config/credentials/staging.key')")
+
+ app("production")
+
+ assert_equal "revealed", Rails.application.credentials.mystery
+ end
+
+ test "reads credentials using environment variable key" do
+ write_credentials_override(:production, with_key: false)
+
+ switch_env("RAILS_MASTER_KEY", credentials_key) do
+ app("production")
+
+ assert_equal "revealed", Rails.application.credentials.mystery
+ end
+ end
+
+ private
+ def write_credentials_override(name, with_key: true)
+ Dir.chdir(app_path) do
+ Dir.mkdir "config/credentials"
+ File.write "config/credentials/#{name}.key", credentials_key if with_key
+
+ # secret_key_base: secret
+ # mystery: revealed
+ File.write "config/credentials/#{name}.yml.enc",
+ "vgvKu4MBepIgZ5VHQMMPwnQNsLlWD9LKmJHu3UA/8yj6x+3fNhz3DwL9brX7UA==--qLdxHP6e34xeTAiI--nrcAsleXuo9NqiEuhntAhw=="
+ end
+ end
+
+ def credentials_key
+ "2117e775dc2024d4f49ddf3aeb585919"
+ end
+end
diff --git a/railties/test/application/multiple_applications_test.rb b/railties/test/application/multiple_applications_test.rb
index d6c81c1fe2..432344bccc 100644
--- a/railties/test/application/multiple_applications_test.rb
+++ b/railties/test/application/multiple_applications_test.rb
@@ -165,12 +165,12 @@ module ApplicationTests
app.config.some_setting = "a_different_setting"
assert_equal "a_different_setting", app.config.some_setting, "The configuration's some_setting should be set."
- new_config = Rails::Application::Configuration.new("root_of_application")
+ new_config = Rails::Application::Configuration.new(Pathname.new("root_of_application"))
new_config.some_setting = "some_setting_dude"
app.config = new_config
assert_equal "some_setting_dude", app.config.some_setting, "The configuration's some_setting should have changed."
- assert_equal "root_of_application", app.config.root, "The root should have changed to the new config's root."
+ assert_equal "root_of_application", app.config.root.to_s, "The root should have changed to the new config's root."
assert_equal new_config, app.config, "The application's config should have changed to the new config."
end
end
diff --git a/railties/test/application/rake/routes_test.rb b/railties/test/application/rake/routes_test.rb
index 933c735078..9879d1f047 100644
--- a/railties/test/application/rake/routes_test.rb
+++ b/railties/test/application/rake/routes_test.rb
@@ -22,7 +22,8 @@ module ApplicationTests
cart GET /cart(.:format) cart#show
rails_amazon_inbound_emails POST /rails/action_mailbox/amazon/inbound_emails(.:format) action_mailbox/ingresses/amazon/inbound_emails#create
rails_mandrill_inbound_emails POST /rails/action_mailbox/mandrill/inbound_emails(.:format) action_mailbox/ingresses/mandrill/inbound_emails#create
- rails_postfix_inbound_emails POST /rails/action_mailbox/postfix/inbound_emails(.:format) action_mailbox/ingresses/postfix/inbound_emails#create
+ rails_postmark_inbound_emails POST /rails/action_mailbox/postmark/inbound_emails(.:format) action_mailbox/ingresses/postmark/inbound_emails#create
+ rails_relay_inbound_emails POST /rails/action_mailbox/relay/inbound_emails(.:format) action_mailbox/ingresses/relay/inbound_emails#create
rails_sendgrid_inbound_emails POST /rails/action_mailbox/sendgrid/inbound_emails(.:format) action_mailbox/ingresses/sendgrid/inbound_emails#create
rails_mailgun_inbound_emails POST /rails/action_mailbox/mailgun/inbound_emails/mime(.:format) action_mailbox/ingresses/mailgun/inbound_emails#create
rails_conductor_inbound_emails GET /rails/conductor/action_mailbox/inbound_emails(.:format) rails/conductor/action_mailbox/inbound_emails#index
diff --git a/railties/test/application/test_runner_test.rb b/railties/test/application/test_runner_test.rb
index 6765eef9d0..fda6df500d 100644
--- a/railties/test/application/test_runner_test.rb
+++ b/railties/test/application/test_runner_test.rb
@@ -98,6 +98,17 @@ module ApplicationTests
end
end
+ def test_run_channels
+ create_test_file :channels, "foo_channel"
+ create_test_file :channels, "bar_channel"
+
+ rails("test:channels").tap do |output|
+ assert_match "FooChannelTest", output
+ assert_match "BarChannelTest", output
+ assert_match "2 runs, 2 assertions, 0 failures", output
+ end
+ end
+
def test_run_controllers
create_test_file :controllers, "foo_controller"
create_test_file :controllers, "bar_controller"
@@ -167,11 +178,11 @@ module ApplicationTests
end
def test_run_all_suites
- suites = [:models, :helpers, :unit, :controllers, :mailers, :functional, :integration, :jobs, :mailboxes]
+ suites = [:models, :helpers, :unit, :channels, :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 "9 runs, 9 assertions, 0 failures", output
+ assert_match "10 runs, 10 assertions, 0 failures", output
end
end
diff --git a/railties/test/commands/routes_test.rb b/railties/test/commands/routes_test.rb
index a43a6d32b9..b4f927060e 100644
--- a/railties/test/commands/routes_test.rb
+++ b/railties/test/commands/routes_test.rb
@@ -18,15 +18,15 @@ class Rails::Command::RoutesTest < ActiveSupport::TestCase
RUBY
assert_equal <<~OUTPUT, run_routes_command([ "-c", "PostController" ])
- Prefix Verb URI Pattern Controller#Action
- new_post GET /post/new(.:format) posts#new
- edit_post GET /post/edit(.:format) posts#edit
- post GET /post(.:format) posts#show
- PATCH /post(.:format) posts#update
- PUT /post(.:format) posts#update
- DELETE /post(.:format) posts#destroy
- POST /post(.:format) posts#create
- rails_postfix_inbound_emails POST /rails/action_mailbox/postfix/inbound_emails(.:format) action_mailbox/ingresses/postfix/inbound_emails#create
+ Prefix Verb URI Pattern Controller#Action
+ new_post GET /post/new(.:format) posts#new
+ edit_post GET /post/edit(.:format) posts#edit
+ post GET /post(.:format) posts#show
+ PATCH /post(.:format) posts#update
+ PUT /post(.:format) posts#update
+ DELETE /post(.:format) posts#destroy
+ POST /post(.:format) posts#create
+ rails_postmark_inbound_emails POST /rails/action_mailbox/postmark/inbound_emails(.:format) action_mailbox/ingresses/postmark/inbound_emails#create
OUTPUT
assert_equal <<~OUTPUT, run_routes_command([ "-c", "UserPermissionController" ])
@@ -64,7 +64,8 @@ class Rails::Command::RoutesTest < ActiveSupport::TestCase
POST /cart(.:format) cart#create
rails_amazon_inbound_emails POST /rails/action_mailbox/amazon/inbound_emails(.:format) action_mailbox/ingresses/amazon/inbound_emails#create
rails_mandrill_inbound_emails POST /rails/action_mailbox/mandrill/inbound_emails(.:format) action_mailbox/ingresses/mandrill/inbound_emails#create
- rails_postfix_inbound_emails POST /rails/action_mailbox/postfix/inbound_emails(.:format) action_mailbox/ingresses/postfix/inbound_emails#create
+ rails_postmark_inbound_emails POST /rails/action_mailbox/postmark/inbound_emails(.:format) action_mailbox/ingresses/postmark/inbound_emails#create
+ rails_relay_inbound_emails POST /rails/action_mailbox/relay/inbound_emails(.:format) action_mailbox/ingresses/relay/inbound_emails#create
rails_sendgrid_inbound_emails POST /rails/action_mailbox/sendgrid/inbound_emails(.:format) action_mailbox/ingresses/sendgrid/inbound_emails#create
rails_mailgun_inbound_emails POST /rails/action_mailbox/mailgun/inbound_emails/mime(.:format) action_mailbox/ingresses/mailgun/inbound_emails#create
POST /rails/conductor/action_mailbox/inbound_emails(.:format) rails/conductor/action_mailbox/inbound_emails#create
@@ -131,15 +132,15 @@ class Rails::Command::RoutesTest < ActiveSupport::TestCase
OUTPUT
assert_equal <<~OUTPUT, run_routes_command([ "-c", "PostController" ])
- Prefix Verb URI Pattern Controller#Action
- new_admin_post GET /admin/post/new(.:format) admin/posts#new
- edit_admin_post GET /admin/post/edit(.:format) admin/posts#edit
- admin_post GET /admin/post(.:format) admin/posts#show
- PATCH /admin/post(.:format) admin/posts#update
- PUT /admin/post(.:format) admin/posts#update
- DELETE /admin/post(.:format) admin/posts#destroy
- POST /admin/post(.:format) admin/posts#create
- rails_postfix_inbound_emails POST /rails/action_mailbox/postfix/inbound_emails(.:format) action_mailbox/ingresses/postfix/inbound_emails#create
+ Prefix Verb URI Pattern Controller#Action
+ new_admin_post GET /admin/post/new(.:format) admin/posts#new
+ edit_admin_post GET /admin/post/edit(.:format) admin/posts#edit
+ admin_post GET /admin/post(.:format) admin/posts#show
+ PATCH /admin/post(.:format) admin/posts#update
+ PUT /admin/post(.:format) admin/posts#update
+ DELETE /admin/post(.:format) admin/posts#destroy
+ POST /admin/post(.:format) admin/posts#create
+ rails_postmark_inbound_emails POST /rails/action_mailbox/postmark/inbound_emails(.:format) action_mailbox/ingresses/postmark/inbound_emails#create
OUTPUT
expected_permission_output = <<~OUTPUT
@@ -167,7 +168,8 @@ class Rails::Command::RoutesTest < ActiveSupport::TestCase
Prefix Verb URI Pattern Controller#Action
rails_amazon_inbound_emails POST /rails/action_mailbox/amazon/inbound_emails(.:format) action_mailbox/ingresses/amazon/inbound_emails#create
rails_mandrill_inbound_emails POST /rails/action_mailbox/mandrill/inbound_emails(.:format) action_mailbox/ingresses/mandrill/inbound_emails#create
- rails_postfix_inbound_emails POST /rails/action_mailbox/postfix/inbound_emails(.:format) action_mailbox/ingresses/postfix/inbound_emails#create
+ rails_postmark_inbound_emails POST /rails/action_mailbox/postmark/inbound_emails(.:format) action_mailbox/ingresses/postmark/inbound_emails#create
+ rails_relay_inbound_emails POST /rails/action_mailbox/relay/inbound_emails(.:format) action_mailbox/ingresses/relay/inbound_emails#create
rails_sendgrid_inbound_emails POST /rails/action_mailbox/sendgrid/inbound_emails(.:format) action_mailbox/ingresses/sendgrid/inbound_emails#create
rails_mailgun_inbound_emails POST /rails/action_mailbox/mailgun/inbound_emails/mime(.:format) action_mailbox/ingresses/mailgun/inbound_emails#create
rails_conductor_inbound_emails GET /rails/conductor/action_mailbox/inbound_emails(.:format) rails/conductor/action_mailbox/inbound_emails#index
@@ -215,86 +217,91 @@ class Rails::Command::RoutesTest < ActiveSupport::TestCase
URI | /rails/action_mailbox/mandrill/inbound_emails(.:format)
Controller#Action | action_mailbox/ingresses/mandrill/inbound_emails#create
--[ Route 4 ]--------------
- Prefix | rails_postfix_inbound_emails
+ Prefix | rails_postmark_inbound_emails
Verb | POST
- URI | /rails/action_mailbox/postfix/inbound_emails(.:format)
- Controller#Action | action_mailbox/ingresses/postfix/inbound_emails#create
+ URI | /rails/action_mailbox/postmark/inbound_emails(.:format)
+ Controller#Action | action_mailbox/ingresses/postmark/inbound_emails#create
--[ Route 5 ]--------------
+ Prefix | rails_relay_inbound_emails
+ Verb | POST
+ URI | /rails/action_mailbox/relay/inbound_emails(.:format)
+ Controller#Action | action_mailbox/ingresses/relay/inbound_emails#create
+ --[ Route 6 ]--------------
Prefix | rails_sendgrid_inbound_emails
Verb | POST
URI | /rails/action_mailbox/sendgrid/inbound_emails(.:format)
Controller#Action | action_mailbox/ingresses/sendgrid/inbound_emails#create
- --[ Route 6 ]--------------
+ --[ Route 7 ]--------------
Prefix | rails_mailgun_inbound_emails
Verb | POST
URI | /rails/action_mailbox/mailgun/inbound_emails/mime(.:format)
Controller#Action | action_mailbox/ingresses/mailgun/inbound_emails#create
- --[ Route 7 ]--------------
+ --[ Route 8 ]--------------
Prefix | rails_conductor_inbound_emails
Verb | GET
URI | /rails/conductor/action_mailbox/inbound_emails(.:format)
Controller#Action | rails/conductor/action_mailbox/inbound_emails#index
- --[ Route 8 ]--------------
+ --[ Route 9 ]--------------
Prefix |
Verb | POST
URI | /rails/conductor/action_mailbox/inbound_emails(.:format)
Controller#Action | rails/conductor/action_mailbox/inbound_emails#create
- --[ Route 9 ]--------------
+ --[ Route 10 ]-------------
Prefix | new_rails_conductor_inbound_email
Verb | GET
URI | /rails/conductor/action_mailbox/inbound_emails/new(.:format)
Controller#Action | rails/conductor/action_mailbox/inbound_emails#new
- --[ Route 10 ]-------------
+ --[ Route 11 ]-------------
Prefix | edit_rails_conductor_inbound_email
Verb | GET
URI | /rails/conductor/action_mailbox/inbound_emails/:id/edit(.:format)
Controller#Action | rails/conductor/action_mailbox/inbound_emails#edit
- --[ Route 11 ]-------------
+ --[ Route 12 ]-------------
Prefix | rails_conductor_inbound_email
Verb | GET
URI | /rails/conductor/action_mailbox/inbound_emails/:id(.:format)
Controller#Action | rails/conductor/action_mailbox/inbound_emails#show
- --[ Route 12 ]-------------
+ --[ Route 13 ]-------------
Prefix |
Verb | PATCH
URI | /rails/conductor/action_mailbox/inbound_emails/:id(.:format)
Controller#Action | rails/conductor/action_mailbox/inbound_emails#update
- --[ Route 13 ]-------------
+ --[ Route 14 ]-------------
Prefix |
Verb | PUT
URI | /rails/conductor/action_mailbox/inbound_emails/:id(.:format)
Controller#Action | rails/conductor/action_mailbox/inbound_emails#update
- --[ Route 14 ]-------------
+ --[ Route 15 ]-------------
Prefix |
Verb | DELETE
URI | /rails/conductor/action_mailbox/inbound_emails/:id(.:format)
Controller#Action | rails/conductor/action_mailbox/inbound_emails#destroy
- --[ Route 15 ]-------------
+ --[ Route 16 ]-------------
Prefix | rails_conductor_inbound_email_reroute
Verb | POST
URI | /rails/conductor/action_mailbox/:inbound_email_id/reroute(.:format)
Controller#Action | rails/conductor/action_mailbox/reroutes#create
- --[ Route 16 ]-------------
+ --[ Route 17 ]-------------
Prefix | rails_service_blob
Verb | GET
URI | /rails/active_storage/blobs/:signed_id/*filename(.:format)
Controller#Action | active_storage/blobs#show
- --[ Route 17 ]-------------
+ --[ Route 18 ]-------------
Prefix | rails_blob_representation
Verb | GET
URI | /rails/active_storage/representations/:signed_blob_id/:variation_key/*filename(.:format)
Controller#Action | active_storage/representations#show
- --[ Route 18 ]-------------
+ --[ Route 19 ]-------------
Prefix | rails_disk_service
Verb | GET
URI | /rails/active_storage/disk/:encoded_key/*filename(.:format)
Controller#Action | active_storage/disk#show
- --[ Route 19 ]-------------
+ --[ Route 20 ]-------------
Prefix | update_rails_disk_service
Verb | PUT
URI | /rails/active_storage/disk/:encoded_token(.:format)
Controller#Action | active_storage/disk#update
- --[ Route 20 ]-------------
+ --[ Route 21 ]-------------
Prefix | rails_direct_uploads
Verb | POST
URI | /rails/active_storage/direct_uploads(.:format)
diff --git a/railties/test/credentials_test.rb b/railties/test/credentials_test.rb
deleted file mode 100644
index 11765b0de5..0000000000
--- a/railties/test/credentials_test.rb
+++ /dev/null
@@ -1,65 +0,0 @@
-# frozen_string_literal: true
-
-require "isolation/abstract_unit"
-require "env_helpers"
-
-class Rails::CredentialsTest < ActiveSupport::TestCase
- include ActiveSupport::Testing::Isolation, EnvHelpers
-
- setup :build_app
- teardown :teardown_app
-
- test "reads credentials from environment specific path" do
- with_credentials do |content, key|
- Dir.chdir(app_path) do
- Dir.mkdir("config/credentials")
- File.write("config/credentials/production.yml.enc", content)
- File.write("config/credentials/production.key", key)
- end
-
- app("production")
-
- assert_equal "revealed", Rails.application.credentials.mystery
- end
- end
-
- test "reads credentials from customized path and key" do
- with_credentials do |content, key|
- Dir.chdir(app_path) do
- Dir.mkdir("config/credentials")
- File.write("config/credentials/staging.yml.enc", content)
- File.write("config/credentials/staging.key", key)
- end
-
- add_to_env_config("production", "config.credentials.content_path = config.root.join('config/credentials/staging.yml.enc')")
- add_to_env_config("production", "config.credentials.key_path = config.root.join('config/credentials/staging.key')")
- app("production")
-
- assert_equal "revealed", Rails.application.credentials.mystery
- end
- end
-
- test "reads credentials using environment variable key" do
- with_credentials do |content, key|
- Dir.chdir(app_path) do
- Dir.mkdir("config/credentials")
- File.write("config/credentials/production.yml.enc", content)
- end
-
- switch_env("RAILS_MASTER_KEY", key) do
- app("production")
-
- assert_equal "revealed", Rails.application.credentials.mystery
- end
- end
- end
-
- private
- def with_credentials
- key = "2117e775dc2024d4f49ddf3aeb585919"
- # secret_key_base: secret
- # mystery: revealed
- content = "vgvKu4MBepIgZ5VHQMMPwnQNsLlWD9LKmJHu3UA/8yj6x+3fNhz3DwL9brX7UA==--qLdxHP6e34xeTAiI--nrcAsleXuo9NqiEuhntAhw=="
- yield(content, key)
- end
-end
diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb
index 839e6feb39..e0cd7f90ce 100644
--- a/railties/test/generators/app_generator_test.rb
+++ b/railties/test/generators/app_generator_test.rb
@@ -81,6 +81,7 @@ DEFAULT_APP_FILES = %w(
test/test_helper.rb
test/fixtures
test/fixtures/files
+ test/channels/application_cable/connection_test.rb
test/controllers
test/models
test/helpers
@@ -363,6 +364,8 @@ class AppGeneratorTest < Rails::Generators::TestCase
assert_file "#{app_root}/config/environments/production.rb" do |content|
assert_no_match(/config\.action_cable/, content)
end
+
+ assert_no_file "#{app_root}/test/channels/application_cable/connection_test.rb"
end
def test_app_update_does_not_generate_bootsnap_contents_when_skip_bootsnap_is_given
diff --git a/railties/test/generators/channel_generator_test.rb b/railties/test/generators/channel_generator_test.rb
index 1cb8465539..1a25422c3c 100644
--- a/railties/test/generators/channel_generator_test.rb
+++ b/railties/test/generators/channel_generator_test.rb
@@ -67,12 +67,23 @@ class ChannelGeneratorTest < Rails::Generators::TestCase
assert_file "app/javascript/channels/consumer.js"
end
+ def test_invokes_default_test_framework
+ run_generator %w(chat -t=test_unit)
+
+ assert_file "test/channels/chat_channel_test.rb" do |test|
+ assert_match(/class ChatChannelTest < ActionCable::Channel::TestCase/, test)
+ assert_match(/# test "subscribes" do/, test)
+ assert_match(/# assert subscription.confirmed\?/, test)
+ end
+ end
+
def test_channel_on_revoke
run_generator ["chat"]
run_generator ["chat"], behavior: :revoke
assert_no_file "app/channels/chat_channel.rb"
assert_no_file "app/javascript/channels/chat_channel.js"
+ assert_no_file "test/channels/chat_channel_test.rb"
assert_file "app/channels/application_cable/channel.rb"
assert_file "app/channels/application_cable/connection.rb"
@@ -88,5 +99,8 @@ class ChannelGeneratorTest < Rails::Generators::TestCase
assert_no_file "app/javascript/channels/chat_channel_channel.js"
assert_file "app/javascript/channels/chat_channel.js"
+
+ assert_no_file "test/channels/chat_channel_channel_test.rb"
+ assert_file "test/channels/chat_channel_test.rb"
end
end
diff --git a/railties/test/generators/shared_generator_tests.rb b/railties/test/generators/shared_generator_tests.rb
index 4dfcb35ad9..f673832caa 100644
--- a/railties/test/generators/shared_generator_tests.rb
+++ b/railties/test/generators/shared_generator_tests.rb
@@ -206,7 +206,7 @@ module SharedGeneratorTests
unless generator_class.name == "Rails::Generators::PluginGenerator"
assert_file "#{application_path}/app/javascript/packs/application.js" do |content|
- assert_match(/^import \* as ActiveStorage from "activestorage"\nActiveStorage.start\(\)/, content)
+ assert_match(/^import \* as ActiveStorage from "@rails\/activestorage"\nActiveStorage.start\(\)/, content)
end
end
@@ -267,7 +267,7 @@ module SharedGeneratorTests
assert_file "#{application_path}/config/application.rb", /#\s+require\s+["']active_storage\/engine["']/
assert_file "#{application_path}/app/javascript/packs/application.js" do |content|
- assert_no_match(/^import * as ActiveStorage from "activestorage"\nActiveStorage.start\(\)/, content)
+ assert_no_match(/^import * as ActiveStorage from "@rails\/activestorage"\nActiveStorage.start\(\)/, content)
end
assert_file "#{application_path}/config/environments/development.rb" do |content|
diff --git a/tasks/release.rb b/tasks/release.rb
index 600e716a8d..214c1fd16e 100644
--- a/tasks/release.rb
+++ b/tasks/release.rb
@@ -173,15 +173,56 @@ namespace :all do
end
task verify: :install do
- app_name = "pkg/verify-#{version}-#{Time.now.to_i}"
+ require "tmpdir"
+
+ cd Dir.tmpdir
+ app_name = "verify-#{version}-#{Time.now.to_i}"
sh "rails _#{version}_ new #{app_name} --skip-bundle" # Generate with the right version.
cd app_name
+ substitute = -> (file_name, regex, replacement) do
+ File.write(file_name, File.read(file_name).sub(regex, replacement))
+ end
+
# Replace the generated gemfile entry with the exact version.
- File.write("Gemfile", File.read("Gemfile").sub(/^gem 'rails.*/, "gem 'rails', '#{version}'"))
+ substitute.call("Gemfile", /^gem 'rails.*/, "gem 'rails', '#{version}'")
+ substitute.call("Gemfile", /^# gem 'image_processing/, "gem 'image_processing")
sh "bundle"
+ sh "rails action_mailbox:install"
+ sh "rails action_text:install"
+
+ sh "rails generate scaffold user name description:text admin:boolean"
+ sh "rails db:migrate"
+
+ # Replace the generated gemfile entry with the exact version.
+ substitute.call("app/models/user.rb", /end\n\z/, <<~CODE)
+ has_one_attached :avatar
+ has_rich_text :description
+ end
+ CODE
- sh "rails generate scaffold user name admin:boolean && rails db:migrate"
+ substitute.call("app/views/users/_form.html.erb", /text_area :description %>\n <\/div>/, <<~CODE)
+ rich_text_area :description %>\n </div>
+
+ <div class="field">
+ Avatar: <%= form.file_field :avatar %>
+ </div>
+ CODE
+
+ substitute.call("app/views/users/show.html.erb", /description %>\n<\/p>/, <<~CODE)
+ description %>\n</p>
+
+ <p>
+ <%= image_tag @user.avatar.representation(resize_to_fit: [500, 500]) %>
+ </p>
+ CODE
+
+ # Permit the avatar param.
+ substitute.call("app/controllers/users_controller.rb", /:admin/, ":admin, :avatar")
+
+ if ENV["EDITOR"]
+ `#{ENV["EDITOR"]} #{File.expand_path(app_name)}`
+ end
puts "Booting a Rails server. Verify the release by:"
puts
diff --git a/yarn.lock b/yarn.lock
index 02d1463723..fdf508b9a3 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5692,6 +5692,11 @@ trim-right@^1.0.1:
resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003"
integrity sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=
+trix@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/trix/-/trix-1.0.0.tgz#e9cc98cf6030c908f8d54e317b5b072f927b0c6b"
+ integrity sha512-feli9QVXe6gzZOCUfpPGpNDURW9jMciIRVQ5gkDudOctcA1oMtI5K/qEbsL2rFCoGl1rSoeRt+HPhIFGyQscKg==
+
tslib@^1.9.0:
version "1.9.3"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286"