action-cable-testing 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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