diff options
Diffstat (limited to 'guides')
-rw-r--r-- | guides/source/6_0_release_notes.md | 8 | ||||
-rw-r--r-- | guides/source/action_cable_overview.md | 51 | ||||
-rw-r--r-- | guides/source/action_mailbox_basics.md | 72 | ||||
-rw-r--r-- | guides/source/testing.md | 114 |
4 files changed, 228 insertions, 17 deletions
diff --git a/guides/source/6_0_release_notes.md b/guides/source/6_0_release_notes.md index 6f1db126c3..14528d1cde 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 -------- diff --git a/guides/source/action_cable_overview.md b/guides/source/action_cable_overview.md index 77a1b73bae..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 --------------- @@ -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 c5ec921ad5..c90892d456 100644 --- a/guides/source/action_mailbox_basics.md +++ b/guides/source/action_mailbox_basics.md @@ -21,7 +21,7 @@ Introduction 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 Postfix ingress. +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,8 @@ 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 @@ -191,6 +221,36 @@ https://actionmailbox:PASSWORD@example.com/rails/action_mailbox/postmark/inbound 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 Tell Action Mailbox to accept emails from SendGrid: diff --git a/guides/source/testing.md b/guides/source/testing.md index f34f9d95f4..576c4d768c 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,114 @@ 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 and connection classes themsleves 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 gets 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 another 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 ---------------------------- |