├── .gem_release.yml
├── .github
└── workflows
│ └── build.yml
├── .gitignore
├── .rubocop.yml
├── CHANGELOG.md
├── Gemfile
├── LICENSE.txt
├── README.md
├── Rakefile
├── action-cable-testing.gemspec
├── cucumber.yml
├── features
├── .nav
├── Generators.md
├── Minitest.md
├── README.md
├── channel_specs
│ └── channel_spec.feature
├── matchers
│ ├── have_broadcasted_matcher.feature
│ └── have_stream_from_matcher.feature
├── shared_contexts
│ └── shared_contexts.feature
├── step_definitions
│ └── additional_cli_steps.rb
└── support
│ └── env.rb
├── gemfiles
├── rails50.gemfile
├── rails5001.gemfile
├── rails51.gemfile
├── rails52.gemfile
├── rspec4rails5.gemfile
└── rspec4rails6.gemfile
├── lib
├── action-cable-testing.rb
├── action_cable
│ ├── subscription_adapter
│ │ └── test.rb
│ ├── testing.rb
│ └── testing
│ │ ├── channel
│ │ └── test_case.rb
│ │ ├── connection
│ │ └── test_case.rb
│ │ ├── rails_six.rb
│ │ ├── rspec.rb
│ │ ├── rspec
│ │ └── features.rb
│ │ ├── test_case.rb
│ │ ├── test_helper.rb
│ │ └── version.rb
├── generators
│ ├── rspec
│ │ └── channel
│ │ │ ├── channel_generator.rb
│ │ │ └── templates
│ │ │ └── channel_spec.rb.erb
│ └── test_unit
│ │ └── channel
│ │ ├── channel_generator.rb
│ │ └── templates
│ │ └── unit_test.rb.erb
└── rspec
│ └── rails
│ ├── example
│ └── channel_example_group.rb
│ ├── matchers
│ ├── action_cable.rb
│ └── action_cable
│ │ ├── have_broadcasted_to.rb
│ │ └── have_streams.rb
│ └── shared_contexts
│ └── action_cable.rb
├── spec
├── dummy
│ ├── .rspec
│ ├── app
│ │ ├── channels
│ │ │ ├── application_cable
│ │ │ │ ├── channel.rb
│ │ │ │ └── connection.rb
│ │ │ ├── chat_channel.rb
│ │ │ ├── echo_channel.rb
│ │ │ └── user_channel.rb
│ │ └── models
│ │ │ ├── broadcaster.rb
│ │ │ └── user.rb
│ ├── bin
│ │ └── rails
│ ├── config.ru
│ ├── config
│ │ ├── application.rb
│ │ ├── boot.rb
│ │ ├── cable.yml
│ │ ├── environment.rb
│ │ ├── puma.rb
│ │ ├── routes.rb
│ │ └── secrets.yml
│ └── spec
│ │ ├── rails_helper.rb
│ │ └── spec_helper.rb
├── generators
│ ├── rspec_spec.rb
│ └── test_unit_spec.rb
├── rspec
│ └── rails
│ │ ├── channel_example_group_spec.rb
│ │ └── matchers
│ │ └── action_cable
│ │ ├── have_broadcasted_to_spec.rb
│ │ └── have_stream_spec.rb
├── spec_helper.rb
└── support
│ ├── deprecated_api.rb
│ ├── helpers.rb
│ └── shared_examples.rb
└── test
├── channel
└── test_case_test.rb
├── connection
└── test_case_test.rb
├── stubs
├── global_id.rb
├── test_adapter.rb
├── test_server.rb
└── user.rb
├── syntax_test.rb
├── test_helper.rb
└── test_helper_test.rb
/.gem_release.yml:
--------------------------------------------------------------------------------
1 | bump:
2 | file: lib/action_cable/testing/version.rb
3 | skip_ci: true
4 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | pull_request:
8 |
9 | jobs:
10 | rake:
11 | runs-on: ubuntu-latest
12 | env:
13 | BUNDLE_JOBS: 4
14 | BUNDLE_RETRY: 3
15 | BUNDLE_FORCE_RUBY_PLATFORM: 1
16 | CI: true
17 | strategy:
18 | fail-fast: false
19 | matrix:
20 | ruby: ["2.7"]
21 | gemfile: ["gemfiles/rails52.gemfile"]
22 | bundler: ["2"]
23 | include:
24 | - ruby: "2.5"
25 | gemfile: "gemfiles/rails5001.gemfile"
26 | bundler: "1"
27 | - ruby: "2.6"
28 | gemfile: "gemfiles/rails51.gemfile"
29 | bundler: "2"
30 | - ruby: "2.6"
31 | gemfile: "gemfiles/rspec4rails5.gemfile"
32 | bundler: "2"
33 | - ruby: "2.7"
34 | gemfile: "gemfiles/rspec4rails6.gemfile"
35 | bundler: "2"
36 | steps:
37 | - uses: actions/checkout@v2
38 | - uses: actions/cache@v1
39 | with:
40 | path: /home/runner/bundle
41 | key: bundle-${{ matrix.ruby }}-${{ matrix.gemfile }}-${{ hashFiles('**/*.gemspec') }}-${{ hashFiles('**/Gemfile') }}
42 | restore-keys: |
43 | bundle-${{ matrix.ruby }}-${{ matrix.gemfile }}-
44 | - uses: ruby/setup-ruby@v1
45 | with:
46 | ruby-version: ${{ matrix.ruby }}
47 | bundler: ${{ matrix.bundler }}
48 | - name: Bundle install
49 | run: |
50 | bundle config path /home/runner/bundle
51 | bundle config --global gemfile ${{ matrix.gemfile }}
52 | bundle install
53 | bundle update
54 | - name: Run RSpec
55 | run: |
56 | bundle exec rake
57 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.bundle/
2 | /.yardoc
3 | /Gemfile.lock
4 | /_yardoc/
5 | /coverage/
6 | /doc/
7 | /pkg/
8 | /spec/reports/
9 | /tmp/
10 | .byebug_history
11 |
--------------------------------------------------------------------------------
/.rubocop.yml:
--------------------------------------------------------------------------------
1 | AllCops:
2 | TargetRubyVersion: 2.4
3 | # RuboCop has a bunch of cops enabled by default. This setting tells RuboCop
4 | # to ignore them, so only the ones explicitly set in this file are enabled.
5 | DisabledByDefault: true
6 | Exclude:
7 | - '**/templates/**/*'
8 | - '**/vendor/**/*'
9 | - 'gemfiles/**/*'
10 | - '**/*.gemfile'
11 | - 'actionpack/lib/action_dispatch/journey/parser.rb'
12 | - 'tmp/**/*'
13 | - 'spec/dummy/**/*'
14 | - 'spec/support/**/*'
15 | - 'spec/rspec/**/*'
16 | - 'features/**/*'
17 |
18 | # Prefer &&/|| over and/or.
19 | Style/AndOr:
20 | Enabled: true
21 |
22 | # Do not use braces for hash literals when they are the last argument of a
23 | # method call.
24 | Style/BracesAroundHashParameters:
25 | Enabled: true
26 | EnforcedStyle: context_dependent
27 |
28 | # Align `when` with `case`.
29 | Layout/CaseIndentation:
30 | Enabled: true
31 |
32 | # Align comments with method definitions.
33 | Layout/CommentIndentation:
34 | Enabled: true
35 |
36 | Layout/EmptyLineAfterMagicComment:
37 | Enabled: true
38 |
39 | # In a regular class definition, no empty lines around the body.
40 | Layout/EmptyLinesAroundClassBody:
41 | Enabled: true
42 |
43 | # In a regular method definition, no empty lines around the body.
44 | Layout/EmptyLinesAroundMethodBody:
45 | Enabled: true
46 |
47 | # In a regular module definition, no empty lines around the body.
48 | Layout/EmptyLinesAroundModuleBody:
49 | Enabled: true
50 |
51 | Layout/IndentFirstArgument:
52 | Enabled: true
53 |
54 | # Use Ruby >= 1.9 syntax for hashes. Prefer { a: :b } over { :a => :b }.
55 | Style/HashSyntax:
56 | Enabled: true
57 |
58 | # Method definitions after `private` or `protected` isolated calls need one
59 | # extra level of indentation.
60 | Layout/IndentationConsistency:
61 | Enabled: true
62 | EnforcedStyle: rails
63 |
64 | # Two spaces, no tabs (for indentation).
65 | Layout/IndentationWidth:
66 | Enabled: true
67 |
68 | Layout/SpaceAfterColon:
69 | Enabled: true
70 |
71 | Layout/SpaceAfterComma:
72 | Enabled: true
73 |
74 | Layout/SpaceAroundEqualsInParameterDefault:
75 | Enabled: true
76 |
77 | Layout/SpaceAroundKeyword:
78 | Enabled: true
79 |
80 | Layout/SpaceAroundOperators:
81 | Enabled: true
82 |
83 | Layout/SpaceBeforeFirstArg:
84 | Enabled: true
85 |
86 | # Defining a method with parameters needs parentheses.
87 | Style/MethodDefParentheses:
88 | Enabled: true
89 |
90 | Style/FrozenStringLiteralComment:
91 | Enabled: true
92 | EnforcedStyle: always
93 | Exclude:
94 | - 'actionview/test/**/*.builder'
95 | - 'actionview/test/**/*.ruby'
96 | - 'actionpack/test/**/*.builder'
97 | - 'actionpack/test/**/*.ruby'
98 | - 'activestorage/db/migrate/**/*.rb'
99 |
100 | # Use `foo {}` not `foo{}`.
101 | Layout/SpaceBeforeBlockBraces:
102 | EnforcedStyleForEmptyBraces: space
103 | Enabled: true
104 |
105 | # Use `foo { bar }` not `foo {bar}`.
106 | Layout/SpaceInsideBlockBraces:
107 | Enabled: true
108 |
109 | # Use `{ a: 1 }` not `{a:1}`.
110 | Layout/SpaceInsideHashLiteralBraces:
111 | Enabled: true
112 |
113 | Layout/SpaceInsideParens:
114 | Enabled: true
115 |
116 | # Check quotes usage according to lint rule below.
117 | Style/StringLiterals:
118 | Enabled: true
119 | EnforcedStyle: double_quotes
120 |
121 | # Detect hard tabs, no hard tabs.
122 | Layout/Tab:
123 | Enabled: true
124 |
125 | # Blank lines should not have any spaces.
126 | Layout/TrailingBlankLines:
127 | Enabled: true
128 |
129 | # No trailing whitespace.
130 | Layout/TrailingWhitespace:
131 | Enabled: true
132 |
133 | # Use quotes for string literals when they are enough.
134 | Style/UnneededPercentQ:
135 | Enabled: true
136 |
137 | # Align `end` with the matching keyword or starting expression except for
138 | # assignments, where it should be aligned with the LHS.
139 | Layout/EndAlignment:
140 | Enabled: true
141 | EnforcedStyleAlignWith: variable
142 |
143 | # Use my_method(my_arg) not my_method( my_arg ) or my_method my_arg.
144 | Lint/RequireParentheses:
145 | Enabled: true
146 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change log
2 |
3 | ## 0.6.1 (2020-03-02)
4 |
5 | - Fix Ruby 2.7 warnings. ([@palkan][])
6 |
7 | ## 0.6.0 (2019-08-19)
8 |
9 | - **Ruby 2.4+** is required.
10 |
11 | - Fix Rails 6 compatibility. ([@palkan][])
12 |
13 | ## 0.5.0 (2019-02-24)
14 |
15 | - Make compatible with Rails 6. ([@palkan][])
16 |
17 | That allows using RSpec part of the gem with Rails 6
18 | (since Action Cable testing will included only in the upcoming RSpec 4).
19 |
20 | - Backport Rails 6.0 API changes. ([@palkan][], [@sponomarev][])
21 |
22 | Some APIs have been deprecated (to be removed in 1.0).
23 |
24 | ## 0.4.0 (2019-01-10)
25 |
26 | - Add stream assert methods and matchers. ([@sponomarev][])
27 |
28 | See https://github.com/palkan/action-cable-testing/pull/42
29 |
30 | - Add session support for connection. ([@sponomarev][])
31 |
32 | See https://github.com/palkan/action-cable-testing/pull/35
33 |
34 | ## 0.3.0
35 |
36 | - Add connection unit-testing utilities. ([@palkan][])
37 |
38 | See https://github.com/palkan/action-cable-testing/pull/6
39 |
40 | ## 0.2.0
41 |
42 | - Update minitest's `assert_broadcast_on` and `assert_broadcasts` matchers to support a record as an argument. ([@thesmartnik][])
43 |
44 | See https://github.com/palkan/action-cable-testing/issues/11
45 |
46 | - Update `have_broadcasted_to` matcher to support a record as an argument. ([@thesmartnik][])
47 |
48 | See https://github.com/palkan/action-cable-testing/issues/9
49 |
50 | ## 0.1.2 (2017-11-14)
51 |
52 | - Add RSpec shared contexts to switch between adapters. ([@palkan][])
53 |
54 | See https://github.com/palkan/action-cable-testing/issues/4.
55 |
56 | ## 0.1.1
57 |
58 | - Support Rails 5.0.0.1. ([@palkan][])
59 |
60 | ## 0.1.0
61 |
62 | - Initial version. ([@palkan][])
63 |
64 | [@palkan]: https://github.com/palkan
65 | [@thesmartnik]: https://github.com/thesmartnik
66 | [@sponomarev]: https://github.com/sponomarev
67 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | source "https://rubygems.org"
4 |
5 | # Specify your gem's dependencies in action-cable-testing.gemspec
6 | gemspec
7 |
8 | gem "rails", "~> 6.0"
9 |
10 | gem "pry-byebug"
11 |
12 | local_gemfile = "Gemfile.local"
13 |
14 | if File.exist?(local_gemfile)
15 | eval(File.read(local_gemfile)) # rubocop:disable Security/Eval
16 | end
17 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2017 Vladimir Dementyev
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://rubygems.org/gems/action-cable-testing)
2 | 
3 | [](https://travis-ci.org/palkan/action-cable-testing)[](http://www.rubydoc.info/gems/action-cable-testing)
4 |
5 | # Action Cable Testing
6 |
7 | This gem provides missing testing utils for [Action Cable][].
8 |
9 | **NOTE:** this gem [has](https://github.com/rails/rails/pull/33659) [been](https://github.com/rails/rails/pull/33969) [merged](https://github.com/rails/rails/pull/34845) into Rails 6.0 and [into RSpec 4](https://github.com/rspec/rspec-rails/pull/2113).
10 |
11 | If you're using Minitest – you don't need this gem anymore.
12 |
13 | If you're using RSpec < 4, you still can use this gem to write Action Cable specs even for Rails 6.
14 |
15 | ## Installation
16 | # For Rails < 6.0 ONLY:
17 |
18 | Add this line to your application's Gemfile:
19 |
20 | ```ruby
21 | gem 'action-cable-testing'
22 | ```
23 |
24 | And then execute:
25 |
26 | $ bundle
27 |
28 | # For Usage with Rspec (any version of Rails, including 6+):
29 | add to `spec/rails_helper.rb`
30 | ```
31 | RSpec.configure do |config|
32 | //more rspec configs...
33 | config.include ActionCable::TestHelper
34 | end
35 | ```
36 |
37 | (note in older versions of Rails you will make Rspec config changes in `spec_helper.rb`)
38 |
39 | ## Usage
40 |
41 | ### Test Adapter and Broadcasting
42 |
43 | We add `ActionCable::SubscriptionAdapter::Test` (very similar Active Job and Action Mailer tests adapters) and `ActionCable::TestCase` with a couple of matchers to track broadcasting messages in our tests:
44 |
45 | ```ruby
46 | # Using ActionCable::TestCase
47 | class MyCableTest < ActionCable::TestCase
48 | def test_broadcasts
49 | # Check the number of messages broadcasted to the stream
50 | assert_broadcasts 'messages', 0
51 | ActionCable.server.broadcast 'messages', { text: 'hello' }
52 | assert_broadcasts 'messages', 1
53 |
54 | # Check the number of messages broadcasted to the stream within a block
55 | assert_broadcasts('messages', 1) do
56 | ActionCable.server.broadcast 'messages', { text: 'hello' }
57 | end
58 |
59 | # Check that no broadcasts has been made
60 | assert_no_broadcasts('messages') do
61 | ActionCable.server.broadcast 'another_stream', { text: 'hello' }
62 | end
63 | end
64 | end
65 |
66 | # Or including ActionCable::TestHelper
67 | class ExampleTest < ActionDispatch::IntegrationTest
68 | include ActionCable::TestHelper
69 |
70 | def test_broadcasts
71 | room = rooms(:office)
72 |
73 | assert_broadcast_on("messages:#{room.id}", text: 'Hello!') do
74 | post "/say/#{room.id}", xhr: true, params: { message: 'Hello!' }
75 | end
76 | end
77 | end
78 | ```
79 |
80 | If you want to test the broadcasting made with `Channel.broadcast_to`, you should use
81 | `Channel.broadcasting_for`\* to generate an underlying stream name and **use Rails 6 compatibility refinement**:
82 |
83 | ```ruby
84 | # app/jobs/chat_relay_job.rb
85 | class ChatRelayJob < ApplicationJob
86 | def perform_later(room, message)
87 | ChatChannel.broadcast_to room, text: message
88 | end
89 | end
90 |
91 |
92 | # test/jobs/chat_relay_job_test.rb
93 | require "test_helper"
94 |
95 | # Activate Rails 6 compatible API (for `broadcasting_for`)
96 | using ActionCable::Testing::Rails6
97 |
98 | class ChatRelayJobTest < ActiveJob::TestCase
99 | include ActionCable::TestHelper
100 |
101 | test "broadcast message to room" do
102 | room = rooms(:all)
103 |
104 | assert_broadcast_on(ChatChannel.broadcasting_for(room), text: "Hi!") do
105 | ChatRelayJob.perform_now(room, "Hi!")
106 | end
107 | end
108 | end
109 | ```
110 |
111 | \* **NOTE:** in Rails 6.0 you should use `.broadcasting_for`, but it's not backward compatible
112 | and we cannot use it in Rails 5.x. See https://github.com/rails/rails/pull/35021.
113 | Note also, that this feature hasn't been released in Rails 6.0.0.beta1, so you still need the refinement.
114 |
115 | ### Channels Testing
116 |
117 | Channels tests are written as follows:
118 | 1. First, one uses the `subscribe` method to simulate subscription creation.
119 | 2. Then, one asserts whether the current state is as expected. "State" can be anything:
120 | transmitted messages, subscribed streams, etc.
121 |
122 | For example:
123 |
124 | ```ruby
125 | class ChatChannelTest < ActionCable::Channel::TestCase
126 | def test_subscribed_with_room_number
127 | # Simulate a subscription creation
128 | subscribe room_number: 1
129 |
130 | # Asserts that the subscription was successfully created
131 | assert subscription.confirmed?
132 |
133 | # Asserts that the channel subscribes connection to a stream
134 | assert_has_stream "chat_1"
135 |
136 | # Asserts that the channel subscribes connection to a stream created with `stream_for`
137 | assert_has_stream_for Room.find(1)
138 | end
139 |
140 | def test_subscribed_without_room_number
141 | subscribe
142 |
143 | assert subscription.confirmed?
144 | # Asserts that no streams was started
145 | # (e.g., we want to subscribe later by performing an action)
146 | assert_no_streams
147 | end
148 |
149 | def test_does_not_subscribe_with_invalid_room_number
150 | subscribe room_number: -1
151 |
152 | # Asserts that the subscription was rejected
153 | assert subscription.rejected?
154 | end
155 | end
156 | ```
157 |
158 | You can also perform actions:
159 |
160 | ```ruby
161 | def test_perform_speak
162 | subscribe room_number: 1
163 |
164 | perform :speak, message: "Hello, Rails!"
165 |
166 | # `transmissions` stores messages sent directly to the channel (i.e. with `transmit` method)
167 | assert_equal "Hello, Rails!", transmissions.last["text"]
168 | end
169 | ```
170 |
171 | You can set up your connection identifiers:
172 |
173 | ```ruby
174 | class ChatChannelTest < ActionCable::Channel::TestCase
175 | include ActionCable::TestHelper
176 |
177 | def test_identifiers
178 | stub_connection(user: users[:john])
179 |
180 | subscribe room_number: 1
181 |
182 | assert_broadcast_on("messages_1", text: "I'm here!", from: "John") do
183 | perform :speak, message: "I'm here!"
184 | end
185 | end
186 | end
187 | ```
188 | When broadcasting to an object:
189 |
190 | ```ruby
191 | class ChatChannelTest < ActionCable::Channel::TestCase
192 | def setup
193 | @room = Room.find 1
194 |
195 | stub_connection(user: users[:john])
196 | subscribe room_number: room.id
197 | end
198 |
199 | def test_broadcasting
200 | assert_broadcasts(@room, 1) do
201 | perform :speak, message: "I'm here!"
202 | end
203 | end
204 |
205 | # or
206 |
207 | def test_broadcasted_data
208 | assert_broadcast_on(@room, text: "I'm here!", from: "John") do
209 | perform :speak, message: "I'm here!"
210 | end
211 | end
212 | end
213 | ```
214 |
215 | ### Connection Testing
216 |
217 | Connection unit tests are written as follows:
218 | 1. First, one uses the `connect` method to simulate connection.
219 | 2. Then, one asserts whether the current state is as expected (e.g. identifiers).
220 |
221 | For example:
222 |
223 | ```ruby
224 | module ApplicationCable
225 | class ConnectionTest < ActionCable::Connection::TestCase
226 | def test_connects_with_cookies
227 | cookies.signed[:user_id] = users[:john].id
228 |
229 | # Simulate a connection
230 | connect
231 |
232 | # Asserts that the connection identifier is correct
233 | assert_equal "John", connection.user.name
234 | end
235 |
236 | def test_does_not_connect_without_user
237 | assert_reject_connection do
238 | connect
239 | end
240 | end
241 | end
242 | end
243 | ```
244 |
245 | You can also provide additional information about underlying HTTP request:
246 |
247 | ```ruby
248 | def test_connect_with_headers_and_query_string
249 | connect "/cable?user_id=1", headers: { "X-API-TOKEN" => 'secret-my' }
250 |
251 | assert_equal connection.user_id, "1"
252 | end
253 |
254 | def test_connect_with_session
255 | connect "/cable", session: { users[:john].id }
256 |
257 | assert_equal connection.user_id, "1"
258 | end
259 | ```
260 |
261 | ### RSpec Usage
262 |
263 | First, you need to have [rspec-rails](https://github.com/rspec/rspec-rails) installed.
264 |
265 | Second, add this to your `"rails_helper.rb"` after requiring `environment.rb`:
266 |
267 | ```ruby
268 | require "action_cable/testing/rspec"
269 | ```
270 |
271 | To use `have_broadcasted_to` / `broadcast_to` matchers anywhere in your specs, set your adapter to `test` in `cable.yml`:
272 |
273 | ```yml
274 | # config/cable.yml
275 | test:
276 | adapter: test
277 | ```
278 |
279 | And then use these matchers, for example:
280 |
281 |
282 | ```ruby
283 | RSpec.describe CommentsController do
284 | describe "POST #create" do
285 | expect { post :create, comment: { text: 'Cool!' } }.to
286 | have_broadcasted_to("comments").with(text: 'Cool!')
287 | end
288 | end
289 | ```
290 |
291 | Or when broadcasting to an object:
292 |
293 | ```ruby
294 | RSpec.describe CommentsController do
295 | describe "POST #create" do
296 | let(:the_post) { create :post }
297 |
298 | expect { post :create, comment: { text: 'Cool!', post_id: the_post.id } }.to
299 | have_broadcasted_to(the_post).from_channel(PostChannel).with(text: 'Cool!')
300 | end
301 | end
302 | ```
303 |
304 | You can also unit-test your channels:
305 |
306 |
307 | ```ruby
308 | # spec/channels/chat_channel_spec.rb
309 |
310 | require "rails_helper"
311 |
312 | RSpec.describe ChatChannel, type: :channel do
313 | before do
314 | # initialize connection with identifiers
315 | stub_connection user_id: user.id
316 | end
317 |
318 | it "subscribes without streams when no room id" do
319 | subscribe
320 |
321 | expect(subscription).to be_confirmed
322 | expect(subscription).not_to have_streams
323 | end
324 |
325 | it "rejects when room id is invalid" do
326 | subscribe(room_id: -1)
327 |
328 | expect(subscription).to be_rejected
329 | end
330 |
331 | it "subscribes to a stream when room id is provided" do
332 | subscribe(room_id: 42)
333 |
334 | expect(subscription).to be_confirmed
335 |
336 | # check particular stream by name
337 | expect(subscription).to have_stream_from("chat_42")
338 |
339 | # or directly by model if you create streams with `stream_for`
340 | expect(subscription).to have_stream_for(Room.find(42))
341 | end
342 | end
343 | ```
344 |
345 | And, of course, connections:
346 |
347 | ```ruby
348 | require "rails_helper"
349 |
350 | RSpec.describe ApplicationCable::Connection, type: :channel do
351 | it "successfully connects" do
352 | connect "/cable", headers: { "X-USER-ID" => "325" }
353 | expect(connection.user_id).to eq "325"
354 | end
355 |
356 | it "rejects connection" do
357 | expect { connect "/cable" }.to have_rejected_connection
358 | end
359 | end
360 | ```
361 |
362 | **NOTE:** for connections testing you must use `type: :channel` too.
363 |
364 | #### Shared contexts to switch between adapters
365 |
366 | **NOTE:** this feature is gem-only and hasn't been migrated to RSpec 4. You can still use the gem for that by adding `require "rspec/rails/shared_contexts/action_cable"` to your `rspec_helper.rb`.
367 |
368 | Sometimes you may want to use _real_ Action Cable adapter instead of the test one (for example, in Capybara-like tests).
369 |
370 | We provide shared contexts to do that:
371 |
372 | ```ruby
373 | # Use async adapter for this example group only
374 | RSpec.describe "cable case", action_cable: :async do
375 | # ...
376 |
377 | context "inline cable", action_cable: :inline do
378 | # ...
379 | end
380 |
381 | # or test adapter
382 | context "test cable", action_cable: :test do
383 | # ...
384 | end
385 |
386 | # you can also include contexts by names
387 | context "by name" do
388 | include "action_cable:async"
389 | # ...
390 | end
391 | end
392 | ```
393 |
394 | We also provide an integration for _feature_ specs (having `type: :feature`). Just add `require "action_cable/testing/rspec/features"`:
395 |
396 | ```ruby
397 | # rails_helper.rb
398 | require "action_cable/testing/rspec"
399 | require "action_cable/testing/rspec/features"
400 |
401 | # spec/features/my_feature_spec.rb
402 | feature "Cables!" do
403 | # here we have "action_cable:async" context included automatically!
404 | end
405 | ```
406 |
407 | For more RSpec documentation see https://relishapp.com/palkan/action-cable-testing/docs.
408 |
409 | ### Generators
410 |
411 | This gem also provides Rails generators:
412 |
413 | ```sh
414 | # Generate a channel test case for ChatChannel
415 | rails generate test_unit:channel chat
416 |
417 | # or for RSpec
418 | rails generate rspec:channel chat
419 | ```
420 |
421 | ## Development
422 |
423 | After checking out the repo, run `bundle install` to install dependencies. Then, run `bundle exec rake` to run the tests.
424 |
425 | ## Contributing
426 |
427 | Bug reports and pull requests are welcome on GitHub at https://github.com/palkan/action-cable-testing.
428 |
429 | ## License
430 |
431 | The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
432 |
433 | [Action Cable]: http://guides.rubyonrails.org/action_cable_overview.html
434 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "bundler/gem_tasks"
4 | require "rspec/core/rake_task"
5 | require "rubocop/rake_task"
6 | require "rake/testtask"
7 | require "cucumber/rake/task"
8 |
9 | Rake::TestTask.new do |t|
10 | t.test_files = FileList["test/**/*_test.rb"]
11 | end
12 |
13 | RuboCop::RakeTask.new
14 | RSpec::Core::RakeTask.new(:spec)
15 | Cucumber::Rake::Task.new(:cucumber)
16 |
17 | task default: [:rubocop, :spec, :test, :cucumber]
18 |
--------------------------------------------------------------------------------
/action-cable-testing.gemspec:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | lib = File.expand_path("../lib", __FILE__)
4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5 |
6 | require "action_cable/testing/version"
7 |
8 | Gem::Specification.new do |spec|
9 | spec.name = "action-cable-testing"
10 | spec.version = ActionCable::Testing::VERSION
11 |
12 | spec.authors = ["Vladimir Dementyev"]
13 | spec.email = ["dementiev.vm@gmail.com"]
14 |
15 | spec.summary = "Testing utils for Action Cable"
16 | spec.description = "Testing utils for Action Cable"
17 | spec.homepage = "http://github.com/palkan/action-cable-testing"
18 | spec.license = "MIT"
19 |
20 | spec.files = `git ls-files`.split($/).select { |p| p.match(%r{^lib/}) } +
21 | %w(README.md CHANGELOG.md LICENSE.txt)
22 |
23 | spec.require_paths = ["lib"]
24 |
25 | spec.required_ruby_version = ">= 2.4.0"
26 |
27 | spec.add_dependency "actioncable", ">= 5.0"
28 |
29 | spec.add_development_dependency "bundler", ">= 1.10"
30 | spec.add_development_dependency "cucumber", "~> 3.1.1"
31 | spec.add_development_dependency "rake", "~> 10.0"
32 | spec.add_development_dependency "rspec-rails", "~> 3.5"
33 | spec.add_development_dependency "aruba", "~> 0.14.6"
34 | spec.add_development_dependency "minitest", "~> 5.9"
35 | spec.add_development_dependency "ammeter", "~> 1.1"
36 | spec.add_development_dependency "rubocop", "~> 0.68.0"
37 | end
38 |
--------------------------------------------------------------------------------
/cucumber.yml:
--------------------------------------------------------------------------------
1 | default: --require features --format progress --tags "not @wip"
2 |
--------------------------------------------------------------------------------
/features/.nav:
--------------------------------------------------------------------------------
1 | - README.md (Getting Started)
2 | - Minitest.md (Minitest Usage)
3 | - Generators.md
4 | - channel_specs:
5 | - channel_spec.feature
6 |
7 |
--------------------------------------------------------------------------------
/features/Generators.md:
--------------------------------------------------------------------------------
1 | This gem provides RSpec generators for channels specs. For example:
2 |
3 | rails generate rspec:channel chat
4 |
5 | will create a new spec file in `spec/channels/chat_channel_spec.rb`.
6 |
--------------------------------------------------------------------------------
/features/Minitest.md:
--------------------------------------------------------------------------------
1 | [Full Documentation](http://www.rubydoc.info/gems/action-cable-testing)
2 |
3 | ### Test Adapter and Broadcasting
4 |
5 | We add `ActionCable::SubscriptionAdapter::Test` (very similar Active Job and Action Mailer tests adapters) and `ActionCable::TestCase` with a couple of matchers to track broadcasting messages in our tests:
6 |
7 | ```ruby
8 | # Using ActionCable::TestCase
9 | class MyCableTest < ActionCable::TestCase
10 | def test_broadcasts
11 | # Check the number of messages broadcasted to the stream
12 | assert_broadcasts 'messages', 0
13 | ActionCable.server.broadcast 'messages', { text: 'hello' }
14 | assert_broadcasts 'messages', 1
15 |
16 | # Check the number of messages broadcasted to the stream within a block
17 | assert_broadcasts('messages', 1) do
18 | ActionCable.server.broadcast 'messages', { text: 'hello' }
19 | end
20 |
21 | # Check that no broadcasts has been made
22 | assert_no_broadcasts('messages') do
23 | ActionCable.server.broadcast 'another_stream', { text: 'hello' }
24 | end
25 | end
26 | end
27 |
28 | # Or including ActionCable::TestHelper
29 | class ExampleTest < ActionDispatch::IntegrationTest
30 | include ActionCable::TestHelper
31 |
32 | def test_broadcasts
33 | room = rooms(:office)
34 |
35 | assert_broadcast_on("messages:#{room.id}", text: 'Hello!') do
36 | post "/say/#{room.id}", xhr: true, params: { message: 'Hello!' }
37 | end
38 | end
39 | end
40 | ```
41 |
42 | If you want to test the broadcasting made with `Channel.broadcast_to`, you should use
43 | `Channel.broadcasting_for`\* to generate an underlying stream name and **use Rails 6 compatibility refinement**:
44 |
45 | ```ruby
46 | # app/jobs/chat_relay_job.rb
47 | class ChatRelayJob < ApplicationJob
48 | def perform_later(room, message)
49 | ChatChannel.broadcast_to room, text: message
50 | end
51 | end
52 |
53 |
54 | # test/jobs/chat_relay_job_test.rb
55 | require "test_helper"
56 |
57 | # Activate Rails 6 compatible API (for `broadcasting_for`)
58 | using ActionCable::Testing::Rails6
59 |
60 | class ChatRelayJobTest < ActiveJob::TestCase
61 | include ActionCable::TestHelper
62 |
63 | test "broadcast message to room" do
64 | room = rooms(:all)
65 |
66 | assert_broadcast_on(ChatChannel.broadcasting_for(room), text: "Hi!") do
67 | ChatRelayJob.perform_now(room, "Hi!")
68 | end
69 | end
70 | end
71 | ```
72 |
73 | \* **NOTE:** in Rails 6.0 you should use `.broadcasting_for`, but it's not backward compatible
74 | and we cannot use it in Rails 5.x. See https://github.com/rails/rails/pull/35021.
75 |
76 |
77 | ### Channels Testing
78 |
79 | Channels tests are written as follows:
80 | 1. First, one uses the `subscribe` method to simulate subscription creation.
81 | 2. Then, one asserts whether the current state is as expected. "State" can be anything:
82 | transmitted messages, subscribed streams, etc.
83 |
84 | For example:
85 |
86 | ```ruby
87 | class ChatChannelTest < ActionCable::Channel::TestCase
88 | def test_subscribed_with_room_number
89 | # Simulate a subscription creation
90 | subscribe room_number: 1
91 |
92 | # Asserts that the subscription was successfully created
93 | assert subscription.confirmed?
94 |
95 | # Asserts that the channel subscribes connection to a stream
96 | assert_has_stream "chat_1"
97 |
98 | # Asserts that the channel subscribes connection to a specific
99 | # stream created for a model
100 | assert_has_stream_for Room.find(1)
101 | end
102 |
103 | def test_does_not_stream_with_incorrect_room_number
104 | subscribe room_number: -1
105 |
106 | # Asserts that not streams was started
107 | assert_no_streams
108 | end
109 |
110 | def test_does_not_subscribe_without_room_number
111 | subscribe
112 |
113 | # Asserts that the subscription was rejected
114 | assert subscription.rejected?
115 | end
116 | end
117 | ```
118 |
119 | You can also perform actions:
120 |
121 | ```ruby
122 | def test_perform_speak
123 | subscribe room_number: 1
124 |
125 | perform :speak, message: "Hello, Rails!"
126 |
127 | # `transmissions` stores messages sent directly to the channel (i.e. with `transmit` method)
128 | assert_equal "Hello, Rails!", transmissions.last["text"]
129 | end
130 | ```
131 |
132 | You can set up your connection identifiers:
133 |
134 | ```ruby
135 | class ChatChannelTest < ActionCable::Channel::TestCase
136 | include ActionCable::TestHelper
137 |
138 | def test_identifiers
139 | stub_connection(user: users[:john])
140 |
141 | subscribe room_number: 1
142 |
143 | assert_broadcast_on("messages_1", text: "I'm here!", from: "John") do
144 | perform :speak, message: "I'm here!"
145 | end
146 | end
147 | end
148 | ```
149 | When broadcasting to an object:
150 |
151 | ```ruby
152 | class ChatChannelTest < ActionCable::Channel::TestCase
153 | include ActionCable::TestHelper
154 |
155 | def setup
156 | @room = Room.find 1
157 |
158 | stub_connection(user: users[:john])
159 | subscribe room_number: room.id
160 | end
161 |
162 | def test_broadcating
163 | assert_broadcasts(@room, 1) do
164 | perform :speak, message: "I'm here!"
165 | end
166 | end
167 |
168 | # or
169 |
170 | def test_broadcasted_data
171 | assert_broadcast_on(@room, text: "I'm here!", from: "John") do
172 | perform :speak, message: "I'm here!"
173 | end
174 | end
175 | end
176 | ```
177 |
178 | ### Generators
179 |
180 | This gem also provides Rails generators:
181 |
182 | ```sh
183 | # Generate a channel test case for ChatChannel
184 | rails generate test_unit:channel chat
185 | ```
186 |
--------------------------------------------------------------------------------
/features/README.md:
--------------------------------------------------------------------------------
1 | This gem provides missing testing utils for [Action Cable][].
2 |
3 | **NOTE:** this gem is just a combination of two PRs to Rails itself ([#23211](https://github.com/rails/rails/pull/23211) and [#27191](https://github.com/rails/rails/pull/27191)) and (hopefully) will be merged into Rails eventually.
4 |
5 | **NOTE 2:** this is the documentation for (mostly) RSpec part of the gem. For Minitest usage see the repo's [Readme](https://github.com/palkan/action-cable-testing) or [Minitest Usage](minitest) chapter.
6 |
7 | ## Installation
8 |
9 | Add this line to your application's Gemfile:
10 |
11 | ```ruby
12 | group :test, :development do
13 | gem 'action-cable-testing'
14 | end
15 | ```
16 |
17 | And then execute:
18 |
19 | $ bundle
20 |
21 | ## Basic Usage
22 |
23 | First, you need to have [rspec-rails](https://github.com/rspec/rspec-rails) installed.
24 |
25 | Second, add this to your `"rails_helper.rb"` after requiring `environment.rb`:
26 |
27 | ```ruby
28 | require "action_cable/testing/rspec"
29 | ```
30 |
31 | To use `have_broadcasted_to` / `broadcast_to` matchers anywhere in your specs, set your adapter to `test` in `cable.yml`:
32 |
33 | ```yml
34 | # config/cable.yml
35 | test:
36 | adapter: test
37 | ```
38 |
39 | And then use these matchers, for example:
40 |
41 | ```ruby
42 | RSpec.describe CommentsController do
43 | describe "POST #create" do
44 | expect { post :create, comment: { text: 'Cool!' } }.to
45 | have_broadcasted_to("comments").with(text: 'Cool!')
46 | end
47 | end
48 | ```
49 |
50 | You can also unit-test your channels:
51 |
52 |
53 | ```ruby
54 | # spec/channels/chat_channel_spec.rb
55 |
56 | require "rails_helper"
57 |
58 | RSpec.describe ChatChannel, type: :channel do
59 | before do
60 | # initialize connection with identifiers
61 | stub_connection user_id: user.id
62 | end
63 |
64 | it "rejects when no room id" do
65 | subscribe
66 | expect(subscription).to be_rejected
67 | end
68 |
69 | it "subscribes to a stream when room id is provided" do
70 | subscribe(room_id: 42)
71 |
72 | expect(subscription).to be_confirmed
73 | expect(streams).to include("chat_42")
74 | end
75 | end
76 | ```
77 |
78 | ## Issues
79 |
80 | Bug reports and pull requests are welcome on GitHub at https://github.com/palkan/action-cable-testing.
81 |
82 | [Action Cable]: http://guides.rubyonrails.org/action_cable_overview.html
83 |
--------------------------------------------------------------------------------
/features/channel_specs/channel_spec.feature:
--------------------------------------------------------------------------------
1 | Feature: channel spec
2 |
3 | Channel specs are marked by `:type => :channel` or if you have set
4 | `config.infer_spec_type_from_file_location!` by placing them in `spec/channels`.
5 |
6 | A channel spec is a thin wrapper for an ActionCable::Channel::TestCase, and includes all
7 | of the behavior and assertions that it provides, in addition to RSpec's own
8 | behavior and expectations.
9 |
10 | It also includes helpers from ActionCable::Connection::TestCase to make it possible to
11 | test connection behavior.
12 |
13 | Background:
14 | Given action cable is available
15 |
16 | Scenario: simple passing example
17 | Given a file named "spec/channels/echo_channel_spec.rb" with:
18 | """ruby
19 | require "rails_helper"
20 |
21 | RSpec.describe EchoChannel, :type => :channel do
22 | it "successfully subscribes" do
23 | subscribe
24 | expect(subscription).to be_confirmed
25 | end
26 | end
27 | """
28 | When I run `rspec spec/channels/echo_channel_spec.rb`
29 | Then the example should pass
30 |
31 | Scenario: verifying that subscription is rejected
32 | Given a file named "spec/channels/chat_channel_spec.rb" with:
33 | """ruby
34 | require "rails_helper"
35 |
36 | RSpec.describe ChatChannel, :type => :channel do
37 | it "rejects subscription" do
38 | stub_connection user_id: nil
39 | subscribe
40 | expect(subscription).to be_rejected
41 | end
42 | end
43 | """
44 | When I run `rspec spec/channels/chat_channel_spec.rb`
45 | Then the example should pass
46 |
47 | Scenario: specifying connection identifiers
48 | Given a file named "spec/channels/chat_channel_spec.rb" with:
49 | """ruby
50 | require "rails_helper"
51 |
52 | RSpec.describe ChatChannel, :type => :channel do
53 | it "successfully subscribes" do
54 | stub_connection user_id: 42
55 | subscribe
56 | expect(subscription).to be_confirmed
57 | expect(subscription.user_id).to eq 42
58 | end
59 | end
60 | """
61 | When I run `rspec spec/channels/chat_channel_spec.rb`
62 | Then the example should pass
63 |
64 |
65 |
66 | Scenario: performing actions and checking transmissions
67 | Given a file named "spec/channels/echo_channel_spec.rb" with:
68 | """ruby
69 | require "rails_helper"
70 |
71 | RSpec.describe EchoChannel, :type => :channel do
72 | it "successfully subscribes" do
73 | subscribe
74 |
75 | perform :echo, foo: 'bar'
76 | expect(transmissions.last).to eq('foo' => 'bar')
77 | end
78 | end
79 | """
80 | When I run `rspec spec/channels/echo_channel_spec.rb`
81 | Then the example should pass
82 |
83 | Scenario: successful connection with url params
84 | Given a file named "spec/channels/connection_spec.rb" with:
85 | """ruby
86 | require "rails_helper"
87 |
88 | RSpec.describe ApplicationCable::Connection, :type => :channel do
89 | it "successfully connects" do
90 | connect "/cable?user_id=323"
91 | expect(connection.user_id).to eq "323"
92 | end
93 | end
94 | """
95 | When I run `rspec spec/channels/connection_spec.rb`
96 | Then the example should pass
97 |
98 | Scenario: successful connection with cookies
99 | Given a file named "spec/channels/connection_spec.rb" with:
100 | """ruby
101 | require "rails_helper"
102 |
103 | RSpec.describe ApplicationCable::Connection, :type => :channel do
104 | it "successfully connects" do
105 | cookies.signed[:user_id] = "324"
106 |
107 | connect "/cable"
108 | expect(connection.user_id).to eq "324"
109 | end
110 | end
111 | """
112 | When I run `rspec spec/channels/connection_spec.rb`
113 | Then the example should pass
114 |
115 | Scenario: successful connection with session
116 | Given a file named "spec/channels/connection_spec.rb" with:
117 | """ruby
118 | require "rails_helper"
119 |
120 | RSpec.describe ApplicationCable::Connection, :type => :channel do
121 | it "successfully connects" do
122 | connect "/cable", session: { user_id: "324" }
123 | expect(connection.user_id).to eq "324"
124 | end
125 | end
126 | """
127 | When I run `rspec spec/channels/connection_spec.rb`
128 | Then the example should pass
129 |
130 | Scenario: successful connection with headers
131 | Given a file named "spec/channels/connection_spec.rb" with:
132 | """ruby
133 | require "rails_helper"
134 |
135 | RSpec.describe ApplicationCable::Connection, :type => :channel do
136 | it "successfully connects" do
137 | connect "/cable", headers: { "X-USER-ID" => "325" }
138 | expect(connection.user_id).to eq "325"
139 | end
140 | end
141 | """
142 | When I run `rspec spec/channels/connection_spec.rb`
143 | Then the example should pass
144 |
145 | Scenario: rejected connection
146 | Given a file named "spec/channels/connection_spec.rb" with:
147 | """ruby
148 | require "rails_helper"
149 |
150 | RSpec.describe ApplicationCable::Connection, :type => :channel do
151 | it "rejects connection" do
152 | expect { connect "/cable" }.to have_rejected_connection
153 | end
154 | end
155 | """
156 | When I run `rspec spec/channels/connection_spec.rb`
157 | Then the example should pass
158 |
159 | Scenario: disconnect connection
160 | Given a file named "spec/channels/connection_spec.rb" with:
161 | """ruby
162 | require "rails_helper"
163 |
164 | RSpec.describe ApplicationCable::Connection, :type => :channel do
165 | it "disconnects" do
166 | connect "/cable?user_id=42"
167 | expect { disconnect }.to output(/User 42 disconnected/).to_stdout
168 | end
169 | end
170 | """
171 | When I run `rspec spec/channels/connection_spec.rb`
172 | Then the example should pass
173 |
--------------------------------------------------------------------------------
/features/matchers/have_broadcasted_matcher.feature:
--------------------------------------------------------------------------------
1 | Feature: have_broadcasted matcher
2 |
3 | The `have_broadcasted_to` (also aliased as `broadcast_to`) matcher is used to check if a message has been broadcasted to a given stream.
4 |
5 | Background:
6 | Given action cable is available
7 |
8 | Scenario: Checking stream name
9 | Given a file named "spec/models/broadcaster_spec.rb" with:
10 | """ruby
11 | require "rails_helper"
12 |
13 | RSpec.describe Broadcaster do
14 | it "matches with stream name" do
15 | expect {
16 | ActionCable.server.broadcast(
17 | "notifications", text: 'Hello!'
18 | )
19 | }.to have_broadcasted_to("notifications")
20 | end
21 | end
22 | """
23 | When I run `rspec spec/models/broadcaster_spec.rb`
24 | Then the examples should all pass
25 |
26 | Scenario: Checking passed message to stream
27 | Given a file named "spec/models/broadcaster_spec.rb" with:
28 | """ruby
29 | require "rails_helper"
30 |
31 | RSpec.describe Broadcaster do
32 | it "matches with message" do
33 | expect {
34 | ActionCable.server.broadcast(
35 | "notifications", text: 'Hello!'
36 | )
37 | }.to have_broadcasted_to("notifications").with(text: 'Hello!')
38 | end
39 | end
40 | """
41 | When I run `rspec spec/models/broadcaster_spec.rb`
42 | Then the examples should all pass
43 |
44 |
45 | Scenario: Checking that message passed to stream matches
46 | Given a file named "spec/models/broadcaster_spec.rb" with:
47 | """ruby
48 | require "rails_helper"
49 |
50 | RSpec.describe Broadcaster do
51 | it "matches with message" do
52 | expect {
53 | ActionCable.server.broadcast(
54 | "notifications", text: 'Hello!', user_id: 12
55 | )
56 | }.to have_broadcasted_to("notifications").with(a_hash_including(text: 'Hello!'))
57 | end
58 | end
59 | """
60 | When I run `rspec spec/models/broadcaster_spec.rb`
61 | Then the examples should all pass
62 |
63 | Scenario: Checking passed message with block
64 | Given a file named "spec/models/broadcaster_spec.rb" with:
65 | """ruby
66 | require "rails_helper"
67 |
68 | RSpec.describe Broadcaster do
69 | it "matches with message" do
70 | expect {
71 | ActionCable.server.broadcast(
72 | "notifications", text: 'Hello!', user_id: 12
73 | )
74 | }.to have_broadcasted_to("notifications").with { |data|
75 | expect(data['user_id']).to eq 12
76 | }
77 | end
78 | end
79 | """
80 | When I run `rspec spec/models/broadcaster_spec.rb`
81 | Then the examples should all pass
82 |
83 | Scenario: Using alias method
84 | Given a file named "spec/models/broadcaster_spec.rb" with:
85 | """ruby
86 | require "rails_helper"
87 |
88 | RSpec.describe Broadcaster do
89 | it "matches with stream name" do
90 | expect {
91 | ActionCable.server.broadcast(
92 | "notifications", text: 'Hello!'
93 | )
94 | }.to broadcast_to("notifications")
95 | end
96 | end
97 | """
98 | When I run `rspec spec/models/broadcaster_spec.rb`
99 | Then the examples should all pass
100 |
101 | Scenario: Checking broadcast to a record
102 | Given a file named "spec/channels/chat_channel_spec.rb" with:
103 | """ruby
104 | require "rails_helper"
105 |
106 | RSpec.describe ChatChannel, :type => :channel do
107 | it "successfully subscribes" do
108 | user = User.new(42)
109 |
110 | expect {
111 | ChatChannel.broadcast_to(user, text: 'Hi')
112 | }.to have_broadcasted_to(user)
113 | end
114 | end
115 | """
116 | And a file named "app/models/user.rb" with:
117 | """ruby
118 | class User < Struct.new(:name)
119 | def to_global_id
120 | name
121 | end
122 | end
123 | """
124 | When I run `rspec spec/channels/chat_channel_spec.rb`
125 | Then the example should pass
126 |
127 | Scenario: Checking broadcast to a record in non-channel spec
128 | Given a file named "spec/models/broadcaster_spec.rb" with:
129 | """ruby
130 | require "rails_helper"
131 |
132 | # Activate Rails 6 compatible API (for `broadcasting_for`)
133 | using ActionCable::Testing::Rails6
134 |
135 | RSpec.describe Broadcaster do
136 | it "matches with stream name" do
137 | user = User.new(42)
138 | skip unless ActionCable::Testing::Rails6::SUPPORTED
139 | expect {
140 | ChatChannel.broadcast_to(user, text: 'Hi')
141 | }.to broadcast_to(ChatChannel.broadcasting_for(user))
142 | end
143 | end
144 | """
145 | And a file named "app/models/user.rb" with:
146 | """ruby
147 | class User < Struct.new(:name)
148 | def to_global_id
149 | name
150 | end
151 | end
152 | """
153 | When I run `rspec spec/models/broadcaster_spec.rb`
154 | Then the example should pass
155 |
--------------------------------------------------------------------------------
/features/matchers/have_stream_from_matcher.feature:
--------------------------------------------------------------------------------
1 | Feature: have_stream_from matcher
2 |
3 | The `have_stream_from` matcher is used to check if a channel has been subscribed to a given stream specified as a String.
4 | If you use `stream_for` in you channel to subscribe to a model, use `have_stream_for` matcher instead.
5 |
6 | The `have_no_streams` matcher is used to check if a channe hasn't been subscribed to any stream.
7 |
8 | It is available only in channel specs.
9 |
10 | Background:
11 | Given action cable is available
12 |
13 | Scenario: subscribing with params and checking streams
14 | Given a file named "spec/channels/chat_channel_spec.rb" with:
15 | """ruby
16 | require "rails_helper"
17 |
18 | RSpec.describe ChatChannel, :type => :channel do
19 | it "successfully subscribes" do
20 | stub_connection user_id: 42
21 | subscribe(room_id: 1)
22 |
23 | expect(subscription).to be_confirmed
24 | expect(subscription).to have_stream_from("chat_1")
25 | end
26 | end
27 | """
28 | When I run `rspec spec/channels/chat_channel_spec.rb`
29 | Then the example should pass
30 |
31 | Scenario: subscribing and checking streams for models
32 | Given a file named "spec/channels/user_channel_spec.rb" with:
33 | """ruby
34 | require "rails_helper"
35 | RSpec.describe UserChannel, :type => :channel do
36 | it "successfully subscribes" do
37 | subscribe(id: 42)
38 | expect(subscription).to be_confirmed
39 | expect(subscription).to have_stream_for(User.new(42))
40 | end
41 | end
42 | """
43 | When I run `rspec spec/channels/user_channel_spec.rb`
44 | Then the example should pass
45 |
46 | Scenario: stopping all streams
47 | Given a file named "spec/channels/chat_channel_spec.rb" with:
48 | """ruby
49 | require "rails_helper"
50 |
51 | RSpec.describe ChatChannel, :type => :channel do
52 | it "successfully subscribes" do
53 | stub_connection user_id: 42
54 | subscribe(room_id: 1)
55 |
56 | expect(subscription).to have_stream_from("chat_1")
57 |
58 | perform :leave
59 | expect(subscription).not_to have_streams
60 | end
61 | end
62 | """
63 | When I run `rspec spec/channels/chat_channel_spec.rb`
64 | Then the example should pass
65 |
--------------------------------------------------------------------------------
/features/shared_contexts/shared_contexts.feature:
--------------------------------------------------------------------------------
1 | Feature: action cable shared contexts
2 |
3 | Sometimes you may want to use _real_ Action Cable adapter instead of the test one (for example,
4 | in Capybara-like tests).
5 |
6 | We provide shared contexts to do that.
7 |
8 | Background:
9 | Given action cable is available
10 |
11 | Scenario: using tags to set async adapter
12 | Given a file named "spec/models/broadcaster_spec.rb" with:
13 | """ruby
14 | require "rails_helper"
15 |
16 | RSpec.describe Broadcaster, action_cable: :async do
17 | it "uses async adapter" do
18 | expect(ActionCable.server.pubsub).to be_a(ActionCable::SubscriptionAdapter::Async)
19 | end
20 | end
21 | """
22 | When I run `rspec spec/models/broadcaster_spec.rb`
23 | Then the example should pass
24 |
25 | Scenario: using shared context to set async adapter
26 | Given a file named "spec/models/broadcaster_spec.rb" with:
27 | """ruby
28 | require "rails_helper"
29 |
30 | RSpec.describe Broadcaster do
31 | context "with async cable" do
32 | include_context "action_cable:async"
33 |
34 | it "uses async adapter" do
35 | expect(ActionCable.server.pubsub).to be_a(ActionCable::SubscriptionAdapter::Async)
36 | end
37 | end
38 | end
39 | """
40 | When I run `rspec spec/models/broadcaster_spec.rb`
41 | Then the example should pass
42 |
43 | Scenario: using tags to set inline adapter
44 | Given a file named "spec/models/broadcaster_spec.rb" with:
45 | """ruby
46 | require "rails_helper"
47 |
48 | RSpec.describe Broadcaster, action_cable: :inline do
49 | it "uses inline adapter" do
50 | expect(ActionCable.server.pubsub).to be_a(ActionCable::SubscriptionAdapter::Inline)
51 | end
52 | end
53 | """
54 | When I run `rspec spec/models/broadcaster_spec.rb`
55 | Then the example should pass
56 |
57 | Scenario: require features specs setup
58 | Given a file named "spec/models/broadcaster_spec.rb" with:
59 | """ruby
60 | require "rails_helper"
61 | require "action_cable/testing/rspec/features"
62 |
63 | RSpec.describe Broadcaster, type: :feature do
64 | it "uses async adapter for feature specs" do
65 | expect(ActionCable.server.pubsub).to be_a(ActionCable::SubscriptionAdapter::Async)
66 | end
67 | end
68 | """
69 | When I run `rspec spec/models/broadcaster_spec.rb`
70 | Then the example should pass
71 |
--------------------------------------------------------------------------------
/features/step_definitions/additional_cli_steps.rb:
--------------------------------------------------------------------------------
1 | begin
2 | require "action_controller/railtie"
3 | require "action_view/railtie"
4 | require "action_cable"
5 | rescue LoadError # rubocop:disable Lint/HandleExceptions
6 | end
7 |
8 | require "rails/version"
9 | require "action_cable/testing/rspec"
10 |
11 | Then /^the example(s)? should( all)? pass$/ do |_, _|
12 | step %q{the output should contain "0 failures"}
13 | step %q{the exit status should be 0}
14 | end
15 |
16 | Then /^the example(s)? should( all)? fail/ do |_, _|
17 | step %q{the output should not contain "0 failures"}
18 | step %q{the exit status should not be 0}
19 | end
20 |
21 | Given /action cable is available/ do
22 | if !RSpec::Rails::FeatureCheck.has_action_cable_testing?
23 | pending "Action Cable is not available"
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/features/support/env.rb:
--------------------------------------------------------------------------------
1 | require "aruba/cucumber"
2 | require "fileutils"
3 |
4 | Before do
5 | @aruba_timeout_seconds = 30
6 | end
7 |
8 | Before do
9 | example_app_dir = "spec/dummy"
10 | aruba_dir = "tmp/aruba"
11 |
12 | # Remove the previous aruba workspace.
13 | FileUtils.rm_rf(aruba_dir) if File.exist?(aruba_dir)
14 | FileUtils.cp_r(example_app_dir, aruba_dir)
15 | end
16 |
--------------------------------------------------------------------------------
/gemfiles/rails50.gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | gem "rails", "~> 5.0.0"
4 |
5 | gemspec path: '..'
6 |
--------------------------------------------------------------------------------
/gemfiles/rails5001.gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | gem "rails", "5.0.0.1"
4 |
5 | gemspec path: '..'
6 |
--------------------------------------------------------------------------------
/gemfiles/rails51.gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | gem "rails", "~> 5.1.0"
4 |
5 | gemspec path: '..'
6 |
--------------------------------------------------------------------------------
/gemfiles/rails52.gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | gem "rails", "~> 5.2.0"
4 |
5 | gemspec path: '..'
6 |
--------------------------------------------------------------------------------
/gemfiles/rspec4rails5.gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | gem "rails", "~> 5.2"
4 | gem "rspec-rails", "4.0.0.beta4"
5 |
6 | gemspec path: '..'
7 |
--------------------------------------------------------------------------------
/gemfiles/rspec4rails6.gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | gem "rails", "~> 6.0"
4 | gem "rspec-rails", "4.0.0.beta4"
5 |
6 | gemspec path: '..'
7 |
--------------------------------------------------------------------------------
/lib/action-cable-testing.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "action_cable/testing"
4 |
--------------------------------------------------------------------------------
/lib/action_cable/subscription_adapter/test.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "action_cable/subscription_adapter/async"
4 |
5 | module ActionCable
6 | module SubscriptionAdapter
7 | # == Test adapter for Action Cable
8 | #
9 | # The test adapter should be used only in testing. Along with
10 | # ActionCable::TestHelper it makes a great tool to test your Rails application.
11 | #
12 | # To use the test adapter set adapter value to +test+ in your +cable.yml+.
13 | #
14 | # NOTE: Test adapter extends the ActionCable::SubscriptionsAdapter::Async adapter,
15 | # so it could be used in system tests too.
16 | class Test < Async
17 | def broadcast(channel, payload)
18 | broadcasts(channel) << payload
19 | super
20 | end
21 |
22 | def broadcasts(channel)
23 | channels_data[channel] ||= []
24 | end
25 |
26 | def clear_messages(channel)
27 | channels_data[channel] = []
28 | end
29 |
30 | def clear
31 | @channels_data = nil
32 | end
33 |
34 | private
35 | def channels_data
36 | @channels_data ||= {}
37 | end
38 | end
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/lib/action_cable/testing.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "action_cable/testing/version"
4 |
5 | require "action_cable"
6 | require "action_cable/testing/rails_six"
7 |
8 | # These has been merged into Rails 6
9 | unless ActionCable::VERSION::MAJOR >= 6
10 | require "action_cable/testing/test_helper"
11 | require "action_cable/testing/test_case"
12 |
13 | require "action_cable/testing/channel/test_case"
14 |
15 | require "action_cable/testing/connection/test_case"
16 |
17 | # We cannot move subsription adapter under 'testing/' path,
18 | # 'cause Action Cable uses this path when resolving an
19 | # adapter from its name (in the config.yml)
20 | require "action_cable/subscription_adapter/test"
21 | end
22 |
--------------------------------------------------------------------------------
/lib/action_cable/testing/channel/test_case.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "active_support"
4 | require "active_support/test_case"
5 | require "active_support/core_ext/hash/indifferent_access"
6 | require "json"
7 |
8 | require "action_cable/testing/rails_six"
9 | using ActionCable::Testing::Rails6
10 |
11 | module ActionCable
12 | module Channel
13 | class NonInferrableChannelError < ::StandardError
14 | def initialize(name)
15 | super "Unable to determine the channel to test from #{name}. " +
16 | "You'll need to specify it using `tests YourChannel` in your " +
17 | "test case definition."
18 | end
19 | end
20 |
21 | # Stub `stream_from` to track streams for the channel.
22 | # Add public aliases for `subscription_confirmation_sent?` and
23 | # `subscription_rejected?`.
24 | module ChannelStub
25 | def confirmed?
26 | subscription_confirmation_sent?
27 | end
28 |
29 | def rejected?
30 | subscription_rejected?
31 | end
32 |
33 | def stream_from(broadcasting, *)
34 | streams << broadcasting
35 | end
36 |
37 | def stop_all_streams
38 | @_streams = []
39 | end
40 |
41 | def streams
42 | @_streams ||= []
43 | end
44 |
45 | # Make periodic timers no-op
46 | def start_periodic_timers; end
47 | alias stop_periodic_timers start_periodic_timers
48 | end
49 |
50 | class ConnectionStub
51 | attr_reader :transmissions, :identifiers, :subscriptions, :logger
52 |
53 | def initialize(identifiers = {})
54 | @transmissions = []
55 |
56 | identifiers.each do |identifier, val|
57 | define_singleton_method(identifier) { val }
58 | end
59 |
60 | @subscriptions = ActionCable::Connection::Subscriptions.new(self)
61 | @identifiers = identifiers.keys
62 | @logger = ActiveSupport::TaggedLogging.new ActiveSupport::Logger.new(StringIO.new)
63 | end
64 |
65 | def transmit(cable_message)
66 | transmissions << cable_message.with_indifferent_access
67 | end
68 | end
69 |
70 | # Superclass for Action Cable channel functional tests.
71 | #
72 | # == Basic example
73 | #
74 | # Functional tests are written as follows:
75 | # 1. First, one uses the +subscribe+ method to simulate subscription creation.
76 | # 2. Then, one asserts whether the current state is as expected. "State" can be anything:
77 | # transmitted messages, subscribed streams, etc.
78 | #
79 | # For example:
80 | #
81 | # class ChatChannelTest < ActionCable::Channel::TestCase
82 | # def test_subscribed_with_room_number
83 | # # Simulate a subscription creation
84 | # subscribe room_number: 1
85 | #
86 | # # Asserts that the subscription was successfully created
87 | # assert subscription.confirmed?
88 | #
89 | # # Asserts that the channel subscribes connection to a stream
90 | # assert_has_stream "chat_1"
91 | #
92 | # # Asserts that the channel subscribes connection to a specific
93 | # # stream created for a model
94 | # assert_has_stream_for Room.find(1)
95 | # end
96 | #
97 | # def test_does_not_stream_with_incorrect_room_number
98 | # subscribe room_number: -1
99 | #
100 | # # Asserts that not streams was started
101 | # assert_no_streams
102 | # end
103 | #
104 | # def test_does_not_subscribe_without_room_number
105 | # subscribe
106 | #
107 | # # Asserts that the subscription was rejected
108 | # assert subscription.rejected?
109 | # end
110 | # end
111 | #
112 | # You can also perform actions:
113 | # def test_perform_speak
114 | # subscribe room_number: 1
115 | #
116 | # perform :speak, message: "Hello, Rails!"
117 | #
118 | # assert_equal "Hello, Rails!", transmissions.last["text"]
119 | # end
120 | #
121 | # == Special methods
122 | #
123 | # ActionCable::Channel::TestCase will also automatically provide the following instance
124 | # methods for use in the tests:
125 | #
126 | # connection::
127 | # An ActionCable::Channel::ConnectionStub, representing the current HTTP connection.
128 | # subscription::
129 | # An instance of the current channel, created when you call `subscribe`.
130 | # transmissions::
131 | # A list of all messages that have been transmitted into the channel.
132 | #
133 | #
134 | # == Channel is automatically inferred
135 | #
136 | # ActionCable::Channel::TestCase will automatically infer the channel under test
137 | # from the test class name. If the channel cannot be inferred from the test
138 | # class name, you can explicitly set it with +tests+.
139 | #
140 | # class SpecialEdgeCaseChannelTest < ActionCable::Channel::TestCase
141 | # tests SpecialChannel
142 | # end
143 | #
144 | # == Specifying connection identifiers
145 | #
146 | # You need to set up your connection manually to privide values for the identifiers.
147 | # To do this just use:
148 | #
149 | # stub_connection(user: users[:john])
150 | class TestCase < ActiveSupport::TestCase
151 | module Behavior
152 | extend ActiveSupport::Concern
153 |
154 | include ActiveSupport::Testing::ConstantLookup
155 | include ActionCable::TestHelper
156 |
157 | CHANNEL_IDENTIFIER = "test_stub"
158 |
159 | included do
160 | class_attribute :_channel_class
161 |
162 | attr_reader :connection, :subscription
163 |
164 | def streams
165 | ActiveSupport::Deprecation.warn "Use appropriate `assert_has_stream`, `assert_has_stream_for`, `assert_no_streams` " +
166 | "assertion methods for minitest or `have_stream`, `have_stream_for` and `have_stream_from` matchers " +
167 | "for RSpec. Direct access to `streams` is deprecated and is going to be removed in version 1.0"
168 | subscription.streams
169 | end
170 |
171 | ActiveSupport.run_load_hooks(:action_cable_channel_test_case, self)
172 | end
173 |
174 | module ClassMethods
175 | def tests(channel)
176 | case channel
177 | when String, Symbol
178 | self._channel_class = channel.to_s.camelize.constantize
179 | when Module
180 | self._channel_class = channel
181 | else
182 | raise NonInferrableChannelError.new(channel)
183 | end
184 | end
185 |
186 | def channel_class
187 | if channel = self._channel_class
188 | channel
189 | else
190 | tests determine_default_channel(name)
191 | end
192 | end
193 |
194 | def determine_default_channel(name)
195 | channel = determine_constant_from_test_name(name) do |constant|
196 | Class === constant && constant < ActionCable::Channel::Base
197 | end
198 | raise NonInferrableChannelError.new(name) if channel.nil?
199 | channel
200 | end
201 | end
202 |
203 | # Setup test connection with the specified identifiers:
204 | #
205 | # class ApplicationCable < ActionCable::Connection::Base
206 | # identified_by :user, :token
207 | # end
208 | #
209 | # stub_connection(user: users[:john], token: 'my-secret-token')
210 | def stub_connection(identifiers = {})
211 | @connection = ConnectionStub.new(identifiers)
212 | end
213 |
214 | # Subsribe to the channel under test. Optionally pass subscription parameters as a Hash.
215 | def subscribe(params = {})
216 | @connection ||= stub_connection
217 | # NOTE: Rails < 5.0.1 calls subscribe_to_channel during #initialize.
218 | # We have to stub before it
219 | @subscription = self.class.channel_class.allocate
220 | @subscription.singleton_class.include(ChannelStub)
221 | @subscription.send(:initialize, connection, CHANNEL_IDENTIFIER, params.with_indifferent_access)
222 | # Call subscribe_to_channel if it's public (Rails 5.0.1+)
223 | @subscription.subscribe_to_channel if ActionCable.gem_version >= Gem::Version.new("5.0.1")
224 | @subscription
225 | end
226 |
227 | # Unsubscribe the subscription under test.
228 | def unsubscribe
229 | check_subscribed!
230 | subscription.unsubscribe_from_channel
231 | end
232 |
233 | # Perform action on a channel.
234 | #
235 | # NOTE: Must be subscribed.
236 | def perform(action, data = {})
237 | check_subscribed!
238 | subscription.perform_action(data.stringify_keys.merge("action" => action.to_s))
239 | end
240 |
241 | # Returns messages transmitted into channel
242 | def transmissions
243 | # Return only directly sent message (via #transmit)
244 | connection.transmissions.map { |data| data["message"] }.compact
245 | end
246 |
247 | # Enhance TestHelper assertions to handle non-String
248 | # broadcastings
249 | def assert_broadcasts(stream_or_object, *args)
250 | super(broadcasting_for(stream_or_object), *args)
251 | end
252 |
253 | def assert_broadcast_on(stream_or_object, *args)
254 | super(broadcasting_for(stream_or_object), *args)
255 | end
256 |
257 | # Asserts that no streams have been started.
258 | #
259 | # def test_assert_no_started_stream
260 | # subscribe
261 | # assert_no_streams
262 | # end
263 | #
264 | def assert_no_streams
265 | assert subscription.streams.empty?, "No streams started was expected, but #{subscription.streams.count} found"
266 | end
267 |
268 | # Asserts that the specified stream has been started.
269 | #
270 | # def test_assert_started_stream
271 | # subscribe
272 | # assert_has_stream 'messages'
273 | # end
274 | #
275 | def assert_has_stream(stream)
276 | assert subscription.streams.include?(stream), "Stream #{stream} has not been started"
277 | end
278 |
279 | # Asserts that the specified stream for a model has started.
280 | #
281 | # def test_assert_started_stream_for
282 | # subscribe id: 42
283 | # assert_has_stream_for User.find(42)
284 | # end
285 | #
286 | def assert_has_stream_for(object)
287 | assert_has_stream(broadcasting_for(object))
288 | end
289 |
290 | private
291 | def check_subscribed!
292 | raise "Must be subscribed!" if subscription.nil? || subscription.rejected?
293 | end
294 |
295 | def broadcasting_for(stream_or_object)
296 | return stream_or_object if stream_or_object.is_a?(String)
297 |
298 | self.class.channel_class.broadcasting_for(stream_or_object)
299 | end
300 | end
301 |
302 | include Behavior
303 | end
304 | end
305 | end
306 |
--------------------------------------------------------------------------------
/lib/action_cable/testing/connection/test_case.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "active_support"
4 | require "active_support/test_case"
5 | require "active_support/core_ext/hash/indifferent_access"
6 | require "action_dispatch"
7 | require "action_dispatch/http/headers"
8 | require "action_dispatch/testing/test_request"
9 |
10 | module ActionCable
11 | module Connection
12 | class NonInferrableConnectionError < ::StandardError
13 | def initialize(name)
14 | super "Unable to determine the connection to test from #{name}. " +
15 | "You'll need to specify it using `tests YourConnection` in your " +
16 | "test case definition."
17 | end
18 | end
19 |
20 | module Assertions
21 | # Asserts that the connection is rejected (via +reject_unauthorized_connection+).
22 | #
23 | # # Asserts that connection without user_id fails
24 | # assert_reject_connection { connect cookies: { user_id: '' } }
25 | def assert_reject_connection(&block)
26 | assert_raises(Authorization::UnauthorizedError, "Expected to reject connection but no rejection was made", &block)
27 | end
28 | end
29 |
30 | # We don't want to use the whole "encryption stack" for connection
31 | # unit-tests, but we want to make sure that users test against the correct types
32 | # of cookies (i.e. signed or encrypted or plain)
33 | class TestCookieJar < ActiveSupport::HashWithIndifferentAccess
34 | def signed
35 | self[:signed] ||= {}.with_indifferent_access
36 | end
37 |
38 | def encrypted
39 | self[:encrypted] ||= {}.with_indifferent_access
40 | end
41 | end
42 |
43 | class TestRequest < ActionDispatch::TestRequest
44 | attr_accessor :session, :cookie_jar
45 | end
46 |
47 | module TestConnection
48 | attr_reader :logger, :request
49 |
50 | def initialize(request)
51 | inner_logger = ActiveSupport::Logger.new(StringIO.new)
52 | tagged_logging = ActiveSupport::TaggedLogging.new(inner_logger)
53 | @logger = ActionCable::Connection::TaggedLoggerProxy.new(tagged_logging, tags: [])
54 | @request = request
55 | @env = request.env
56 | end
57 | end
58 |
59 | # Unit test Action Cable connections.
60 | #
61 | # Useful to check whether a connection's +identified_by+ gets assigned properly
62 | # and that any improper connection requests are rejected.
63 | #
64 | # == Basic example
65 | #
66 | # Unit tests are written as follows:
67 | #
68 | # 1. Simulate a connection attempt by calling +connect+.
69 | # 2. Assert state, e.g. identifiers, has been assigned.
70 | #
71 | #
72 | # class ApplicationCable::ConnectionTest < ActionCable::Connection::TestCase
73 | # def test_connects_with_proper_cookie
74 | # # Simulate the connection request with a cookie.
75 | # cookies["user_id"] = users(:john).id
76 | #
77 | # connect
78 | #
79 | # # Assert the connection identifier matches the fixture.
80 | # assert_equal users(:john).id, connection.user.id
81 | # end
82 | #
83 | # def test_rejects_connection_without_proper_cookie
84 | # assert_reject_connection { connect }
85 | # end
86 | # end
87 | #
88 | # +connect+ accepts additional information the HTTP request with the
89 | # +params+, +headers+, +session+ and Rack +env+ options.
90 | #
91 | # def test_connect_with_headers_and_query_string
92 | # connect params: { user_id: 1 }, headers: { "X-API-TOKEN" => "secret-my" }
93 | #
94 | # assert_equal "1", connection.user.id
95 | # assert_equal "secret-my", connection.token
96 | # end
97 | #
98 | # def test_connect_with_params
99 | # connect params: { user_id: 1 }
100 | #
101 | # assert_equal "1", connection.user.id
102 | # end
103 | #
104 | # You can also setup the correct cookies before the connection request:
105 | #
106 | # def test_connect_with_cookies
107 | # # Plain cookies:
108 | # cookies["user_id"] = 1
109 | #
110 | # # Or signed/encrypted:
111 | # # cookies.signed["user_id"] = 1
112 | # # cookies.encrypted["user_id"] = 1
113 | #
114 | # connect
115 | #
116 | # assert_equal "1", connection.user_id
117 | # end
118 | #
119 | # == Connection is automatically inferred
120 | #
121 | # ActionCable::Connection::TestCase will automatically infer the connection under test
122 | # from the test class name. If the channel cannot be inferred from the test
123 | # class name, you can explicitly set it with +tests+.
124 | #
125 | # class ConnectionTest < ActionCable::Connection::TestCase
126 | # tests ApplicationCable::Connection
127 | # end
128 | #
129 | class TestCase < ActiveSupport::TestCase
130 | module Behavior
131 | extend ActiveSupport::Concern
132 |
133 | DEFAULT_PATH = "/cable"
134 |
135 | include ActiveSupport::Testing::ConstantLookup
136 | include Assertions
137 |
138 | included do
139 | class_attribute :_connection_class
140 |
141 | attr_reader :connection
142 |
143 | ActiveSupport.run_load_hooks(:action_cable_connection_test_case, self)
144 | end
145 |
146 | module ClassMethods
147 | def tests(connection)
148 | case connection
149 | when String, Symbol
150 | self._connection_class = connection.to_s.camelize.constantize
151 | when Module
152 | self._connection_class = connection
153 | else
154 | raise NonInferrableConnectionError.new(connection)
155 | end
156 | end
157 |
158 | def connection_class
159 | if connection = self._connection_class
160 | connection
161 | else
162 | tests determine_default_connection(name)
163 | end
164 | end
165 |
166 | def determine_default_connection(name)
167 | connection = determine_constant_from_test_name(name) do |constant|
168 | Class === constant && constant < ActionCable::Connection::Base
169 | end
170 | raise NonInferrableConnectionError.new(name) if connection.nil?
171 | connection
172 | end
173 | end
174 |
175 | # Performs connection attempt to exert #connect on the connection under test.
176 | #
177 | # Accepts request path as the first argument and the following request options:
178 | #
179 | # - params – url parameters (Hash)
180 | # - headers – request headers (Hash)
181 | # - session – session data (Hash)
182 | # - env – additional Rack env configuration (Hash)
183 | def connect(path = ActionCable.server.config.mount_path, cookies: nil, **request_params)
184 | path ||= DEFAULT_PATH
185 |
186 | unless cookies.nil?
187 | ActiveSupport::Deprecation.warn(
188 | "Use `cookies[:param] = value` (or `cookies.signed[:param] = value`). " \
189 | "Passing `cookies` as the `connect` option is deprecated and is going to be removed in version 1.0"
190 | )
191 | cookies.each { |k, v| self.cookies[k] = v }
192 | end
193 |
194 | connection = self.class.connection_class.allocate
195 | connection.singleton_class.include(TestConnection)
196 | connection.send(:initialize, build_test_request(path, **request_params))
197 | connection.connect if connection.respond_to?(:connect)
198 |
199 | # Only set instance variable if connected successfully
200 | @connection = connection
201 | end
202 |
203 | # Exert #disconnect on the connection under test.
204 | def disconnect
205 | raise "Must be connected!" if connection.nil?
206 |
207 | connection.disconnect if connection.respond_to?(:disconnect)
208 | @connection = nil
209 | end
210 |
211 | def cookies
212 | @cookie_jar ||= TestCookieJar.new
213 | end
214 |
215 | private
216 | def build_test_request(path, params: nil, headers: {}, session: {}, env: {})
217 | wrapped_headers = ActionDispatch::Http::Headers.from_hash(headers)
218 |
219 | uri = URI.parse(path)
220 |
221 | query_string = params.nil? ? uri.query : params.to_query
222 |
223 | request_env = {
224 | "QUERY_STRING" => query_string,
225 | "PATH_INFO" => uri.path
226 | }.merge(env)
227 |
228 | if wrapped_headers.present?
229 | ActionDispatch::Http::Headers.from_hash(request_env).merge!(wrapped_headers)
230 | end
231 |
232 | TestRequest.create(request_env).tap do |request|
233 | request.session = session.with_indifferent_access
234 | request.cookie_jar = cookies
235 | end
236 | end
237 | end
238 |
239 | include Behavior
240 | end
241 | end
242 | end
243 |
--------------------------------------------------------------------------------
/lib/action_cable/testing/rails_six.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module ActionCable
4 | module Testing
5 | # Enables Rails 6 compatible API via Refinements.
6 | #
7 | # Use this to write Rails 6 compatible tests (not every Rails 6 API
8 | # could be backported).
9 | #
10 | # Usage:
11 | # # my_test.rb
12 | # require "test_helper"
13 | #
14 | # using ActionCable::Testing::Syntax
15 | module Rails6
16 | begin
17 | # Has been added only after 6.0.0.beta1
18 | unless ActionCable::Channel::Base.respond_to?(:serialize_broadcasting)
19 | refine ActionCable::Channel::Broadcasting::ClassMethods do
20 | def broadcasting_for(model)
21 | super([channel_name, model])
22 | end
23 | end
24 | end
25 |
26 | SUPPORTED = true
27 | rescue TypeError
28 | warn "Your Ruby version doesn't suppport Module refinements. " \
29 | "Rails 6 compatibility refinement could not be applied"
30 |
31 | SUPPORTED = false
32 | end
33 | end
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/lib/action_cable/testing/rspec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "action-cable-testing"
4 | require "rspec/rails"
5 |
6 | if RSpec::Rails::FeatureCheck.respond_to?(:has_action_cable_testing?)
7 | warn <<~MSG
8 | You're using RSpec with Action Cable support.
9 |
10 | You can remove `require "action_cable/testing/rspec"` from your RSpec setup.
11 |
12 | NOTE: if you use Action Cable shared contexts ("action_cable:async", "action_cable:inline", etc.)
13 | you still need to use the gem and add `require "rspec/rails/shared_contexts/action_cable"`.
14 | MSG
15 | else
16 | require "rspec/rails/example/channel_example_group"
17 | require "rspec/rails/matchers/action_cable"
18 |
19 | module RSpec # :nodoc:
20 | module Rails
21 | module FeatureCheck
22 | module_function
23 | def has_action_cable_testing?
24 | defined?(::ActionCable)
25 | end
26 | end
27 |
28 | self::DIRECTORY_MAPPINGS[:channel] = %w[spec channels]
29 | end
30 | end
31 |
32 | RSpec.configure do |config|
33 | if defined?(ActionCable)
34 | config.include RSpec::Rails::ChannelExampleGroup, type: :channel
35 | end
36 | end
37 | end
38 |
39 | require "rspec/rails/shared_contexts/action_cable"
40 |
--------------------------------------------------------------------------------
/lib/action_cable/testing/rspec/features.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Use async adapter for features specs by deafault
4 | RSpec.configure do |config|
5 | config.include_context "action_cable:async", type: [:feature, :system]
6 | end
7 |
--------------------------------------------------------------------------------
/lib/action_cable/testing/test_case.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "active_support/test_case"
4 |
5 | module ActionCable
6 | class TestCase < ActiveSupport::TestCase
7 | include ActionCable::TestHelper
8 |
9 | ActiveSupport.run_load_hooks(:action_cable_test_case, self)
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/lib/action_cable/testing/test_helper.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "action_cable/testing/rails_six"
4 | using ActionCable::Testing::Rails6
5 |
6 | module ActionCable
7 | # Provides helper methods for testing Action Cable broadcasting
8 | module TestHelper
9 | CHANNEL_NOT_FOUND = ArgumentError.new("Broadcasting channel can't be infered. Please, specify it with `:channel`")
10 |
11 | def before_setup # :nodoc:
12 | server = ActionCable.server
13 | test_adapter = ActionCable::SubscriptionAdapter::Test.new(server)
14 |
15 | @old_pubsub_adapter = server.pubsub
16 |
17 | server.instance_variable_set(:@pubsub, test_adapter)
18 | super
19 | end
20 |
21 | def after_teardown # :nodoc:
22 | super
23 | ActionCable.server.instance_variable_set(:@pubsub, @old_pubsub_adapter)
24 | end
25 |
26 | # Asserts that the number of broadcasted messages to the channel matches the given number.
27 | #
28 | # def test_broadcasts
29 | # assert_broadcasts 'messages', 0
30 | # ActionCable.server.broadcast 'messages', { text: 'hello' }
31 | # assert_broadcasts 'messages', 1
32 | # ActionCable.server.broadcast 'messages', { text: 'world' }
33 | # assert_broadcasts 'messages', 2
34 | # end
35 | #
36 | # If a block is passed, that block should cause the specified number of
37 | # messages to be broadcasted.
38 | #
39 | # def test_broadcasts_again
40 | # assert_broadcasts('messages', 1) do
41 | # ActionCable.server.broadcast 'messages', { text: 'hello' }
42 | # end
43 | #
44 | # assert_broadcasts('messages', 2) do
45 | # ActionCable.server.broadcast 'messages', { text: 'hi' }
46 | # ActionCable.server.broadcast 'messages', { text: 'how are you?' }
47 | # end
48 | # end
49 | #
50 | def assert_broadcasts(target, number, channel: nil)
51 | warn_deprecated_channel! unless channel.nil?
52 |
53 | stream = stream(target, channel)
54 |
55 | if block_given?
56 | original_count = broadcasts_size(stream)
57 | yield
58 | new_count = broadcasts_size(stream)
59 | assert_equal number, new_count - original_count, "#{number} broadcasts to #{stream} expected, but #{new_count - original_count} were sent"
60 | else
61 | actual_count = broadcasts_size(stream)
62 | assert_equal number, actual_count, "#{number} broadcasts to #{stream} expected, but #{actual_count} were sent"
63 | end
64 | end
65 |
66 | # Asserts that no messages have been sent to the channel.
67 | #
68 | # def test_no_broadcasts
69 | # assert_no_broadcasts 'messages'
70 | # ActionCable.server.broadcast 'messages', { text: 'hi' }
71 | # assert_broadcasts 'messages', 1
72 | # end
73 | #
74 | # If a block is passed, that block should not cause any message to be sent.
75 | #
76 | # def test_broadcasts_again
77 | # assert_no_broadcasts 'messages' do
78 | # # No job messages should be sent from this block
79 | # end
80 | # end
81 | #
82 | # Note: This assertion is simply a shortcut for:
83 | #
84 | # assert_broadcasts 'messages', 0, &block
85 | #
86 | def assert_no_broadcasts(target, &block)
87 | assert_broadcasts target, 0, &block
88 | end
89 |
90 | # Asserts that the specified message has been sent to the channel.
91 | #
92 | # def test_assert_transmited_message
93 | # ActionCable.server.broadcast 'messages', text: 'hello'
94 | # assert_broadcast_on('messages', text: 'hello')
95 | # end
96 | #
97 | # If a block is passed, that block should cause a message with the specified data to be sent.
98 | #
99 | # def test_assert_broadcast_on_again
100 | # assert_broadcast_on('messages', text: 'hello') do
101 | # ActionCable.server.broadcast 'messages', text: 'hello'
102 | # end
103 | # end
104 | #
105 | def assert_broadcast_on(target, data, channel: nil)
106 | warn_deprecated_channel! unless channel.nil?
107 |
108 | # Encode to JSON and back–we want to use this value to compare
109 | # with decoded JSON.
110 | # Comparing JSON strings doesn't work due to the order if the keys.
111 | serialized_msg =
112 | ActiveSupport::JSON.decode(ActiveSupport::JSON.encode(data))
113 | stream = stream(target, channel)
114 |
115 | new_messages = broadcasts(stream)
116 | if block_given?
117 | old_messages = new_messages
118 | clear_messages(stream)
119 |
120 | yield
121 | new_messages = broadcasts(stream)
122 | clear_messages(stream)
123 |
124 | # Restore all sent messages
125 | (old_messages + new_messages).each { |m| pubsub_adapter.broadcast(stream, m) }
126 | end
127 |
128 | message = new_messages.find { |msg| ActiveSupport::JSON.decode(msg) == serialized_msg }
129 |
130 | assert message, "No messages sent with #{data} to #{stream}"
131 | end
132 |
133 | def pubsub_adapter # :nodoc:
134 | ActionCable.server.pubsub
135 | end
136 |
137 | delegate :broadcasts, :clear_messages, to: :pubsub_adapter
138 |
139 | private
140 | def broadcasts_size(channel) # :nodoc:
141 | broadcasts(channel).size
142 | end
143 |
144 | def stream(target, channel = nil)
145 | return target if target.is_a?(String)
146 |
147 | channel ||= @subscription
148 | return target unless channel && channel.respond_to?(:channel_name)
149 |
150 | channel.broadcasting_for(target)
151 | end
152 |
153 | def warn_deprecated_channel!
154 | ActiveSupport::Deprecation.warn(
155 | "Passing channel class is deprecated and will be removed in version 1.0. " \
156 | "Use `Channel.broadcasting_for(object) to build a stream name instead and " \
157 | "add `using ActionCable::Testing::Rails6` to your test file."
158 | )
159 | end
160 | end
161 | end
162 |
--------------------------------------------------------------------------------
/lib/action_cable/testing/version.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module ActionCable
4 | module Testing
5 | VERSION = "0.6.1"
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/lib/generators/rspec/channel/channel_generator.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "generators/rspec"
4 |
5 | module Rspec
6 | module Generators
7 | # @private
8 | class ChannelGenerator < Base
9 | source_root File.expand_path("../templates", __FILE__)
10 |
11 | def create_channel_spec
12 | template "channel_spec.rb.erb", File.join("spec/channels", class_path, "#{file_name}_channel_spec.rb")
13 | end
14 | end
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/lib/generators/rspec/channel/templates/channel_spec.rb.erb:
--------------------------------------------------------------------------------
1 | require 'rails_helper'
2 |
3 | <% module_namespacing do -%>
4 | RSpec.describe <%= class_name %>Channel, <%= type_metatag(:channel) %> do
5 | pending "add some examples to (or delete) #{__FILE__}"
6 | end
7 | <% end -%>
8 |
--------------------------------------------------------------------------------
/lib/generators/test_unit/channel/channel_generator.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "rails/generators/test_unit"
4 |
5 | module TestUnit # :nodoc:
6 | module Generators # :nodoc:
7 | class ChannelGenerator < Base # :nodoc:
8 | source_root File.expand_path("../templates", __FILE__)
9 |
10 | check_class_collision suffix: "ChannelTest"
11 |
12 | def create_test_file
13 | template "unit_test.rb.erb", File.join("test/channels", class_path, "#{file_name}_channel_test.rb")
14 | end
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/lib/generators/test_unit/channel/templates/unit_test.rb.erb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | <% module_namespacing do -%>
4 | class <%= class_name %>ChannelTest < ActionCable::Channel::TestCase
5 | # test "the truth" do
6 | # assert true
7 | # end
8 | end
9 | <% end -%>
10 |
--------------------------------------------------------------------------------
/lib/rspec/rails/example/channel_example_group.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "rspec/rails/matchers/action_cable/have_streams"
4 |
5 | module RSpec
6 | module Rails
7 | # @api public
8 | # Container module for channel spec functionality. It is only available if
9 | # ActionCable has been loaded before it.
10 | module ChannelExampleGroup
11 | # This blank module is only necessary for YARD processing. It doesn't
12 | # handle the conditional `defined?` check below very well.
13 | end
14 | end
15 | end
16 |
17 | if defined?(ActionCable)
18 | module RSpec
19 | module Rails
20 | # Container module for channel spec functionality.
21 | module ChannelExampleGroup
22 | extend ActiveSupport::Concern
23 | include RSpec::Rails::RailsExampleGroup
24 | include ActionCable::Connection::TestCase::Behavior
25 | include ActionCable::Channel::TestCase::Behavior
26 |
27 | # Class-level DSL for channel specs.
28 | module ClassMethods
29 | # @private
30 | def channel_class
31 | described_class
32 | end
33 |
34 | # @private
35 | def connection_class
36 | raise "Described class is not a Connection class" unless
37 | described_class <= ::ActionCable::Connection::Base
38 | described_class
39 | end
40 | end
41 |
42 | def have_rejected_connection
43 | raise_error(::ActionCable::Connection::Authorization::UnauthorizedError)
44 | end
45 |
46 | def have_streams
47 | check_subscribed!
48 |
49 | RSpec::Rails::Matchers::ActionCable::HaveStream.new
50 | end
51 |
52 | def have_stream_from(stream)
53 | check_subscribed!
54 |
55 | RSpec::Rails::Matchers::ActionCable::HaveStream.new(stream)
56 | end
57 |
58 | def have_stream_for(object)
59 | check_subscribed!
60 |
61 | RSpec::Rails::Matchers::ActionCable::HaveStream.new(broadcasting_for(object))
62 | end
63 | end
64 | end
65 | end
66 | end
67 |
--------------------------------------------------------------------------------
/lib/rspec/rails/matchers/action_cable.rb:
--------------------------------------------------------------------------------
1 | # rubocop: disable Style/FrozenStringLiteralComment
2 |
3 | require "rspec/rails/matchers/action_cable/have_broadcasted_to"
4 |
5 | module RSpec
6 | module Rails
7 | module Matchers
8 | # Namespace for various implementations of ActionCable features
9 | #
10 | # @api private
11 | module ActionCable
12 | end
13 |
14 | # @api public
15 | # Passes if a message has been sent to a stream/object inside a block.
16 | # May chain `at_least`, `at_most` or `exactly` to specify a number of times.
17 | # To specify channel from which message has been broadcasted to object use `from_channel`.
18 | #
19 | #
20 | # @example
21 | # expect {
22 | # ActionCable.server.broadcast "messages", text: 'Hi!'
23 | # }.to have_broadcasted_to("messages")
24 | #
25 | # expect {
26 | # SomeChannel.broadcast_to(user)
27 | # }.to have_broadcasted_to(user).from_channel(SomeChannel)
28 | #
29 | # # Using alias
30 | # expect {
31 | # ActionCable.server.broadcast "messages", text: 'Hi!'
32 | # }.to broadcast_to("messages")
33 | #
34 | # expect {
35 | # ActionCable.server.broadcast "messages", text: 'Hi!'
36 | # ActionCable.server.broadcast "all", text: 'Hi!'
37 | # }.to have_broadcasted_to("messages").exactly(:once)
38 | #
39 | # expect {
40 | # 3.times { ActionCable.server.broadcast "messages", text: 'Hi!' }
41 | # }.to have_broadcasted_to("messages").at_least(2).times
42 | #
43 | # expect {
44 | # ActionCable.server.broadcast "messages", text: 'Hi!'
45 | # }.to have_broadcasted_to("messages").at_most(:twice)
46 | #
47 | # expect {
48 | # ActionCable.server.broadcast "messages", text: 'Hi!'
49 | # }.to have_broadcasted_to("messages").with(text: 'Hi!')
50 |
51 | def have_broadcasted_to(target = nil)
52 | check_action_cable_adapter
53 |
54 | ActionCable::HaveBroadcastedTo.new(target, channel: described_class)
55 | end
56 | alias_method :broadcast_to, :have_broadcasted_to
57 |
58 | private
59 |
60 | # @private
61 | def check_action_cable_adapter
62 | return if ::ActionCable::SubscriptionAdapter::Test === ::ActionCable.server.pubsub
63 | raise StandardError, "To use ActionCable matchers set `adapter: test` in your cable.yml"
64 | end
65 | end
66 | end
67 | end
68 |
--------------------------------------------------------------------------------
/lib/rspec/rails/matchers/action_cable/have_broadcasted_to.rb:
--------------------------------------------------------------------------------
1 | # rubocop: disable Style/FrozenStringLiteralComment
2 |
3 | require "action_cable/testing/rails_six"
4 | using ActionCable::Testing::Rails6
5 |
6 | module RSpec
7 | module Rails
8 | module Matchers
9 | module ActionCable
10 | # rubocop: disable Metrics/ClassLength
11 | # @private
12 | class HaveBroadcastedTo < RSpec::Matchers::BuiltIn::BaseMatcher
13 | def initialize(target, channel:)
14 | @target = target
15 | @channel = channel
16 | @block = Proc.new {}
17 | set_expected_number(:exactly, 1)
18 | end
19 |
20 | def with(data = nil, &block)
21 | @data = data
22 | @data = @data.with_indifferent_access if @data.is_a?(Hash)
23 | @block = block if block_given?
24 | self
25 | end
26 |
27 | def exactly(count)
28 | set_expected_number(:exactly, count)
29 | self
30 | end
31 |
32 | def at_least(count)
33 | set_expected_number(:at_least, count)
34 | self
35 | end
36 |
37 | def at_most(count)
38 | set_expected_number(:at_most, count)
39 | self
40 | end
41 |
42 | def times
43 | self
44 | end
45 |
46 | def once
47 | exactly(:once)
48 | end
49 |
50 | def twice
51 | exactly(:twice)
52 | end
53 |
54 | def thrice
55 | exactly(:thrice)
56 | end
57 |
58 | def failure_message
59 | "expected to broadcast #{base_message}".tap do |msg|
60 | if @unmatching_msgs.any?
61 | msg << "\nBroadcasted messages to #{stream}:"
62 | @unmatching_msgs.each do |data|
63 | msg << "\n #{data}"
64 | end
65 | end
66 | end
67 | end
68 |
69 | def failure_message_when_negated
70 | "expected not to broadcast #{base_message}"
71 | end
72 |
73 | def message_expectation_modifier
74 | case @expectation_type
75 | when :exactly then "exactly"
76 | when :at_most then "at most"
77 | when :at_least then "at least"
78 | end
79 | end
80 |
81 | def supports_block_expectations?
82 | true
83 | end
84 |
85 | def matches?(proc)
86 | raise ArgumentError, "have_broadcasted_to and broadcast_to only support block expectations" unless Proc === proc
87 |
88 | original_sent_messages_count = pubsub_adapter.broadcasts(stream).size
89 | proc.call
90 | in_block_messages = pubsub_adapter.broadcasts(stream).drop(original_sent_messages_count)
91 |
92 | check(in_block_messages)
93 | end
94 |
95 | def from_channel(channel)
96 | @channel = channel
97 | self
98 | end
99 |
100 | private
101 |
102 | def stream
103 | @stream ||= if @target.is_a?(String)
104 | @target
105 | else
106 | check_channel_presence
107 | @channel.broadcasting_for(@target)
108 | end
109 | end
110 |
111 | def check(messages)
112 | @matching_msgs, @unmatching_msgs = messages.partition do |msg|
113 | decoded = ActiveSupport::JSON.decode(msg)
114 | decoded = decoded.with_indifferent_access if decoded.is_a?(Hash)
115 |
116 | if @data.nil? || @data === decoded
117 | @block.call(decoded)
118 | true
119 | else
120 | false
121 | end
122 | end
123 |
124 | @matching_msgs_count = @matching_msgs.size
125 |
126 | case @expectation_type
127 | when :exactly then @expected_number == @matching_msgs_count
128 | when :at_most then @expected_number >= @matching_msgs_count
129 | when :at_least then @expected_number <= @matching_msgs_count
130 | end
131 | end
132 |
133 | def set_expected_number(relativity, count)
134 | @expectation_type = relativity
135 | @expected_number =
136 | case count
137 | when :once then 1
138 | when :twice then 2
139 | when :thrice then 3
140 | else Integer(count)
141 | end
142 | end
143 |
144 | def base_message
145 | "#{message_expectation_modifier} #{@expected_number} messages to #{stream}".tap do |msg|
146 | msg << " with #{data_description(@data)}" unless @data.nil?
147 | msg << ", but broadcast #{@matching_msgs_count}"
148 | end
149 | end
150 |
151 | def data_description(data)
152 | if data.is_a?(RSpec::Matchers::Composable)
153 | data.description
154 | else
155 | data
156 | end
157 | end
158 |
159 | def pubsub_adapter
160 | ::ActionCable.server.pubsub
161 | end
162 |
163 | def check_channel_presence
164 | return if @channel.present? && @channel.respond_to?(:channel_name)
165 |
166 | error_msg = "Broadcasting channel can't be infered. Please, specify it with `from_channel`"
167 | raise ArgumentError, error_msg
168 | end
169 | end
170 | end
171 | end
172 | end
173 | end
174 |
--------------------------------------------------------------------------------
/lib/rspec/rails/matchers/action_cable/have_streams.rb:
--------------------------------------------------------------------------------
1 | # rubocop: disable Style/FrozenStringLiteralComment
2 |
3 | module RSpec
4 | module Rails
5 | module Matchers
6 | module ActionCable
7 | # @api private
8 | # Provides the implementation for `have_stream`, `have_stream_for`, and `have_stream_from`.
9 | # Not intended to be instantiated directly.
10 | class HaveStream < RSpec::Matchers::BuiltIn::BaseMatcher
11 | # @api private
12 | # @return [String]
13 | def failure_message
14 | "expected to have #{base_message}"
15 | end
16 |
17 | # @api private
18 | # @return [String]
19 | def failure_message_when_negated
20 | "expected not to have #{base_message}"
21 | end
22 |
23 | # @api private
24 | # @return [Boolean]
25 | def matches?(subscription)
26 | raise(ArgumentError, "have_streams is used for negated expectations only") if no_expected?
27 |
28 | match(subscription)
29 | end
30 |
31 | # @api private
32 | # @return [Boolean]
33 | def does_not_match?(subscription)
34 | !match(subscription)
35 | end
36 |
37 | private
38 |
39 | def match(subscription)
40 | case subscription
41 | when ::ActionCable::Channel::Base
42 | @actual = subscription.streams
43 | no_expected? ? actual.any? : actual.any? { |i| expected === i }
44 | else
45 | raise ArgumentError, "have_stream, have_stream_from and have_stream_from support expectations on subscription only"
46 | end
47 | end
48 |
49 | def base_message
50 | no_expected? ? "any stream started" : "stream #{expected_formatted} started, but have #{actual_formatted}"
51 | end
52 |
53 | def no_expected?
54 | !defined?(@expected)
55 | end
56 | end
57 | end
58 | end
59 | end
60 | end
61 |
--------------------------------------------------------------------------------
/lib/rspec/rails/shared_contexts/action_cable.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Generate contexts to use specific Action Cable adapter:
4 | # - "action_cable:async" (action_cable: :async)
5 | # - "action_cable:inline" (action_cable: :inline)
6 | # - "action_cable:test" (action_cable: :test)
7 | %w[async inline test].each do |adapter|
8 | RSpec.shared_context "action_cable:#{adapter}" do
9 | require "action_cable/subscription_adapter/#{adapter}"
10 |
11 | adapter_class = ActionCable::SubscriptionAdapter.const_get(adapter.capitalize)
12 |
13 | before do
14 | next if ActionCable.server.pubsub.is_a?(adapter_class)
15 |
16 | @__was_pubsub_adapter__ = ActionCable.server.pubsub
17 |
18 | adapter = adapter_class.new(ActionCable.server)
19 | ActionCable.server.instance_variable_set(:@pubsub, adapter)
20 | end
21 |
22 | after do
23 | next unless instance_variable_defined?(:@__was_pubsub_adapter__)
24 | ActionCable.server.instance_variable_set(:@pubsub, @__was_pubsub_adapter__)
25 | end
26 | end
27 |
28 | RSpec.configure do |config|
29 | config.include_context "action_cable:#{adapter}", action_cable: adapter.to_sym
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/spec/dummy/.rspec:
--------------------------------------------------------------------------------
1 | --require spec_helper
2 |
--------------------------------------------------------------------------------
/spec/dummy/app/channels/application_cable/channel.rb:
--------------------------------------------------------------------------------
1 | module ApplicationCable
2 | class Channel < ActionCable::Channel::Base
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/spec/dummy/app/channels/application_cable/connection.rb:
--------------------------------------------------------------------------------
1 | module ApplicationCable
2 | class Connection < ActionCable::Connection::Base
3 | identified_by :user_id
4 |
5 | def connect
6 | self.user_id = verify_user
7 | end
8 |
9 | def disconnect
10 | $stdout.puts "User #{user_id} disconnected"
11 | end
12 |
13 | private
14 |
15 | def verify_user
16 | user_id = request.params[:user_id] ||
17 | request.headers["x-user-id"] ||
18 | cookies.signed[:user_id] ||
19 | request.session[:user_id]
20 | reject_unauthorized_connection unless user_id.present?
21 | user_id
22 | end
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/spec/dummy/app/channels/chat_channel.rb:
--------------------------------------------------------------------------------
1 | class ChatChannel < ApplicationCable::Channel
2 | periodically every: 5.seconds do
3 | transmit action: :now, time: Time.now
4 | end
5 |
6 | def subscribed
7 | reject unless user_id.present?
8 |
9 | @room_id = params[:room_id]
10 |
11 | stream_from "chat_#{@room_id}" if @room_id.present?
12 | end
13 |
14 | def speak(data)
15 | ActionCable.server.broadcast(
16 | "chat_#{@room_id}", text: data['message'], user_id: user_id
17 | )
18 | end
19 |
20 | def leave
21 | @room_id = nil
22 | stop_all_streams
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/spec/dummy/app/channels/echo_channel.rb:
--------------------------------------------------------------------------------
1 | class EchoChannel < ApplicationCable::Channel
2 | def subscribed
3 | end
4 |
5 | def echo(data)
6 | data.delete("action")
7 | transmit data
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/spec/dummy/app/channels/user_channel.rb:
--------------------------------------------------------------------------------
1 | class UserChannel < ApplicationCable::Channel
2 | def subscribed
3 | user = User.new(params[:id])
4 | stream_for user
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/spec/dummy/app/models/broadcaster.rb:
--------------------------------------------------------------------------------
1 | class Broadcaster; end
2 |
--------------------------------------------------------------------------------
/spec/dummy/app/models/user.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class User
4 | include GlobalID::Identification
5 |
6 | attr_reader :id
7 |
8 | def initialize(id)
9 | @id = id
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/spec/dummy/bin/rails:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | APP_PATH = File.expand_path('../../config/application', __FILE__)
4 | require_relative '../config/boot'
5 | require 'rails/commands'
6 |
--------------------------------------------------------------------------------
/spec/dummy/config.ru:
--------------------------------------------------------------------------------
1 | # This file is used by Rack-based servers to start the application.
2 |
3 | require_relative 'config/environment'
4 |
5 | run Rails.application
6 |
--------------------------------------------------------------------------------
/spec/dummy/config/application.rb:
--------------------------------------------------------------------------------
1 | require_relative "boot"
2 | require "action_controller/railtie"
3 | require "action_cable/engine"
4 | require "global_id"
5 |
6 | GlobalID.app = "dummy"
7 |
8 | module Dummy
9 | class Application < Rails::Application
10 | # Settings in config/environments/* take precedence over those specified here.
11 | # Application configuration should go into files in config/initializers
12 | # -- all .rb files in that directory are automatically loaded.
13 |
14 | # Only loads a smaller set of middleware suitable for API only apps.
15 | # Middleware like session, flash, cookies can be added back manually.
16 | # Skip views, helpers and assets when generating a new resource.
17 | config.api_only = true
18 | config.logger = Logger.new(STDOUT)
19 | config.log_level = :fatal
20 | config.eager_load = false
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/spec/dummy/config/boot.rb:
--------------------------------------------------------------------------------
1 | # Set up gems listed in the Gemfile.
2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../Gemfile', __dir__)
3 |
4 | require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
5 | $LOAD_PATH.unshift File.expand_path('../../../lib', __dir__)
6 |
--------------------------------------------------------------------------------
/spec/dummy/config/cable.yml:
--------------------------------------------------------------------------------
1 | development:
2 | adapter: async
3 |
4 | test:
5 | adapter: test
6 |
7 | production:
8 | adapter: redis
9 | url: redis://localhost:6379/1
10 |
--------------------------------------------------------------------------------
/spec/dummy/config/environment.rb:
--------------------------------------------------------------------------------
1 | # Load the Rails application.
2 | require_relative 'application'
3 |
4 | # Initialize the Rails application.
5 | Rails.application.initialize!
6 |
--------------------------------------------------------------------------------
/spec/dummy/config/puma.rb:
--------------------------------------------------------------------------------
1 | # Puma can serve each request in a thread from an internal thread pool.
2 | # The `threads` method setting takes two numbers a minimum and maximum.
3 | # Any libraries that use thread pools should be configured to match
4 | # the maximum value specified for Puma. Default is set to 5 threads for minimum
5 | # and maximum, this matches the default thread size of Active Record.
6 | #
7 | threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }.to_i
8 | threads threads_count, threads_count
9 |
10 | # Specifies the `port` that Puma will listen on to receive requests, default is 3000.
11 | #
12 | port ENV.fetch("PORT") { 3000 }
13 |
14 | # Specifies the `environment` that Puma will run in.
15 | #
16 | environment ENV.fetch("RAILS_ENV") { "development" }
17 |
18 | # Specifies the number of `workers` to boot in clustered mode.
19 | # Workers are forked webserver processes. If using threads and workers together
20 | # the concurrency of the application would be max `threads` * `workers`.
21 | # Workers do not work on JRuby or Windows (both of which do not support
22 | # processes).
23 | #
24 | # workers ENV.fetch("WEB_CONCURRENCY") { 2 }
25 |
26 | # Use the `preload_app!` method when specifying a `workers` number.
27 | # This directive tells Puma to first boot the application and load code
28 | # before forking the application. This takes advantage of Copy On Write
29 | # process behavior so workers use less memory. If you use this option
30 | # you need to make sure to reconnect any threads in the `on_worker_boot`
31 | # block.
32 | #
33 | # preload_app!
34 |
35 | # The code in the `on_worker_boot` will be called if you are using
36 | # clustered mode by specifying a number of `workers`. After each worker
37 | # process is booted this block will be run, if you are using `preload_app!`
38 | # option you will want to use this block to reconnect to any threads
39 | # or connections that may have been created at application boot, Ruby
40 | # cannot share connections between processes.
41 | #
42 | # on_worker_boot do
43 | # ActiveRecord::Base.establish_connection if defined?(ActiveRecord)
44 | # end
45 |
46 | # Allow puma to be restarted by `rails restart` command.
47 | plugin :tmp_restart
48 |
--------------------------------------------------------------------------------
/spec/dummy/config/routes.rb:
--------------------------------------------------------------------------------
1 | Rails.application.routes.draw do
2 | # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
3 | end
4 |
--------------------------------------------------------------------------------
/spec/dummy/config/secrets.yml:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Your secret key is used for verifying the integrity of signed cookies.
4 | # If you change this key, all old signed cookies will become invalid!
5 |
6 | # Make sure the secret is at least 30 characters and all random,
7 | # no regular words or you'll be exposed to dictionary attacks.
8 | # You can use `rails secret` to generate a secure secret key.
9 |
10 | # Make sure the secrets in this file are kept private
11 | # if you're sharing your code publicly.
12 |
13 | development:
14 | secret_key_base: 7e2f9ba6bdde6c08b1be9c8389ea715acd61506712f44b28ee2570267249dd3ecf84d4fada6516a869b834a90d876bff61cce637a7645e815afb1c1501262e4e
15 |
16 | test:
17 | secret_key_base: e304849faefdc1a5e10b31a030e97eadb723a60593e6f53e8c99ed8bf6e31af8c80e4a0fe4814fafc5a8029669e07b323f3a89fbca7e9c9b5aa6eb3ca51e84e1
18 |
19 | # Do not keep production secrets in the repository,
20 | # instead read values from the environment.
21 | production:
22 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>
23 |
--------------------------------------------------------------------------------
/spec/dummy/spec/rails_helper.rb:
--------------------------------------------------------------------------------
1 | # This file is copied to spec/ when you run 'rails generate rspec:install'
2 | require 'spec_helper'
3 | ENV['RAILS_ENV'] ||= 'test'
4 | require File.expand_path('../../config/environment', __FILE__)
5 | # Prevent database truncation if the environment is production
6 | abort("The Rails environment is running in production mode!") if Rails.env.production?
7 | require "action_cable/testing/rspec"
8 |
9 | # Add additional requires below this line. Rails is not loaded until this point!
10 |
11 | # Requires supporting ruby files with custom matchers and macros, etc, in
12 | # spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are
13 | # run as spec files by default. This means that files in spec/support that end
14 | # in _spec.rb will both be required and run as specs, causing the specs to be
15 | # run twice. It is recommended that you do not name files matching this glob to
16 | # end with _spec.rb. You can configure this pattern with the --pattern
17 | # option on the command line or in ~/.rspec, .rspec or `.rspec-local`.
18 | #
19 | # The following line is provided for convenience purposes. It has the downside
20 | # of increasing the boot-up time by auto-requiring all files in the support
21 | # directory. Alternatively, in the individual `*_spec.rb` files, manually
22 | # require only the support files necessary.
23 | #
24 | # Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }
25 |
26 | RSpec.configure do |config|
27 | # RSpec Rails can automatically mix in different behaviours to your tests
28 | # based on their file location, for example enabling you to call `get` and
29 | # `post` in specs under `spec/controllers`.
30 | #
31 | # You can disable this behaviour by removing the line below, and instead
32 | # explicitly tag your specs with their type, e.g.:
33 | #
34 | # RSpec.describe UsersController, :type => :controller do
35 | # # ...
36 | # end
37 | #
38 | # The different available types are documented in the features, such as in
39 | # https://relishapp.com/rspec/rspec-rails/docs
40 | config.infer_spec_type_from_file_location!
41 |
42 | # Filter lines from Rails gems in backtraces.
43 | config.filter_rails_from_backtrace!
44 | # arbitrary gems may also be filtered via:
45 | # config.filter_gems_from_backtrace("gem name")
46 | end
47 |
--------------------------------------------------------------------------------
/spec/dummy/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | # This file was generated by the `rails generate rspec:install` command. Conventionally, all
2 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
3 | # The generated `.rspec` file contains `--require spec_helper` which will cause
4 | # this file to always be loaded, without a need to explicitly require it in any
5 | # files.
6 | #
7 | # Given that it is always loaded, you are encouraged to keep this file as
8 | # light-weight as possible. Requiring heavyweight dependencies from this file
9 | # will add to the boot time of your test suite on EVERY test run, even for an
10 | # individual file that may not need all of that loaded. Instead, consider making
11 | # a separate helper file that requires the additional dependencies and performs
12 | # the additional setup, and require it from the spec files that actually need
13 | # it.
14 | #
15 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
16 | RSpec.configure do |config|
17 | # rspec-expectations config goes here. You can use an alternate
18 | # assertion/expectation library such as wrong or the stdlib/minitest
19 | # assertions if you prefer.
20 | config.expect_with :rspec do |expectations|
21 | # This option will default to `true` in RSpec 4. It makes the `description`
22 | # and `failure_message` of custom matchers include text for helper methods
23 | # defined using `chain`, e.g.:
24 | # be_bigger_than(2).and_smaller_than(4).description
25 | # # => "be bigger than 2 and smaller than 4"
26 | # ...rather than:
27 | # # => "be bigger than 2"
28 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true
29 | end
30 |
31 | # rspec-mocks config goes here. You can use an alternate test double
32 | # library (such as bogus or mocha) by changing the `mock_with` option here.
33 | config.mock_with :rspec do |mocks|
34 | # Prevents you from mocking or stubbing a method that does not exist on
35 | # a real object. This is generally recommended, and will default to
36 | # `true` in RSpec 4.
37 | mocks.verify_partial_doubles = true
38 | end
39 |
40 | # This option will default to `:apply_to_host_groups` in RSpec 4 (and will
41 | # have no way to turn it off -- the option exists only for backwards
42 | # compatibility in RSpec 3). It causes shared context metadata to be
43 | # inherited by the metadata hash of host groups and examples, rather than
44 | # triggering implicit auto-inclusion in groups with matching metadata.
45 | config.shared_context_metadata_behavior = :apply_to_host_groups
46 |
47 | # The settings below are suggested to provide a good initial experience
48 | # with RSpec, but feel free to customize to your heart's content.
49 | =begin
50 | # This allows you to limit a spec run to individual examples or groups
51 | # you care about by tagging them with `:focus` metadata. When nothing
52 | # is tagged with `:focus`, all examples get run. RSpec also provides
53 | # aliases for `it`, `describe`, and `context` that include `:focus`
54 | # metadata: `fit`, `fdescribe` and `fcontext`, respectively.
55 | config.filter_run_when_matching :focus
56 |
57 | # Allows RSpec to persist some state between runs in order to support
58 | # the `--only-failures` and `--next-failure` CLI options. We recommend
59 | # you configure your source control system to ignore this file.
60 | config.example_status_persistence_file_path = "spec/examples.txt"
61 |
62 | # Limits the available syntax to the non-monkey patched syntax that is
63 | # recommended. For more details, see:
64 | # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/
65 | # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
66 | # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode
67 | config.disable_monkey_patching!
68 |
69 | # Many RSpec users commonly either run the entire suite or an individual
70 | # file, and it's useful to allow more verbose output when running an
71 | # individual spec file.
72 | if config.files_to_run.one?
73 | # Use the documentation formatter for detailed output,
74 | # unless a formatter has already been configured
75 | # (e.g. via a command-line flag).
76 | config.default_formatter = "doc"
77 | end
78 |
79 | # Print the 10 slowest examples and example groups at the
80 | # end of the spec run, to help surface which specs are running
81 | # particularly slow.
82 | config.profile_examples = 10
83 |
84 | # Run specs in random order to surface order dependencies. If you find an
85 | # order dependency and want to debug it, you can fix the order by providing
86 | # the seed, which is printed after each run.
87 | # --seed 1234
88 | config.order = :random
89 |
90 | # Seed global randomization in this process using the `--seed` CLI option.
91 | # Setting this allows you to use `--seed` to deterministically reproduce
92 | # test failures related to randomization by passing the same `--seed` value
93 | # as the one that triggered the failure.
94 | Kernel.srand config.seed
95 | =end
96 | end
97 |
--------------------------------------------------------------------------------
/spec/generators/rspec_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 | require "generators/rspec/channel/channel_generator"
5 |
6 | describe Rspec::Generators::ChannelGenerator, type: :generator do
7 | destination File.expand_path("../../../tmp", __FILE__)
8 |
9 | let(:args) { ["chat"] }
10 |
11 | before do
12 | prepare_destination
13 | run_generator(args)
14 | end
15 |
16 | subject { file("spec/channels/chat_channel_spec.rb") }
17 |
18 | it "creates script", :aggregate_failures do
19 | is_expected.to exist
20 | is_expected.to contain("require 'rails_helper'")
21 | is_expected.to contain("RSpec.describe ChatChannel, type: :channel")
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/spec/generators/test_unit_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 | require "generators/test_unit/channel/channel_generator"
5 |
6 | describe TestUnit::Generators::ChannelGenerator, type: :generator do
7 | destination File.expand_path("../../../tmp", __FILE__)
8 |
9 | let(:args) { ["chat"] }
10 |
11 | before do
12 | prepare_destination
13 | run_generator(args)
14 | end
15 |
16 | subject { file("test/channels/chat_channel_test.rb") }
17 |
18 | it "creates script", :aggregate_failures do
19 | is_expected.to exist
20 | is_expected.to contain("class ChatChannelTest < ActionCable::Channel::TestCase")
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/spec/rspec/rails/channel_example_group_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | module RSpec::Rails
4 | describe ChannelExampleGroup do
5 | if defined?(ActionCable)
6 | it_behaves_like "an rspec-rails example group mixin", :channel,
7 | './spec/channels/', '.\\spec\\channels\\'
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/spec/rspec/rails/matchers/action_cable/have_broadcasted_to_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 | require "rspec/rails/feature_check"
3 |
4 | if RSpec::Rails::FeatureCheck.has_action_cable_testing?
5 | require "rspec/rails/matchers/action_cable"
6 | end
7 |
8 | RSpec.describe "have_broadcasted_to matchers", :skip => !RSpec::Rails::FeatureCheck.has_action_cable_testing? do
9 | let(:channel) do
10 | Class.new(ActionCable::Channel::Base) do
11 | def self.channel_name
12 | "broadcast"
13 | end
14 | end
15 | end
16 |
17 | def broadcast(stream, msg)
18 | ActionCable.server.broadcast stream, msg
19 | end
20 |
21 | before do
22 | server = ActionCable.server
23 | test_adapter = ActionCable::SubscriptionAdapter::Test.new(server)
24 | server.instance_variable_set(:@pubsub, test_adapter)
25 | end
26 |
27 | describe "have_broadcasted_to" do
28 | it "raises ArgumentError when no Proc passed to expect" do
29 | expect {
30 | expect(true).to have_broadcasted_to('stream')
31 | }.to raise_error(ArgumentError)
32 | end
33 |
34 | it "passes with default messages count (exactly one)" do
35 | expect {
36 | broadcast('stream', 'hello')
37 | }.to have_broadcasted_to('stream')
38 | end
39 |
40 | it "passes when using alias" do
41 | expect {
42 | broadcast('stream', 'hello')
43 | }.to broadcast_to('stream')
44 | end
45 |
46 | it "counts only messages sent in block" do
47 | broadcast('stream', 'one')
48 | expect {
49 | broadcast('stream', 'two')
50 | }.to have_broadcasted_to('stream').exactly(1)
51 | end
52 |
53 | it "passes when negated" do
54 | expect { }.not_to have_broadcasted_to('stream')
55 | end
56 |
57 | it "fails when message is not sent" do
58 | expect {
59 | expect { }.to have_broadcasted_to('stream')
60 | }.to raise_error(/expected to broadcast exactly 1 messages to stream, but broadcast 0/)
61 | end
62 |
63 | it "fails when too many messages broadcast" do
64 | expect {
65 | expect {
66 | broadcast('stream', 'one')
67 | broadcast('stream', 'two')
68 | }.to have_broadcasted_to('stream').exactly(1)
69 | }.to raise_error(/expected to broadcast exactly 1 messages to stream, but broadcast 2/)
70 | end
71 |
72 | it "reports correct number in fail error message" do
73 | broadcast('stream', 'one')
74 | expect {
75 | expect { }.to have_broadcasted_to('stream').exactly(1)
76 | }.to raise_error(/expected to broadcast exactly 1 messages to stream, but broadcast 0/)
77 | end
78 |
79 | it "fails when negated and message is sent" do
80 | expect {
81 | expect { broadcast('stream', 'one') }.not_to have_broadcasted_to('stream')
82 | }.to raise_error(/expected not to broadcast exactly 1 messages to stream, but broadcast 1/)
83 | end
84 |
85 | it "passes with multiple streams" do
86 | expect {
87 | broadcast('stream_a', 'A')
88 | broadcast('stream_b', 'B')
89 | broadcast('stream_c', 'C')
90 | }.to have_broadcasted_to('stream_a').and have_broadcasted_to('stream_b')
91 | end
92 |
93 | it "passes with :once count" do
94 | expect {
95 | broadcast('stream', 'one')
96 | }.to have_broadcasted_to('stream').exactly(:once)
97 | end
98 |
99 | it "passes with :twice count" do
100 | expect {
101 | broadcast('stream', 'one')
102 | broadcast('stream', 'two')
103 | }.to have_broadcasted_to('stream').exactly(:twice)
104 | end
105 |
106 | it "passes with :thrice count" do
107 | expect {
108 | broadcast('stream', 'one')
109 | broadcast('stream', 'two')
110 | broadcast('stream', 'three')
111 | }.to have_broadcasted_to('stream').exactly(:thrice)
112 | end
113 |
114 | it "passes with at_least count when sent messages are over limit" do
115 | expect {
116 | broadcast('stream', 'one')
117 | broadcast('stream', 'two')
118 | }.to have_broadcasted_to('stream').at_least(:once)
119 | end
120 |
121 | it "passes with at_most count when sent messages are under limit" do
122 | expect {
123 | broadcast('stream', 'hello')
124 | }.to have_broadcasted_to('stream').at_most(:once)
125 | end
126 |
127 | it "generates failure message with at least hint" do
128 | expect {
129 | expect { }.to have_broadcasted_to('stream').at_least(:once)
130 | }.to raise_error(/expected to broadcast at least 1 messages to stream, but broadcast 0/)
131 | end
132 |
133 | it "generates failure message with at most hint" do
134 | expect {
135 | expect {
136 | broadcast('stream', 'hello')
137 | broadcast('stream', 'hello')
138 | }.to have_broadcasted_to('stream').at_most(:once)
139 | }.to raise_error(/expected to broadcast at most 1 messages to stream, but broadcast 2/)
140 | end
141 |
142 | it "passes with provided data" do
143 | expect {
144 | broadcast('stream', id: 42, name: "David")
145 | }.to have_broadcasted_to('stream').with(id: 42, name: "David")
146 | end
147 |
148 | it "passes with provided data matchers" do
149 | expect {
150 | broadcast('stream', id: 42, name: "David", message_id: 123)
151 | }.to have_broadcasted_to('stream').with(a_hash_including(name: "David", id: 42))
152 | end
153 |
154 | it "generates failure message when data not match" do
155 | expect {
156 | expect {
157 | broadcast('stream', id: 42, name: "David", message_id: 123)
158 | }.to have_broadcasted_to('stream').with(a_hash_including(name: "John", id: 42))
159 | }.to raise_error(/expected to broadcast exactly 1 messages to stream with a hash including/)
160 | end
161 |
162 | it "throws descriptive error when no test adapter set" do
163 | require "action_cable/subscription_adapter/inline"
164 | ActionCable.server.instance_variable_set(:@pubsub, ActionCable::SubscriptionAdapter::Inline)
165 | expect {
166 | expect { broadcast('stream', 'hello') }.to have_broadcasted_to('stream')
167 | }.to raise_error("To use ActionCable matchers set `adapter: test` in your cable.yml")
168 | end
169 |
170 | it "fails with with block with incorrect data" do
171 | expect {
172 | expect {
173 | broadcast('stream', "asdf")
174 | }.to have_broadcasted_to('stream').with { |data|
175 | expect(data).to eq("zxcv")
176 | }
177 | }.to raise_error { |e|
178 | expect(e.message).to match(/expected: "zxcv"/)
179 | expect(e.message).to match(/got: "asdf"/)
180 | }
181 | end
182 |
183 |
184 | context "when object is passed as first argument" do
185 | let(:user) { User.new(42) }
186 |
187 | context "when channel is present" do
188 | it "passes" do
189 | expect {
190 | channel.broadcast_to(user, text: 'Hi')
191 | }.to have_broadcasted_to(user).from_channel(channel)
192 | end
193 | end
194 |
195 | context "when channel can't be infered" do
196 | it "raises exception" do
197 | expect {
198 | expect {
199 | channel.broadcast_to(user, text: 'Hi')
200 | }.to have_broadcasted_to(user)
201 | }.to raise_error(ArgumentError)
202 | end
203 | end
204 | end
205 | end
206 | end
207 |
--------------------------------------------------------------------------------
/spec/rspec/rails/matchers/action_cable/have_stream_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 | require "rspec/rails/feature_check"
3 |
4 | module FakeChannelHelper
5 | extend ActiveSupport::Concern
6 |
7 | module ClassMethods
8 | # @private
9 | def channel_class
10 | Class.new(ActionCable::Channel::Base) do
11 | def subscribed
12 | stream_from "chat_#{params[:id]}" if params[:id]
13 | stream_for User.new(params[:user]) if params[:user]
14 | end
15 |
16 | def self.channel_name
17 | "broadcast"
18 | end
19 | end
20 | end
21 | end
22 | end
23 |
24 | RSpec.describe "have_streams matchers" do
25 | include RSpec::Rails::ChannelExampleGroup
26 | include FakeChannelHelper
27 |
28 | before { stub_connection }
29 |
30 | describe "have_streams" do
31 | it "raises when no subscription started" do
32 | expect {
33 | expect(subscription).to have_streams
34 | }.to raise_error(/Must be subscribed!/)
35 | end
36 |
37 | it "does not allow usage" do
38 | subscribe
39 |
40 | expect {
41 | expect(subscription).to have_streams
42 | }.to raise_error(ArgumentError, /have_streams is used for negated expectations only/)
43 | end
44 |
45 | context "with negated form" do
46 | it "raises when no subscription started" do
47 | expect {
48 | expect(subscription).not_to have_streams
49 | }.to raise_error(/Must be subscribed!/)
50 | end
51 |
52 | it "raises ArgumentError when no subscription passed to expect" do
53 | subscribe id: 1
54 |
55 | expect {
56 | expect(true).not_to have_streams
57 | }.to raise_error(ArgumentError)
58 | end
59 |
60 | it "passes with negated form" do
61 | subscribe
62 |
63 | expect(subscription).not_to have_streams
64 | end
65 |
66 | it "fails with message" do
67 | subscribe id: 1
68 |
69 | expect {
70 | expect(subscription).not_to have_streams
71 | }.to raise_error(/expected not to have any stream started/)
72 | end
73 | end
74 | end
75 |
76 | describe "have_stream_from" do
77 | it "raises when no subscription started" do
78 | expect {
79 | expect(subscription).to have_stream_from("stream")
80 | }.to raise_error(/Must be subscribed!/)
81 | end
82 |
83 | it "raises ArgumentError when no subscription passed to expect" do
84 | subscribe id: 1
85 |
86 | expect {
87 | expect(true).to have_stream_from("stream")
88 | }.to raise_error(ArgumentError)
89 | end
90 |
91 | it "passes" do
92 | subscribe id: 1
93 |
94 | expect(subscription).to have_stream_from("chat_1")
95 | end
96 |
97 | it "fails with message" do
98 | subscribe id: 1
99 |
100 | expect {
101 | expect(subscription).to have_stream_from("chat_2")
102 | }.to raise_error(/expected to have stream "chat_2" started, but have \[\"chat_1\"\]/)
103 | end
104 |
105 | context "with negated form" do
106 | it "passes" do
107 | subscribe id: 1
108 |
109 | expect(subscription).not_to have_stream_from("chat_2")
110 | end
111 |
112 | it "fails with message" do
113 | subscribe id: 1
114 |
115 | expect {
116 | expect(subscription).not_to have_stream_from("chat_1")
117 | }.to raise_error(/expected not to have stream "chat_1" started, but have \[\"chat_1\"\]/)
118 | end
119 | end
120 |
121 | context "with composable matcher" do
122 | it "passes" do
123 | subscribe id: 1
124 |
125 | expect(subscription).to have_stream_from(a_string_starting_with("chat"))
126 | end
127 |
128 | it "fails with message" do
129 | subscribe id: 1
130 |
131 | expect {
132 | expect(subscription).to have_stream_from(a_string_starting_with("room"))
133 | }.to raise_error(/expected to have stream a string starting with "room" started, but have \[\"chat_1\"\]/)
134 | end
135 | end
136 | end
137 |
138 | describe "have_stream_for" do
139 | it "raises when no subscription started" do
140 | expect {
141 | expect(subscription).to have_stream_for(User.new(42))
142 | }.to raise_error(/Must be subscribed!/)
143 | end
144 |
145 | it "raises ArgumentError when no subscription passed to expect" do
146 | subscribe user: 42
147 |
148 | expect {
149 | expect(true).to have_stream_for(User.new(42))
150 | }.to raise_error(ArgumentError)
151 | end
152 |
153 | it "passes" do
154 | subscribe user: 42
155 |
156 | expect(subscription).to have_stream_for(User.new(42))
157 | end
158 |
159 | it "fails with message" do
160 | subscribe user: 42
161 |
162 | expect {
163 | expect(subscription).to have_stream_for(User.new(31337))
164 | }.to raise_error(/expected to have stream "broadcast:User#31337" started, but have \[\"broadcast:User#42\"\]/)
165 | end
166 |
167 | context "with negated form" do
168 | it "passes" do
169 | subscribe user: 42
170 |
171 | expect(subscription).not_to have_stream_for(User.new(31337))
172 | end
173 |
174 | it "fails with message" do
175 | subscribe user: 42
176 |
177 | expect {
178 | expect(subscription).not_to have_stream_for(User.new(42))
179 | }.to raise_error(/expected not to have stream "broadcast:User#42" started, but have \[\"broadcast:User#42\"\]/)
180 | end
181 | end
182 | end
183 |
184 | if DeprecatedApi.enabled?
185 | specify { expect(self).to respond_to(:streams) }
186 |
187 | it "passes" do
188 | subscribe id: 1
189 |
190 | ActiveSupport::Deprecation.silence do
191 | expect(streams).to include("chat_1")
192 | end
193 | end
194 | else
195 | specify { expect(self).not_to respond_to(:streams) }
196 | end
197 | end
198 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | $LOAD_PATH.unshift File.expand_path("../../lib", __FILE__)
4 |
5 | begin
6 | require "pry-byebug"
7 | rescue LoadError
8 | end
9 |
10 | require "action_controller/railtie"
11 | require "action_view/railtie"
12 | require "action_cable"
13 |
14 | require "action_cable/testing/rspec"
15 |
16 | require "ammeter/init"
17 |
18 | # Require all the stubs and models
19 | Dir[File.expand_path("../test/stubs/*.rb", __dir__)].each { |file| require file }
20 |
21 | # # Set test adapter and logger
22 | ActionCable.server.config.cable = { "adapter" => "test" }
23 | ActionCable.server.config.logger =
24 | ActiveSupport::TaggedLogging.new ActiveSupport::Logger.new(StringIO.new)
25 |
26 | Dir[File.expand_path("support/**/*.rb", __dir__)].each { |f| require f }
27 |
28 | RSpec.configure do |config|
29 | config.mock_with :rspec do |mocks|
30 | mocks.verify_partial_doubles = true
31 | end
32 |
33 | config.example_status_persistence_file_path = "tmp/rspec_examples.txt"
34 | config.filter_run :focus
35 | config.run_all_when_everything_filtered = true
36 |
37 | config.order = :random
38 | Kernel.srand config.seed
39 | end
40 |
--------------------------------------------------------------------------------
/spec/support/deprecated_api.rb:
--------------------------------------------------------------------------------
1 | DeprecatedApi = Module.new do
2 | def self.enabled?
3 | ActionCable::VERSION::MAJOR < 6 && (
4 | Gem::Version.new(ActionCable::Testing::VERSION) < Gem::Version.new("1.0") ||
5 | raise("Deprecated API should be removed")
6 | )
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/spec/support/helpers.rb:
--------------------------------------------------------------------------------
1 | # Copied from rspec-rails
2 |
3 | module Helpers
4 | include RSpec::Rails::FeatureCheck
5 |
6 | def with_isolated_config
7 | original_config = RSpec.configuration
8 | RSpec.configuration = RSpec::Core::Configuration.new
9 | RSpec::Rails.initialize_configuration(RSpec.configuration)
10 |
11 | if defined?(ActionCable)
12 | RSpec.configure do |config|
13 | config.include RSpec::Rails::ChannelExampleGroup, type: :channel
14 | end
15 | end
16 |
17 | yield RSpec.configuration
18 | ensure
19 | RSpec.configuration = original_config
20 | end
21 |
22 | RSpec.configure { |c| c.include self }
23 | end
24 |
--------------------------------------------------------------------------------
/spec/support/shared_examples.rb:
--------------------------------------------------------------------------------
1 | # Copied from rspec-rails
2 |
3 | require 'pathname'
4 |
5 | shared_examples_for "an rspec-rails example group mixin" do |type, *paths|
6 | let(:mixin) { described_class }
7 |
8 | def define_group_in(path, group_definition)
9 | path = Pathname(path)
10 | $_new_group = nil
11 | begin
12 | file = path + "whatever_spec.rb"
13 |
14 | Dir.mktmpdir("rspec-rails-app-root") do |dir|
15 | Dir.chdir(dir) do
16 | path.mkpath
17 | File.open(file, "w") do |f|
18 | f.write("$_new_group = #{group_definition}")
19 | end
20 |
21 | load file
22 | end
23 | end
24 |
25 | group = $_new_group
26 | return group
27 | ensure
28 | $_new_group = nil
29 | end
30 | end
31 |
32 | around(:example) do |ex|
33 | with_isolated_config(&ex)
34 | end
35 |
36 | it "adds does not add `:type` metadata on inclusion" do
37 | mixin = self.mixin
38 | group = RSpec.describe { include mixin }
39 | expect(group.metadata).not_to include(:type)
40 | end
41 |
42 | context 'when `infer_spec_type_from_file_location!` is configured' do
43 | before { RSpec.configuration.infer_spec_type_from_file_location! }
44 |
45 | paths.each do |path|
46 | context "for an example group defined in a file in the #{path} directory" do
47 | it "includes itself in the example group" do
48 | group = define_group_in path, "RSpec.describe"
49 | expect(group.included_modules).to include(mixin)
50 | end
51 |
52 | it "tags groups in that directory with `:type => #{type.inspect}`" do
53 | group = define_group_in path, "RSpec.describe"
54 | expect(group.metadata).to include(:type => type)
55 | end
56 |
57 | it "allows users to override the type" do
58 | group = define_group_in path, "RSpec.describe 'group', :type => :other"
59 | expect(group.metadata).to include(:type => :other)
60 | expect(group.included_modules).not_to include(mixin)
61 | end
62 |
63 | it "applies configured `before(:context)` hooks with `:type => #{type.inspect}` metadata" do
64 | block_run = false
65 | RSpec.configuration.before(:context, :type => type) { block_run = true }
66 |
67 | group = define_group_in path, "RSpec.describe('group') { it { } }"
68 | group.run(double.as_null_object)
69 |
70 | expect(block_run).to eq(true)
71 | end
72 | end
73 | end
74 |
75 | it "includes itself in example groups tagged with `:type => #{type.inspect}`" do
76 | group = define_group_in "spec/other", "RSpec.describe 'group', :type => #{type.inspect}"
77 | expect(group.included_modules).to include(mixin)
78 | end
79 | end
80 |
81 | context 'when `infer_spec_type_from_file_location!` is not configured' do
82 | it "includes itself in example groups tagged with `:type => #{type.inspect}`" do
83 | group = define_group_in "spec/other", "RSpec.describe 'group', :type => #{type.inspect}"
84 | expect(group.included_modules).to include(mixin)
85 | end
86 |
87 | paths.each do |path|
88 | context "for an example group defined in a file in the #{path} directory" do
89 | it "does not include itself in the example group" do
90 | group = define_group_in path, "RSpec.describe"
91 | expect(group.included_modules).not_to include(mixin)
92 | end
93 |
94 | it "does not tag groups in that directory with `:type => #{type.inspect}`" do
95 | group = define_group_in path, "RSpec.describe"
96 | expect(group.metadata).not_to include(:type)
97 | end
98 | end
99 | end
100 | end
101 | end
--------------------------------------------------------------------------------
/test/channel/test_case_test.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require_relative "../test_helper"
4 |
5 | class TestTestChannel < ActionCable::Channel::Base
6 | end
7 |
8 | class CrazyNameChannelTest < ActionCable::Channel::TestCase
9 | tests TestTestChannel
10 |
11 | def test_set_channel_class_manual
12 | assert_equal TestTestChannel, self.class.channel_class
13 | end
14 | end
15 |
16 | class CrazySymbolNameChannelTest < ActionCable::Channel::TestCase
17 | tests :test_test_channel
18 |
19 | def test_set_channel_class_manual_using_symbol
20 | assert_equal TestTestChannel, self.class.channel_class
21 | end
22 | end
23 |
24 | class CrazyStringNameChannelTest < ActionCable::Channel::TestCase
25 | tests "test_test_channel"
26 |
27 | def test_set_channel_class_manual_using_string
28 | assert_equal TestTestChannel, self.class.channel_class
29 | end
30 | end
31 |
32 | class SubscriptionsTestChannel < ActionCable::Channel::Base
33 | end
34 |
35 | class SubscriptionsTestChannelTest < ActionCable::Channel::TestCase
36 | def setup
37 | stub_connection
38 | end
39 |
40 | def test_no_subscribe
41 | assert_nil subscription
42 | end
43 |
44 | def test_subscribe
45 | subscribe
46 |
47 | assert subscription.confirmed?
48 | assert_not subscription.rejected?
49 | assert_equal 1, connection.transmissions.size
50 | assert_equal ActionCable::INTERNAL[:message_types][:confirmation],
51 | connection.transmissions.last["type"]
52 | end
53 | end
54 |
55 | class StubConnectionTest < ActionCable::Channel::TestCase
56 | tests SubscriptionsTestChannel
57 |
58 | def test_connection_identifiers
59 | stub_connection username: "John", admin: true
60 |
61 | subscribe
62 |
63 | assert_equal "John", subscription.username
64 | assert subscription.admin
65 | end
66 | end
67 |
68 | class RejectionTestChannel < ActionCable::Channel::Base
69 | def subscribed
70 | reject
71 | end
72 | end
73 |
74 | class RejectionTestChannelTest < ActionCable::Channel::TestCase
75 | def test_rejection
76 | subscribe
77 |
78 | assert_not subscription.confirmed?
79 | assert subscription.rejected?
80 | assert_equal 1, connection.transmissions.size
81 | assert_equal ActionCable::INTERNAL[:message_types][:rejection],
82 | connection.transmissions.last["type"]
83 | end
84 | end
85 |
86 | class StreamsTestChannel < ActionCable::Channel::Base
87 | def subscribed
88 | stream_from "test_#{params[:id] || 0}"
89 | end
90 | end
91 |
92 | class StreamsTestChannelTest < ActionCable::Channel::TestCase
93 | def test_stream_without_params
94 | subscribe
95 |
96 | assert_has_stream "test_0"
97 | end
98 |
99 | def test_stream_with_params
100 | subscribe id: 42
101 |
102 | assert_has_stream "test_42"
103 | end
104 |
105 | if DeprecatedApi.enabled?
106 | def test_deprecated_streams
107 | subscribe id: 42
108 |
109 | ActiveSupport::Deprecation.silence do
110 | assert "test_42", streams.last
111 | end
112 | end
113 | else
114 | def test_deprecated_streams
115 | assert_not_respond_to self, :streams
116 | end
117 | end
118 | end
119 |
120 | class StreamsForTestChannel < ActionCable::Channel::Base
121 | def subscribed
122 | stream_for User.new(params[:id])
123 | end
124 | end
125 |
126 | class StreamsForTestChannelTest < ActionCable::Channel::TestCase
127 | def test_stream_with_params
128 | subscribe id: 42
129 |
130 | assert_has_stream_for User.new(42)
131 | end
132 | end
133 |
134 | class NoStreamsTestChannel < ActionCable::Channel::Base
135 | def subscribed; end # no-op
136 | end
137 |
138 | class NoStreamsTestChannelTest < ActionCable::Channel::TestCase
139 | def test_stream_with_params
140 | subscribe
141 |
142 | assert_no_streams
143 | end
144 | end
145 |
146 | class PerformTestChannel < ActionCable::Channel::Base
147 | def echo(data)
148 | data.delete("action")
149 | transmit data
150 | end
151 |
152 | def ping
153 | transmit type: "pong"
154 | end
155 | end
156 |
157 | class PerformTestChannelTest < ActionCable::Channel::TestCase
158 | def setup
159 | stub_connection user_id: 2016
160 | subscribe id: 5
161 | end
162 |
163 | def test_perform_with_params
164 | perform :echo, text: "You are man!"
165 |
166 | assert_equal({ "text" => "You are man!" }, transmissions.last)
167 | end
168 |
169 | def test_perform_and_transmit
170 | perform :ping
171 |
172 | assert_equal "pong", transmissions.last["type"]
173 | end
174 | end
175 |
176 | class PerformUnsubscribedTestChannelTest < ActionCable::Channel::TestCase
177 | tests PerformTestChannel
178 |
179 | def test_perform_when_unsubscribed
180 | assert_raises do
181 | perform :echo
182 | end
183 | end
184 | end
185 |
186 | class BroadcastsTestChannel < ActionCable::Channel::Base
187 | def broadcast(data)
188 | ActionCable.server.broadcast(
189 | "broadcast_#{params[:id]}",
190 | text: data["message"], user_id: user_id
191 | )
192 | end
193 |
194 | def broadcast_to_user(data)
195 | user = User.new user_id
196 |
197 | self.class.broadcast_to user, text: data["message"]
198 | end
199 | end
200 |
201 | class BroadcastsTestChannelTest < ActionCable::Channel::TestCase
202 | def setup
203 | stub_connection user_id: 2017
204 | subscribe id: 5
205 | end
206 |
207 | def test_broadcast_matchers_included
208 | assert_broadcast_on("broadcast_5", user_id: 2017, text: "SOS") do
209 | perform :broadcast, message: "SOS"
210 | end
211 | end
212 |
213 | def test_broadcast_to_object
214 | user = User.new(2017)
215 |
216 | assert_broadcasts(user, 1) do
217 | perform :broadcast_to_user, text: "SOS"
218 | end
219 | end
220 |
221 | def test_broadcast_to_object_with_data
222 | user = User.new(2017)
223 |
224 | assert_broadcast_on(user, text: "SOS") do
225 | perform :broadcast_to_user, message: "SOS"
226 | end
227 | end
228 | end
229 |
--------------------------------------------------------------------------------
/test/connection/test_case_test.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require_relative "../test_helper"
4 |
5 | class SimpleConnection < ActionCable::Connection::Base
6 | identified_by :user_id
7 |
8 | class << self
9 | attr_accessor :disconnected_user_id
10 | end
11 |
12 | def connect
13 | self.user_id = request.params[:user_id] || cookies[:user_id]
14 | end
15 |
16 | def disconnect
17 | self.class.disconnected_user_id = user_id
18 | end
19 | end
20 |
21 | class ConnectionSimpleTest < ActionCable::Connection::TestCase
22 | tests SimpleConnection
23 |
24 | def test_connected
25 | connect
26 |
27 | assert_nil connection.user_id
28 | end
29 |
30 | def test_url_params
31 | connect "/cable?user_id=323"
32 |
33 | assert_equal "323", connection.user_id
34 | end
35 |
36 | def test_plain_cookie
37 | cookies[:user_id] = "456"
38 |
39 | connect
40 |
41 | assert_equal "456", connection.user_id
42 | end
43 |
44 | if DeprecatedApi.enabled?
45 | def test_deprecated_cookie
46 | ActiveSupport::Deprecation.silence do
47 | connect cookies: { user_id: "456" }
48 | end
49 |
50 | assert_equal "456", connection.user_id
51 | end
52 | end
53 |
54 | def test_disconnect
55 | cookies[:user_id] = "456"
56 | connect
57 |
58 | assert_equal "456", connection.user_id
59 |
60 | disconnect
61 |
62 | assert_equal "456", SimpleConnection.disconnected_user_id
63 | end
64 | end
65 |
66 | class Connection < ActionCable::Connection::Base
67 | identified_by :current_user_id
68 | identified_by :token
69 |
70 | class << self
71 | attr_accessor :disconnected_user_id
72 | end
73 |
74 | def connect
75 | self.current_user_id = verify_user
76 | self.token = request.headers["X-API-TOKEN"]
77 | logger.add_tags("ActionCable")
78 | end
79 |
80 | private
81 |
82 | def verify_user
83 | reject_unauthorized_connection unless cookies.signed[:user_id].present?
84 | cookies.signed[:user_id]
85 | end
86 | end
87 |
88 | class ConnectionTest < ActionCable::Connection::TestCase
89 | def test_connected_with_signed_cookies_and_headers
90 | cookies.signed[:user_id] = "1123"
91 |
92 | connect headers: { "X-API-TOKEN" => "abc" }
93 |
94 | assert_equal "abc", connection.token
95 | assert_equal "1123", connection.current_user_id
96 | end
97 |
98 | def test_connection_rejected
99 | assert_reject_connection { connect }
100 | end
101 | end
102 |
103 | class EncryptedCookiesConnection < ActionCable::Connection::Base
104 | identified_by :user_id
105 |
106 | def connect
107 | self.user_id = verify_user
108 | end
109 |
110 | private
111 |
112 | def verify_user
113 | reject_unauthorized_connection unless cookies.encrypted[:user_id].present?
114 | cookies.encrypted[:user_id]
115 | end
116 | end
117 |
118 | class EncryptedCookiesConnectionTest < ActionCable::Connection::TestCase
119 | tests EncryptedCookiesConnection
120 |
121 | def test_connected_with_encrypted_cookies
122 | cookies.encrypted[:user_id] = "789"
123 | connect
124 | assert_equal "789", connection.user_id
125 | end
126 |
127 | def test_connection_rejected
128 | assert_reject_connection { connect }
129 | end
130 | end
131 |
132 | class SessionConnection < ActionCable::Connection::Base
133 | identified_by :user_id
134 |
135 | def connect
136 | self.user_id = verify_user
137 | end
138 |
139 | private
140 |
141 | def verify_user
142 | reject_unauthorized_connection unless request.session[:user_id].present?
143 | request.session[:user_id]
144 | end
145 | end
146 |
147 | class SessionConnectionTest < ActionCable::Connection::TestCase
148 | tests SessionConnection
149 |
150 | def test_connected_with_encrypted_cookies
151 | connect session: { user_id: "789" }
152 | assert_equal "789", connection.user_id
153 | end
154 |
155 | def test_connection_rejected
156 | assert_reject_connection { connect }
157 | end
158 | end
159 |
--------------------------------------------------------------------------------
/test/stubs/global_id.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class GlobalID
4 | attr_reader :uri
5 | delegate :to_param, :to_s, to: :uri
6 |
7 | def initialize(gid, options = {})
8 | @uri = gid
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/test/stubs/test_adapter.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class SuccessAdapter < ActionCable::SubscriptionAdapter::Base
4 | def broadcast(channel, payload)
5 | end
6 |
7 | def subscribe(channel, callback, success_callback = nil)
8 | end
9 |
10 | def unsubscribe(channel, callback)
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/test/stubs/test_server.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "ostruct"
4 |
5 | class TestServer
6 | include ActionCable::Server::Connections
7 | include ActionCable::Server::Broadcasting
8 |
9 | attr_reader :logger, :config, :mutex
10 |
11 | def initialize(subscription_adapter: SuccessAdapter)
12 | @logger = ActiveSupport::TaggedLogging.new ActiveSupport::Logger.new(StringIO.new)
13 |
14 | @config = OpenStruct.new(log_tags: [], subscription_adapter: subscription_adapter)
15 |
16 | @mutex = Monitor.new
17 | end
18 |
19 | def pubsub
20 | @pubsub ||= @config.subscription_adapter.new(self)
21 | end
22 |
23 | def event_loop
24 | @event_loop ||= ActionCable::Connection::StreamEventLoop.new.tap do |loop|
25 | loop.instance_variable_set(:@executor, Concurrent.global_io_executor)
26 | end
27 | end
28 |
29 | def worker_pool
30 | @worker_pool ||= ActionCable::Server::Worker.new(max_size: 5)
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/test/stubs/user.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class User
4 | attr_reader :name
5 |
6 | def initialize(name)
7 | @name = name
8 | end
9 |
10 | def to_global_id
11 | GlobalID.new("User##{name}")
12 | end
13 |
14 | def to_gid_param
15 | to_global_id.to_param
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/test/syntax_test.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require_relative "test_helper"
4 |
5 | using ActionCable::Testing::Rails6
6 |
7 | class BroadcastChannel < ActionCable::Channel::Base
8 | end
9 |
10 | class TransmissionsTest < ActionCable::TestCase
11 | def test_broadcasting_for_name
12 | skip unless ActionCable::Testing::Rails6::SUPPORTED
13 |
14 | user = User.new(42)
15 |
16 | assert_nothing_raised do
17 | assert_broadcasts BroadcastChannel.broadcasting_for(user), 1 do
18 | BroadcastChannel.broadcast_to user, text: "text"
19 | end
20 | end
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/test/test_helper.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | $LOAD_PATH.unshift File.expand_path("../../lib", __FILE__)
4 |
5 | begin
6 | require "pry-byebug"
7 | rescue LoadError
8 | end
9 |
10 | require "action_cable"
11 | require "action-cable-testing"
12 |
13 | require "active_support/testing/autorun"
14 |
15 | # Require all the stubs and models
16 | Dir[File.expand_path("stubs/*.rb", __dir__)].each { |file| require file }
17 |
18 | require File.expand_path("../spec/support/deprecated_api", __dir__)
19 |
20 | # # Set test adapter and logger
21 | ActionCable.server.config.cable = { "adapter" => "test" }
22 | ActionCable.server.config.logger =
23 | ActiveSupport::TaggedLogging.new ActiveSupport::Logger.new(StringIO.new)
24 |
--------------------------------------------------------------------------------
/test/test_helper_test.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require_relative "test_helper"
4 |
5 | class BroadcastChannel < ActionCable::Channel::Base
6 | end
7 |
8 | class TransmissionsTest < ActionCable::TestCase
9 | using ActionCable::Testing::Rails6
10 |
11 | def test_assert_broadcasts
12 | assert_nothing_raised do
13 | assert_broadcasts("test", 1) do
14 | ActionCable.server.broadcast "test", "message"
15 | end
16 | end
17 | end
18 |
19 | def test_assert_broadcasts_with_no_block
20 | assert_nothing_raised do
21 | ActionCable.server.broadcast "test", "message"
22 | assert_broadcasts "test", 1
23 | end
24 |
25 | assert_nothing_raised do
26 | ActionCable.server.broadcast "test", "message 2"
27 | ActionCable.server.broadcast "test", "message 3"
28 | assert_broadcasts "test", 3
29 | end
30 | end
31 |
32 | def test_assert_no_broadcasts_with_no_block
33 | assert_nothing_raised do
34 | assert_no_broadcasts "test"
35 | end
36 | end
37 |
38 | def test_assert_no_broadcasts
39 | assert_nothing_raised do
40 | assert_no_broadcasts("test") do
41 | ActionCable.server.broadcast "test2", "message"
42 | end
43 | end
44 | end
45 |
46 | def test_assert_broadcasts_message_too_few_sent
47 | ActionCable.server.broadcast "test", "hello"
48 | error = assert_raises Minitest::Assertion do
49 | assert_broadcasts("test", 2) do
50 | ActionCable.server.broadcast "test", "world"
51 | end
52 | end
53 |
54 | assert_match(/2 .* but 1/, error.message)
55 | end
56 |
57 | def test_assert_broadcasts_message_too_many_sent
58 | error = assert_raises Minitest::Assertion do
59 | assert_broadcasts("test", 1) do
60 | ActionCable.server.broadcast "test", "hello"
61 | ActionCable.server.broadcast "test", "world"
62 | end
63 | end
64 |
65 | assert_match(/1 .* but 2/, error.message)
66 | end
67 |
68 | def test_assert_broadcast_to_objects
69 | skip unless ActionCable::Testing::Rails6::SUPPORTED
70 |
71 | user = User.new(42)
72 |
73 | assert_nothing_raised do
74 | assert_broadcasts BroadcastChannel.broadcasting_for(user), 1 do
75 | BroadcastChannel.broadcast_to user, text: "text"
76 | end
77 | end
78 | end
79 |
80 | if DeprecatedApi.enabled?
81 | def test_deprecated_assert_broadcast_to_object_with_channel
82 | user = User.new(42)
83 |
84 | ActiveSupport::Deprecation.silence do
85 | assert_nothing_raised do
86 | assert_broadcasts user, 1, channel: BroadcastChannel do
87 | BroadcastChannel.broadcast_to user, text: "text"
88 | end
89 | end
90 | end
91 | end
92 | end
93 |
94 | def test_assert_broadcast_to_object_without_channel
95 | user = User.new(42)
96 |
97 | assert_raises Minitest::Assertion do
98 | assert_broadcasts user, 1 do
99 | BroadcastChannel.broadcast_to user, text: "text"
100 | end
101 | end
102 | end
103 | end
104 |
105 | class TransmitedDataTest < ActionCable::TestCase
106 | include ActionCable::TestHelper
107 | using ActionCable::Testing::Rails6
108 |
109 | def test_assert_broadcast_on
110 | assert_nothing_raised do
111 | assert_broadcast_on("test", "message") do
112 | ActionCable.server.broadcast "test", "message"
113 | end
114 | end
115 | end
116 |
117 | def test_assert_broadcast_on_with_hash
118 | assert_nothing_raised do
119 | assert_broadcast_on("test", text: "hello") do
120 | ActionCable.server.broadcast "test", text: "hello"
121 | end
122 | end
123 | end
124 |
125 | def test_assert_broadcast_on_with_no_block
126 | assert_nothing_raised do
127 | ActionCable.server.broadcast "test", "hello"
128 | assert_broadcast_on "test", "hello"
129 | end
130 |
131 | assert_nothing_raised do
132 | ActionCable.server.broadcast "test", "world"
133 | assert_broadcast_on "test", "world"
134 | end
135 | end
136 |
137 | def test_assert_broadcast_on_message
138 | ActionCable.server.broadcast "test", "hello"
139 | error = assert_raises Minitest::Assertion do
140 | assert_broadcast_on("test", "world")
141 | end
142 |
143 | assert_match(/No messages sent/, error.message)
144 | end
145 |
146 | def test_assert_broadcast_on_object
147 | skip unless ActionCable::Testing::Rails6::SUPPORTED
148 |
149 | user = User.new(42)
150 |
151 | assert_nothing_raised do
152 | assert_broadcast_on BroadcastChannel.broadcasting_for(user), text: "text" do
153 | BroadcastChannel.broadcast_to user, text: "text"
154 | end
155 | end
156 | end
157 |
158 | if DeprecatedApi.enabled?
159 | def test_deprecated_assert_broadcast_om_object_with_channel
160 | user = User.new(42)
161 |
162 | ActiveSupport::Deprecation.silence do
163 | assert_nothing_raised do
164 | assert_broadcast_on user, { text: "text" }, { channel: BroadcastChannel } do
165 | BroadcastChannel.broadcast_to user, text: "text"
166 | end
167 | end
168 | end
169 | end
170 | end
171 |
172 | def test_assert_broadcast_on_object_without_channel
173 | user = User.new(42)
174 |
175 | assert_raises Minitest::Assertion do
176 | assert_broadcast_on user, text: "text" do
177 | BroadcastChannel.broadcast_to user, text: "text"
178 | end
179 | end
180 | end
181 | end
182 |
--------------------------------------------------------------------------------