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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 49ea4d058ad808b422aee252af3404f7ffc19b09
4
- data.tar.gz: fc2a1ab66b7d9dbbcb34a8c59d51f614fe715e8e
2
+ SHA256:
3
+ metadata.gz: f485ca6094981aea2459acc0a1334d79855b20180fd7866e4bbb073d97d9d75a
4
+ data.tar.gz: a350c77463c57478ada51df21acdeab8a4ef4621abe7dad12a07347a3820c4f3
5
5
  SHA512:
6
- metadata.gz: 6457b2c9afabf9c6e758a5cbe1cad993c7585734dec33bfed1a96e4d606dcc415e507cefc3d0e6fc7acbcca877edd4d558798919ef2f700846c40d7185faecd7
7
- data.tar.gz: 9cc75eb56ad1f4b6f30540e5d3b523be3ab6278604bbc97325e1b3d1d84024dbd72ea9e7feb0894732b134c4832a4040c2c76843d6d14d4e690ae244c91d2b81
6
+ metadata.gz: 166cd3cfd3481b90e4d67d3c1b2348010e399e762d5278e198de9d3b254babac01287ddfa288c0aa8c7ebd1402e538e2c34bc83a5ebda4214f3105c6288d84c5
7
+ data.tar.gz: a3065fa172d0a0777dc057c68b17e5b12d1b89a4c105b9566b8a44cca3d37db713242982f9d1d9d465a5088b758fdf1b393bd0845f819dbeacac92109d1a8aa1
@@ -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
- # assert "chat_1", streams.last
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
@@ -14,6 +14,12 @@ module ActionCable
14
14
  end
15
15
  end
16
16
 
17
+ module Connection
18
+ eager_autoload do
19
+ autoload :TestCase
20
+ end
21
+ end
22
+
17
23
  module SubscriptionAdapter
18
24
  autoload :Test
19
25
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module ActionCable
4
4
  module Testing
5
- VERSION = "0.2.0"
5
+ VERSION = "0.3.0"
6
6
  end
7
7
  end
@@ -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 Style/ClassLength
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 Style/ClassLength
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.2.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: 2017-12-19 00:00:00.000000000 Z
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.6.13
186
+ rubygems_version: 2.7.4
186
187
  signing_key:
187
188
  specification_version: 4
188
189
  summary: Testing utils for Action Cable