action-cable-testing 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/CHANGELOG.md +8 -0
- data/README.md +54 -0
- data/lib/action_cable/channel/test_case.rb +7 -5
- data/lib/action_cable/connection/test_case.rb +192 -0
- data/lib/action_cable/testing.rb +6 -0
- data/lib/action_cable/testing/version.rb +1 -1
- data/lib/rspec/rails/example/channel_example_group.rb +11 -0
- data/lib/rspec/rails/matchers/action_cable.rb +2 -2
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: f485ca6094981aea2459acc0a1334d79855b20180fd7866e4bbb073d97d9d75a
|
4
|
+
data.tar.gz: a350c77463c57478ada51df21acdeab8a4ef4621abe7dad12a07347a3820c4f3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 166cd3cfd3481b90e4d67d3c1b2348010e399e762d5278e198de9d3b254babac01287ddfa288c0aa8c7ebd1402e538e2c34bc83a5ebda4214f3105c6288d84c5
|
7
|
+
data.tar.gz: a3065fa172d0a0777dc057c68b17e5b12d1b89a4c105b9566b8a44cca3d37db713242982f9d1d9d465a5088b758fdf1b393bd0845f819dbeacac92109d1a8aa1
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,13 @@
|
|
1
1
|
# Change log
|
2
2
|
|
3
|
+
## master
|
4
|
+
|
5
|
+
## 0.3.0
|
6
|
+
|
7
|
+
- Add connection unit-testing utilities. ([@palkan][])
|
8
|
+
|
9
|
+
See https://github.com/palkan/action-cable-testing/pull/6
|
10
|
+
|
3
11
|
## 0.2.0
|
4
12
|
|
5
13
|
- Update minitest's `assert_broadcast_on` and `assert_broadcasts` matchers to support a record as an argument. ([@thesmartnik][])
|
data/README.md
CHANGED
@@ -156,6 +156,43 @@ class ChatChannelTest < ActionCable::Channel::TestCase
|
|
156
156
|
end
|
157
157
|
```
|
158
158
|
|
159
|
+
### Connection Testing
|
160
|
+
|
161
|
+
Connection unit tests are written as follows:
|
162
|
+
1. First, one uses the `connect` method to simulate connection.
|
163
|
+
2. Then, one asserts whether the current state is as expected (e.g. identifiers).
|
164
|
+
|
165
|
+
For example:
|
166
|
+
|
167
|
+
```ruby
|
168
|
+
module ApplicationCable
|
169
|
+
class ConnectionTest < ActionCable::Connection::TestCase
|
170
|
+
def test_connects_with_cookies
|
171
|
+
# Simulate a connection
|
172
|
+
connect cookies: { user_id: users[:john].id }
|
173
|
+
|
174
|
+
# Asserts that the connection identifier is correct
|
175
|
+
assert_equal "John", connection.user.name
|
176
|
+
end
|
177
|
+
|
178
|
+
def test_does_not_connect_without_user
|
179
|
+
assert_reject_connection do
|
180
|
+
connect
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
```
|
185
|
+
|
186
|
+
You can also provide additional information about underlying HTTP request:
|
187
|
+
|
188
|
+
```ruby
|
189
|
+
def test_connect_with_headers_and_query_string
|
190
|
+
connect "/cable?user_id=1", headers: { "X-API-TOKEN" => 'secret-my' }
|
191
|
+
|
192
|
+
assert_equal connection.user_id, "1"
|
193
|
+
end
|
194
|
+
```
|
195
|
+
|
159
196
|
### RSpec Usage
|
160
197
|
|
161
198
|
First, you need to have [rspec-rails](https://github.com/rspec/rspec-rails) installed.
|
@@ -227,6 +264,23 @@ RSpec.describe ChatChannel, type: :channel do
|
|
227
264
|
end
|
228
265
|
```
|
229
266
|
|
267
|
+
And, of course, connections:
|
268
|
+
|
269
|
+
```ruby
|
270
|
+
require "rails_helper"
|
271
|
+
|
272
|
+
RSpec.describe ApplicationCable::Connection, type: :channel do
|
273
|
+
it "successfully connects" do
|
274
|
+
connect "/cable", headers: { "X-USER-ID" => "325" }
|
275
|
+
expect(connection.user_id).to eq "325"
|
276
|
+
end
|
277
|
+
|
278
|
+
it "rejects connection" do
|
279
|
+
expect { connect "/cable" }.to have_rejected_connection
|
280
|
+
end
|
281
|
+
end
|
282
|
+
```
|
283
|
+
|
230
284
|
#### Shared contexts to switch between adapters
|
231
285
|
|
232
286
|
Sometimes you may want to use _real_ Action Cable adapter instead of the test one (for example, in Capybara-like tests).
|
@@ -38,6 +38,10 @@ module ActionCable
|
|
38
38
|
def streams
|
39
39
|
@_streams ||= []
|
40
40
|
end
|
41
|
+
|
42
|
+
# Make periodic timers no-op
|
43
|
+
def start_periodic_timers; end
|
44
|
+
alias stop_periodic_timers start_periodic_timers
|
41
45
|
end
|
42
46
|
|
43
47
|
class ConnectionStub
|
@@ -80,7 +84,7 @@ module ActionCable
|
|
80
84
|
# assert subscription.confirmed?
|
81
85
|
#
|
82
86
|
# # Asserts that the channel subscribes connection to a stream
|
83
|
-
#
|
87
|
+
# assert_equal "chat_1", streams.last
|
84
88
|
# end
|
85
89
|
#
|
86
90
|
# def test_does_not_subscribe_without_room_number
|
@@ -137,6 +141,7 @@ module ActionCable
|
|
137
141
|
|
138
142
|
include ActiveSupport::Testing::ConstantLookup
|
139
143
|
include ActionCable::TestHelper
|
144
|
+
include ActionCable::Connection::TestCase::Behavior
|
140
145
|
|
141
146
|
CHANNEL_IDENTIFIER = "test_stub"
|
142
147
|
|
@@ -191,6 +196,7 @@ module ActionCable
|
|
191
196
|
|
192
197
|
# Subsribe to the channel under test. Optionally pass subscription parameters as a Hash.
|
193
198
|
def subscribe(params = {})
|
199
|
+
@connection ||= stub_connection
|
194
200
|
# NOTE: Rails < 5.0.1 calls subscribe_to_channel during #initialize.
|
195
201
|
# We have to stub before it
|
196
202
|
@subscription = self.class.channel_class.allocate
|
@@ -209,10 +215,6 @@ module ActionCable
|
|
209
215
|
subscription.perform_action(data.stringify_keys.merge("action" => action.to_s))
|
210
216
|
end
|
211
217
|
|
212
|
-
def connection # :nodoc:
|
213
|
-
@connection ||= stub_connection
|
214
|
-
end
|
215
|
-
|
216
218
|
# Returns messages transmitted into channel
|
217
219
|
def transmissions
|
218
220
|
# Return only directly sent message (via #transmit)
|
@@ -0,0 +1,192 @@
|
|
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/testing/test_request"
|
8
|
+
|
9
|
+
module ActionCable
|
10
|
+
module Connection
|
11
|
+
class NonInferrableConnectionError < ::StandardError
|
12
|
+
def initialize(name)
|
13
|
+
super "Unable to determine the connection to test from #{name}. " +
|
14
|
+
"You'll need to specify it using tests YourConnection in your " +
|
15
|
+
"test case definition."
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
module Assertions
|
20
|
+
# Asserts that the connection is rejected (via +reject_unauthorized_connection+).
|
21
|
+
#
|
22
|
+
# # Asserts that connection without user_id fails
|
23
|
+
# assert_reject_connection { connect cookies: { user_id: '' } }
|
24
|
+
def assert_reject_connection(&block)
|
25
|
+
res =
|
26
|
+
begin
|
27
|
+
block.call
|
28
|
+
false
|
29
|
+
rescue ActionCable::Connection::Authorization::UnauthorizedError
|
30
|
+
true
|
31
|
+
end
|
32
|
+
|
33
|
+
assert res, "Expected to reject connection but no rejection were made"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
class TestRequest < ActionDispatch::TestRequest
|
38
|
+
attr_reader :cookie_jar
|
39
|
+
|
40
|
+
module CookiesStub
|
41
|
+
# Stub signed cookies, we don't need to test encryption here
|
42
|
+
def signed
|
43
|
+
self
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def cookie_jar=(val)
|
48
|
+
@cookie_jar = val.tap do |h|
|
49
|
+
h.singleton_class.include(CookiesStub)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
module TestConnection
|
55
|
+
attr_reader :logger, :request
|
56
|
+
|
57
|
+
def initialize(path, cookies, headers)
|
58
|
+
@logger = ActiveSupport::TaggedLogging.new ActiveSupport::Logger.new(StringIO.new)
|
59
|
+
|
60
|
+
uri = URI.parse(path)
|
61
|
+
env = {
|
62
|
+
"QUERY_STRING" => uri.query,
|
63
|
+
"PATH_INFO" => uri.path
|
64
|
+
}.merge(build_headers(headers))
|
65
|
+
|
66
|
+
@request = TestRequest.create(env)
|
67
|
+
@request.cookie_jar = cookies.with_indifferent_access
|
68
|
+
end
|
69
|
+
|
70
|
+
def build_headers(headers)
|
71
|
+
headers.each_with_object({}) do |(k, v), obj|
|
72
|
+
k = k.upcase
|
73
|
+
k.tr!("-", "_")
|
74
|
+
obj["HTTP_#{k}"] = v
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Superclass for Action Cable connection unit tests.
|
80
|
+
#
|
81
|
+
# == Basic example
|
82
|
+
#
|
83
|
+
# Unit tests are written as follows:
|
84
|
+
# 1. First, one uses the +connect+ method to simulate connection.
|
85
|
+
# 2. Then, one asserts whether the current state is as expected (e.g. identifiers).
|
86
|
+
#
|
87
|
+
# For example:
|
88
|
+
#
|
89
|
+
# module ApplicationCable
|
90
|
+
# class ConnectionTest < ActionCable::Connection::TestCase
|
91
|
+
# def test_connects_with_cookies
|
92
|
+
# # Simulate a connection
|
93
|
+
# connect cookies: { user_id: users[:john].id }
|
94
|
+
#
|
95
|
+
# # Asserts that the connection identifier is correct
|
96
|
+
# assert_equal "John", connection.user.name
|
97
|
+
# end
|
98
|
+
#
|
99
|
+
# def test_does_not_connect_without_user
|
100
|
+
# assert_reject_connection do
|
101
|
+
# connect
|
102
|
+
# end
|
103
|
+
# end
|
104
|
+
# end
|
105
|
+
#
|
106
|
+
# You can also provide additional information about underlying HTTP request:
|
107
|
+
# def test_connect_with_headers_and_query_string
|
108
|
+
# connect "/cable?user_id=1", headers: { "X-API-TOKEN" => 'secret-my' }
|
109
|
+
#
|
110
|
+
# assert_equal connection.user_id, "1"
|
111
|
+
# end
|
112
|
+
#
|
113
|
+
# == Connection is automatically inferred
|
114
|
+
#
|
115
|
+
# ActionCable::Connection::TestCase will automatically infer the connection under test
|
116
|
+
# from the test class name. If the channel cannot be inferred from the test
|
117
|
+
# class name, you can explicitly set it with +tests+.
|
118
|
+
#
|
119
|
+
# class ConnectionTest < ActionCable::Connection::TestCase
|
120
|
+
# tests ApplicationCable::Connection
|
121
|
+
# end
|
122
|
+
#
|
123
|
+
class TestCase < ActiveSupport::TestCase
|
124
|
+
module Behavior
|
125
|
+
extend ActiveSupport::Concern
|
126
|
+
|
127
|
+
include ActiveSupport::Testing::ConstantLookup
|
128
|
+
include Assertions
|
129
|
+
|
130
|
+
included do
|
131
|
+
class_attribute :_connection_class
|
132
|
+
|
133
|
+
attr_reader :connection
|
134
|
+
|
135
|
+
ActiveSupport.run_load_hooks(:action_cable_connection_test_case, self)
|
136
|
+
end
|
137
|
+
|
138
|
+
module ClassMethods
|
139
|
+
def tests(connection)
|
140
|
+
case connection
|
141
|
+
when String, Symbol
|
142
|
+
self._connection_class = connection.to_s.camelize.constantize
|
143
|
+
when Module
|
144
|
+
self._connection_class = connection
|
145
|
+
else
|
146
|
+
raise NonInferrableConnectionError.new(connection)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def connection_class
|
151
|
+
if connection = self._connection_class
|
152
|
+
connection
|
153
|
+
else
|
154
|
+
tests determine_default_connection(name)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def determine_default_connection(name)
|
159
|
+
connection = determine_constant_from_test_name(name) do |constant|
|
160
|
+
Class === constant && constant < ActionCable::Connection::Base
|
161
|
+
end
|
162
|
+
raise NonInferrableConnectionError.new(name) if connection.nil?
|
163
|
+
connection
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
# Performs connection attempt (i.e. calls #connect method).
|
168
|
+
#
|
169
|
+
# Accepts request path as the first argument and cookies and headers as options.
|
170
|
+
def connect(path = "/cable", cookies: {}, headers: {})
|
171
|
+
connection = self.class.connection_class.allocate
|
172
|
+
connection.singleton_class.include(TestConnection)
|
173
|
+
connection.send(:initialize, path, cookies, headers)
|
174
|
+
connection.connect if connection.respond_to?(:connect)
|
175
|
+
|
176
|
+
# Only set instance variable if connected successfully
|
177
|
+
@connection = connection
|
178
|
+
end
|
179
|
+
|
180
|
+
# Disconnect the connection under test (i.e. calls #disconnect)
|
181
|
+
def disconnect
|
182
|
+
raise "Must be connected!" if connection.nil?
|
183
|
+
|
184
|
+
connection.disconnect if connection.respond_to?(:disconnect)
|
185
|
+
@connection = nil
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
include Behavior
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
data/lib/action_cable/testing.rb
CHANGED
@@ -27,6 +27,17 @@ if defined?(ActionCable)
|
|
27
27
|
def channel_class
|
28
28
|
described_class
|
29
29
|
end
|
30
|
+
|
31
|
+
# @private
|
32
|
+
def connection_class
|
33
|
+
raise "Described class is not a Connection class" unless
|
34
|
+
described_class <= ::ActionCable::Connection::Base
|
35
|
+
described_class
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def have_rejected_connection
|
40
|
+
raise_error(::ActionCable::Connection::Authorization::UnauthorizedError)
|
30
41
|
end
|
31
42
|
end
|
32
43
|
end
|
@@ -7,7 +7,7 @@ module RSpec
|
|
7
7
|
#
|
8
8
|
# @api private
|
9
9
|
module ActionCable
|
10
|
-
# rubocop: disable
|
10
|
+
# rubocop: disable Metrics/ClassLength
|
11
11
|
# @private
|
12
12
|
class HaveBroadcastedTo < RSpec::Matchers::BuiltIn::BaseMatcher
|
13
13
|
def initialize(target, channel:)
|
@@ -167,7 +167,7 @@ module RSpec
|
|
167
167
|
raise ArgumentError, error_msg
|
168
168
|
end
|
169
169
|
end
|
170
|
-
# rubocop: enable
|
170
|
+
# rubocop: enable Metrics/ClassLength
|
171
171
|
end
|
172
172
|
|
173
173
|
# @api public
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: action-cable-testing
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Vladimir Dementyev
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2018-03-02 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: actioncable
|
@@ -148,6 +148,7 @@ files:
|
|
148
148
|
- README.md
|
149
149
|
- lib/action-cable-testing.rb
|
150
150
|
- lib/action_cable/channel/test_case.rb
|
151
|
+
- lib/action_cable/connection/test_case.rb
|
151
152
|
- lib/action_cable/subscription_adapter/test.rb
|
152
153
|
- lib/action_cable/test_case.rb
|
153
154
|
- lib/action_cable/test_helper.rb
|
@@ -182,7 +183,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
182
183
|
version: '0'
|
183
184
|
requirements: []
|
184
185
|
rubyforge_project:
|
185
|
-
rubygems_version: 2.
|
186
|
+
rubygems_version: 2.7.4
|
186
187
|
signing_key:
|
187
188
|
specification_version: 4
|
188
189
|
summary: Testing utils for Action Cable
|