actioncable 5.2.8.1 → 6.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +58 -112
  3. data/MIT-LICENSE +1 -1
  4. data/README.md +1 -544
  5. data/app/assets/javascripts/action_cable.js +492 -0
  6. data/lib/action_cable/channel/base.rb +5 -1
  7. data/lib/action_cable/channel/test_case.rb +312 -0
  8. data/lib/action_cable/channel.rb +1 -0
  9. data/lib/action_cable/connection/authorization.rb +1 -1
  10. data/lib/action_cable/connection/base.rb +9 -7
  11. data/lib/action_cable/connection/message_buffer.rb +1 -4
  12. data/lib/action_cable/connection/stream.rb +4 -2
  13. data/lib/action_cable/connection/subscriptions.rb +1 -5
  14. data/lib/action_cable/connection/test_case.rb +236 -0
  15. data/lib/action_cable/connection/web_socket.rb +1 -3
  16. data/lib/action_cable/connection.rb +1 -0
  17. data/lib/action_cable/gem_version.rb +4 -4
  18. data/lib/action_cable/server/base.rb +3 -1
  19. data/lib/action_cable/server/worker.rb +5 -7
  20. data/lib/action_cable/subscription_adapter/postgresql.rb +24 -8
  21. data/lib/action_cable/subscription_adapter/redis.rb +2 -1
  22. data/lib/action_cable/subscription_adapter/test.rb +40 -0
  23. data/lib/action_cable/subscription_adapter.rb +1 -0
  24. data/lib/action_cable/test_case.rb +11 -0
  25. data/lib/action_cable/test_helper.rb +133 -0
  26. data/lib/action_cable.rb +15 -7
  27. data/lib/rails/generators/channel/USAGE +4 -5
  28. data/lib/rails/generators/channel/channel_generator.rb +6 -3
  29. data/lib/rails/generators/channel/templates/{assets → javascript}/channel.js.tt +3 -1
  30. data/lib/rails/generators/channel/templates/{assets/cable.js.tt → javascript/consumer.js.tt} +2 -9
  31. data/lib/rails/generators/channel/templates/javascript/index.js.tt +5 -0
  32. data/lib/rails/generators/test_unit/channel_generator.rb +20 -0
  33. data/lib/rails/generators/test_unit/templates/channel_test.rb.tt +8 -0
  34. metadata +24 -17
  35. data/lib/assets/compiled/action_cable.js +0 -601
  36. data/lib/rails/generators/channel/templates/assets/channel.coffee.tt +0 -14
@@ -0,0 +1,312 @@
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
+ module ActionCable
9
+ module Channel
10
+ class NonInferrableChannelError < ::StandardError
11
+ def initialize(name)
12
+ super "Unable to determine the channel to test from #{name}. " +
13
+ "You'll need to specify it using `tests YourChannel` in your " +
14
+ "test case definition."
15
+ end
16
+ end
17
+
18
+ # Stub `stream_from` to track streams for the channel.
19
+ # Add public aliases for `subscription_confirmation_sent?` and
20
+ # `subscription_rejected?`.
21
+ module ChannelStub
22
+ def confirmed?
23
+ subscription_confirmation_sent?
24
+ end
25
+
26
+ def rejected?
27
+ subscription_rejected?
28
+ end
29
+
30
+ def stream_from(broadcasting, *)
31
+ streams << broadcasting
32
+ end
33
+
34
+ def stop_all_streams
35
+ @_streams = []
36
+ end
37
+
38
+ def streams
39
+ @_streams ||= []
40
+ end
41
+
42
+ # Make periodic timers no-op
43
+ def start_periodic_timers; end
44
+ alias stop_periodic_timers start_periodic_timers
45
+ end
46
+
47
+ class ConnectionStub
48
+ attr_reader :transmissions, :identifiers, :subscriptions, :logger
49
+
50
+ def initialize(identifiers = {})
51
+ @transmissions = []
52
+
53
+ identifiers.each do |identifier, val|
54
+ define_singleton_method(identifier) { val }
55
+ end
56
+
57
+ @subscriptions = ActionCable::Connection::Subscriptions.new(self)
58
+ @identifiers = identifiers.keys
59
+ @logger = ActiveSupport::TaggedLogging.new ActiveSupport::Logger.new(StringIO.new)
60
+ end
61
+
62
+ def transmit(cable_message)
63
+ transmissions << cable_message.with_indifferent_access
64
+ end
65
+ end
66
+
67
+ # Superclass for Action Cable channel functional tests.
68
+ #
69
+ # == Basic example
70
+ #
71
+ # Functional tests are written as follows:
72
+ # 1. First, one uses the +subscribe+ method to simulate subscription creation.
73
+ # 2. Then, one asserts whether the current state is as expected. "State" can be anything:
74
+ # transmitted messages, subscribed streams, etc.
75
+ #
76
+ # For example:
77
+ #
78
+ # class ChatChannelTest < ActionCable::Channel::TestCase
79
+ # def test_subscribed_with_room_number
80
+ # # Simulate a subscription creation
81
+ # subscribe room_number: 1
82
+ #
83
+ # # Asserts that the subscription was successfully created
84
+ # assert subscription.confirmed?
85
+ #
86
+ # # Asserts that the channel subscribes connection to a stream
87
+ # assert_has_stream "chat_1"
88
+ #
89
+ # # Asserts that the channel subscribes connection to a specific
90
+ # # stream created for a model
91
+ # assert_has_stream_for Room.find(1)
92
+ # end
93
+ #
94
+ # def test_does_not_stream_with_incorrect_room_number
95
+ # subscribe room_number: -1
96
+ #
97
+ # # Asserts that not streams was started
98
+ # assert_no_streams
99
+ # end
100
+ #
101
+ # def test_does_not_subscribe_without_room_number
102
+ # subscribe
103
+ #
104
+ # # Asserts that the subscription was rejected
105
+ # assert subscription.rejected?
106
+ # end
107
+ # end
108
+ #
109
+ # You can also perform actions:
110
+ # def test_perform_speak
111
+ # subscribe room_number: 1
112
+ #
113
+ # perform :speak, message: "Hello, Rails!"
114
+ #
115
+ # assert_equal "Hello, Rails!", transmissions.last["text"]
116
+ # end
117
+ #
118
+ # == Special methods
119
+ #
120
+ # ActionCable::Channel::TestCase will also automatically provide the following instance
121
+ # methods for use in the tests:
122
+ #
123
+ # <b>connection</b>::
124
+ # An ActionCable::Channel::ConnectionStub, representing the current HTTP connection.
125
+ # <b>subscription</b>::
126
+ # An instance of the current channel, created when you call `subscribe`.
127
+ # <b>transmissions</b>::
128
+ # A list of all messages that have been transmitted into the channel.
129
+ #
130
+ #
131
+ # == Channel is automatically inferred
132
+ #
133
+ # ActionCable::Channel::TestCase will automatically infer the channel under test
134
+ # from the test class name. If the channel cannot be inferred from the test
135
+ # class name, you can explicitly set it with +tests+.
136
+ #
137
+ # class SpecialEdgeCaseChannelTest < ActionCable::Channel::TestCase
138
+ # tests SpecialChannel
139
+ # end
140
+ #
141
+ # == Specifying connection identifiers
142
+ #
143
+ # You need to set up your connection manually to provide values for the identifiers.
144
+ # To do this just use:
145
+ #
146
+ # stub_connection(user: users[:john])
147
+ #
148
+ # == Testing broadcasting
149
+ #
150
+ # ActionCable::Channel::TestCase enhances ActionCable::TestHelper assertions (e.g.
151
+ # +assert_broadcasts+) to handle broadcasting to models:
152
+ #
153
+ #
154
+ # # in your channel
155
+ # def speak(data)
156
+ # broadcast_to room, text: data["message"]
157
+ # end
158
+ #
159
+ # def test_speak
160
+ # subscribe room_id: rooms[:chat].id
161
+ #
162
+ # assert_broadcasts_on(rooms[:chat], text: "Hello, Rails!") do
163
+ # perform :speak, message: "Hello, Rails!"
164
+ # end
165
+ # end
166
+ class TestCase < ActiveSupport::TestCase
167
+ module Behavior
168
+ extend ActiveSupport::Concern
169
+
170
+ include ActiveSupport::Testing::ConstantLookup
171
+ include ActionCable::TestHelper
172
+
173
+ CHANNEL_IDENTIFIER = "test_stub"
174
+
175
+ included do
176
+ class_attribute :_channel_class
177
+
178
+ attr_reader :connection, :subscription
179
+
180
+ ActiveSupport.run_load_hooks(:action_cable_channel_test_case, self)
181
+ end
182
+
183
+ module ClassMethods
184
+ def tests(channel)
185
+ case channel
186
+ when String, Symbol
187
+ self._channel_class = channel.to_s.camelize.constantize
188
+ when Module
189
+ self._channel_class = channel
190
+ else
191
+ raise NonInferrableChannelError.new(channel)
192
+ end
193
+ end
194
+
195
+ def channel_class
196
+ if channel = self._channel_class
197
+ channel
198
+ else
199
+ tests determine_default_channel(name)
200
+ end
201
+ end
202
+
203
+ def determine_default_channel(name)
204
+ channel = determine_constant_from_test_name(name) do |constant|
205
+ Class === constant && constant < ActionCable::Channel::Base
206
+ end
207
+ raise NonInferrableChannelError.new(name) if channel.nil?
208
+ channel
209
+ end
210
+ end
211
+
212
+ # Setup test connection with the specified identifiers:
213
+ #
214
+ # class ApplicationCable < ActionCable::Connection::Base
215
+ # identified_by :user, :token
216
+ # end
217
+ #
218
+ # stub_connection(user: users[:john], token: 'my-secret-token')
219
+ def stub_connection(identifiers = {})
220
+ @connection = ConnectionStub.new(identifiers)
221
+ end
222
+
223
+ # Subscribe to the channel under test. Optionally pass subscription parameters as a Hash.
224
+ def subscribe(params = {})
225
+ @connection ||= stub_connection
226
+ @subscription = self.class.channel_class.new(connection, CHANNEL_IDENTIFIER, params.with_indifferent_access)
227
+ @subscription.singleton_class.include(ChannelStub)
228
+ @subscription.subscribe_to_channel
229
+ @subscription
230
+ end
231
+
232
+ # Unsubscribe the subscription under test.
233
+ def unsubscribe
234
+ check_subscribed!
235
+ subscription.unsubscribe_from_channel
236
+ end
237
+
238
+ # Perform action on a channel.
239
+ #
240
+ # NOTE: Must be subscribed.
241
+ def perform(action, data = {})
242
+ check_subscribed!
243
+ subscription.perform_action(data.stringify_keys.merge("action" => action.to_s))
244
+ end
245
+
246
+ # Returns messages transmitted into channel
247
+ def transmissions
248
+ # Return only directly sent message (via #transmit)
249
+ connection.transmissions.map { |data| data["message"] }.compact
250
+ end
251
+
252
+ # Enhance TestHelper assertions to handle non-String
253
+ # broadcastings
254
+ def assert_broadcasts(stream_or_object, *args)
255
+ super(broadcasting_for(stream_or_object), *args)
256
+ end
257
+
258
+ def assert_broadcast_on(stream_or_object, *args)
259
+ super(broadcasting_for(stream_or_object), *args)
260
+ end
261
+
262
+ # Asserts that no streams have been started.
263
+ #
264
+ # def test_assert_no_started_stream
265
+ # subscribe
266
+ # assert_no_streams
267
+ # end
268
+ #
269
+ def assert_no_streams
270
+ assert subscription.streams.empty?, "No streams started was expected, but #{subscription.streams.count} found"
271
+ end
272
+
273
+ # Asserts that the specified stream has been started.
274
+ #
275
+ # def test_assert_started_stream
276
+ # subscribe
277
+ # assert_has_stream 'messages'
278
+ # end
279
+ #
280
+ def assert_has_stream(stream)
281
+ assert subscription.streams.include?(stream), "Stream #{stream} has not been started"
282
+ end
283
+
284
+ # Asserts that the specified stream for a model has started.
285
+ #
286
+ # def test_assert_started_stream_for
287
+ # subscribe id: 42
288
+ # assert_has_stream_for User.find(42)
289
+ # end
290
+ #
291
+ def assert_has_stream_for(object)
292
+ assert_has_stream(broadcasting_for(object))
293
+ end
294
+
295
+ private
296
+ def check_subscribed!
297
+ raise "Must be subscribed!" if subscription.nil? || subscription.rejected?
298
+ end
299
+
300
+ def broadcasting_for(stream_or_object)
301
+ return stream_or_object if stream_or_object.is_a?(String)
302
+
303
+ self.class.channel_class.broadcasting_for(
304
+ [self.class.channel_class.channel_name, stream_or_object]
305
+ )
306
+ end
307
+ end
308
+
309
+ include Behavior
310
+ end
311
+ end
312
+ end
@@ -11,6 +11,7 @@ module ActionCable
11
11
  autoload :Naming
12
12
  autoload :PeriodicTimers
13
13
  autoload :Streams
14
+ autoload :TestCase
14
15
  end
15
16
  end
16
17
  end
@@ -5,7 +5,7 @@ module ActionCable
5
5
  module Authorization
6
6
  class UnauthorizedError < StandardError; end
7
7
 
8
- # Closes the \WebSocket connection if it is open and returns a 404 "File not Found" response.
8
+ # Closes the WebSocket connection if it is open and returns a 404 "File not Found" response.
9
9
  def reject_unauthorized_connection
10
10
  logger.error "An unauthorized connection attempt was rejected"
11
11
  raise UnauthorizedError
@@ -95,7 +95,12 @@ module ActionCable
95
95
  end
96
96
 
97
97
  # Close the WebSocket connection.
98
- def close
98
+ def close(reason: nil, reconnect: true)
99
+ transmit(
100
+ type: ActionCable::INTERNAL[:message_types][:disconnect],
101
+ reason: reason,
102
+ reconnect: reconnect
103
+ )
99
104
  websocket.close
100
105
  end
101
106
 
@@ -136,13 +141,10 @@ module ActionCable
136
141
  send_async :handle_close
137
142
  end
138
143
 
139
- # TODO Change this to private once we've dropped Ruby 2.2 support.
140
- # Workaround for Ruby 2.2 "private attribute?" warning.
141
- protected
144
+ private
142
145
  attr_reader :websocket
143
146
  attr_reader :message_buffer
144
147
 
145
- private
146
148
  # The request that initiated the WebSocket connection is available here. This gives access to the environment, cookies, etc.
147
149
  def request # :doc:
148
150
  @request ||= begin
@@ -173,7 +175,7 @@ module ActionCable
173
175
  message_buffer.process!
174
176
  server.add_connection(self)
175
177
  rescue ActionCable::Connection::Authorization::UnauthorizedError
176
- respond_to_invalid_request
178
+ close(reason: ActionCable::INTERNAL[:disconnect_reasons][:unauthorized], reconnect: false) if websocket.alive?
177
179
  end
178
180
 
179
181
  def handle_close
@@ -214,7 +216,7 @@ module ActionCable
214
216
  end
215
217
 
216
218
  def respond_to_invalid_request
217
- close if websocket.alive?
219
+ close(reason: ActionCable::INTERNAL[:disconnect_reasons][:invalid_request]) if websocket.alive?
218
220
 
219
221
  logger.error invalid_request_message
220
222
  logger.info finished_request_message
@@ -30,13 +30,10 @@ module ActionCable
30
30
  receive_buffered_messages
31
31
  end
32
32
 
33
- # TODO Change this to private once we've dropped Ruby 2.2 support.
34
- # Workaround for Ruby 2.2 "private attribute?" warning.
35
- protected
33
+ private
36
34
  attr_reader :connection
37
35
  attr_reader :buffered_messages
38
36
 
39
- private
40
37
  def valid?(message)
41
38
  message.is_a?(String)
42
39
  end
@@ -98,8 +98,10 @@ module ActionCable
98
98
  def hijack_rack_socket
99
99
  return unless @socket_object.env["rack.hijack"]
100
100
 
101
- @socket_object.env["rack.hijack"].call
102
- @rack_hijack_io = @socket_object.env["rack.hijack_io"]
101
+ # This should return the underlying io according to the SPEC:
102
+ @rack_hijack_io = @socket_object.env["rack.hijack"].call
103
+ # Retain existing behaviour if required:
104
+ @rack_hijack_io ||= @socket_object.env["rack.hijack_io"]
103
105
 
104
106
  @event_loop.attach(@rack_hijack_io, self)
105
107
  end
@@ -63,12 +63,8 @@ module ActionCable
63
63
  subscriptions.each { |id, channel| remove_subscription(channel) }
64
64
  end
65
65
 
66
- # TODO Change this to private once we've dropped Ruby 2.2 support.
67
- # Workaround for Ruby 2.2 "private attribute?" warning.
68
- protected
69
- attr_reader :connection, :subscriptions
70
-
71
66
  private
67
+ attr_reader :connection, :subscriptions
72
68
  delegate :logger, to: :connection
73
69
 
74
70
  def find(data)
@@ -0,0 +1,236 @@
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 params: { 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
+
46
+ attr_writer :cookie_jar
47
+ end
48
+
49
+ module TestConnection
50
+ attr_reader :logger, :request
51
+
52
+ def initialize(request)
53
+ inner_logger = ActiveSupport::Logger.new(StringIO.new)
54
+ tagged_logging = ActiveSupport::TaggedLogging.new(inner_logger)
55
+ @logger = ActionCable::Connection::TaggedLoggerProxy.new(tagged_logging, tags: [])
56
+ @request = request
57
+ @env = request.env
58
+ end
59
+ end
60
+
61
+ # Unit test Action Cable connections.
62
+ #
63
+ # Useful to check whether a connection's +identified_by+ gets assigned properly
64
+ # and that any improper connection requests are rejected.
65
+ #
66
+ # == Basic example
67
+ #
68
+ # Unit tests are written as follows:
69
+ #
70
+ # 1. Simulate a connection attempt by calling +connect+.
71
+ # 2. Assert state, e.g. identifiers, has been assigned.
72
+ #
73
+ #
74
+ # class ApplicationCable::ConnectionTest < ActionCable::Connection::TestCase
75
+ # def test_connects_with_proper_cookie
76
+ # # Simulate the connection request with a cookie.
77
+ # cookies["user_id"] = users(:john).id
78
+ #
79
+ # connect
80
+ #
81
+ # # Assert the connection identifier matches the fixture.
82
+ # assert_equal users(:john).id, connection.user.id
83
+ # end
84
+ #
85
+ # def test_rejects_connection_without_proper_cookie
86
+ # assert_reject_connection { connect }
87
+ # end
88
+ # end
89
+ #
90
+ # +connect+ accepts additional information the HTTP request with the
91
+ # +params+, +headers+, +session+ and Rack +env+ options.
92
+ #
93
+ # def test_connect_with_headers_and_query_string
94
+ # connect params: { user_id: 1 }, headers: { "X-API-TOKEN" => "secret-my" }
95
+ #
96
+ # assert_equal "1", connection.user.id
97
+ # assert_equal "secret-my", connection.token
98
+ # end
99
+ #
100
+ # def test_connect_with_params
101
+ # connect params: { user_id: 1 }
102
+ #
103
+ # assert_equal "1", connection.user.id
104
+ # end
105
+ #
106
+ # You can also setup the correct cookies before the connection request:
107
+ #
108
+ # def test_connect_with_cookies
109
+ # # Plain cookies:
110
+ # cookies["user_id"] = 1
111
+ #
112
+ # # Or signed/encrypted:
113
+ # # cookies.signed["user_id"] = 1
114
+ # # cookies.encrypted["user_id"] = 1
115
+ #
116
+ # connect
117
+ #
118
+ # assert_equal "1", connection.user_id
119
+ # end
120
+ #
121
+ # == Connection is automatically inferred
122
+ #
123
+ # ActionCable::Connection::TestCase will automatically infer the connection under test
124
+ # from the test class name. If the channel cannot be inferred from the test
125
+ # class name, you can explicitly set it with +tests+.
126
+ #
127
+ # class ConnectionTest < ActionCable::Connection::TestCase
128
+ # tests ApplicationCable::Connection
129
+ # end
130
+ #
131
+ class TestCase < ActiveSupport::TestCase
132
+ module Behavior
133
+ extend ActiveSupport::Concern
134
+
135
+ DEFAULT_PATH = "/cable"
136
+
137
+ include ActiveSupport::Testing::ConstantLookup
138
+ include Assertions
139
+
140
+ included do
141
+ class_attribute :_connection_class
142
+
143
+ attr_reader :connection
144
+
145
+ ActiveSupport.run_load_hooks(:action_cable_connection_test_case, self)
146
+ end
147
+
148
+ module ClassMethods
149
+ def tests(connection)
150
+ case connection
151
+ when String, Symbol
152
+ self._connection_class = connection.to_s.camelize.constantize
153
+ when Module
154
+ self._connection_class = connection
155
+ else
156
+ raise NonInferrableConnectionError.new(connection)
157
+ end
158
+ end
159
+
160
+ def connection_class
161
+ if connection = self._connection_class
162
+ connection
163
+ else
164
+ tests determine_default_connection(name)
165
+ end
166
+ end
167
+
168
+ def determine_default_connection(name)
169
+ connection = determine_constant_from_test_name(name) do |constant|
170
+ Class === constant && constant < ActionCable::Connection::Base
171
+ end
172
+ raise NonInferrableConnectionError.new(name) if connection.nil?
173
+ connection
174
+ end
175
+ end
176
+
177
+ # Performs connection attempt to exert #connect on the connection under test.
178
+ #
179
+ # Accepts request path as the first argument and the following request options:
180
+ #
181
+ # - params – url parameters (Hash)
182
+ # - headers – request headers (Hash)
183
+ # - session – session data (Hash)
184
+ # - env – additional Rack env configuration (Hash)
185
+ def connect(path = ActionCable.server.config.mount_path, **request_params)
186
+ path ||= DEFAULT_PATH
187
+
188
+ connection = self.class.connection_class.allocate
189
+ connection.singleton_class.include(TestConnection)
190
+ connection.send(:initialize, build_test_request(path, request_params))
191
+ connection.connect if connection.respond_to?(:connect)
192
+
193
+ # Only set instance variable if connected successfully
194
+ @connection = connection
195
+ end
196
+
197
+ # Exert #disconnect on the connection under test.
198
+ def disconnect
199
+ raise "Must be connected!" if connection.nil?
200
+
201
+ connection.disconnect if connection.respond_to?(:disconnect)
202
+ @connection = nil
203
+ end
204
+
205
+ def cookies
206
+ @cookie_jar ||= TestCookieJar.new
207
+ end
208
+
209
+ private
210
+ def build_test_request(path, params: nil, headers: {}, session: {}, env: {})
211
+ wrapped_headers = ActionDispatch::Http::Headers.from_hash(headers)
212
+
213
+ uri = URI.parse(path)
214
+
215
+ query_string = params.nil? ? uri.query : params.to_query
216
+
217
+ request_env = {
218
+ "QUERY_STRING" => query_string,
219
+ "PATH_INFO" => uri.path
220
+ }.merge(env)
221
+
222
+ if wrapped_headers.present?
223
+ ActionDispatch::Http::Headers.from_hash(request_env).merge!(wrapped_headers)
224
+ end
225
+
226
+ TestRequest.create(request_env).tap do |request|
227
+ request.session = session.with_indifferent_access
228
+ request.cookie_jar = cookies
229
+ end
230
+ end
231
+ end
232
+
233
+ include Behavior
234
+ end
235
+ end
236
+ end
@@ -34,9 +34,7 @@ module ActionCable
34
34
  websocket.rack_response
35
35
  end
36
36
 
37
- # TODO Change this to private once we've dropped Ruby 2.2 support.
38
- # Workaround for Ruby 2.2 "private attribute?" warning.
39
- protected
37
+ private
40
38
  attr_reader :websocket
41
39
  end
42
40
  end
@@ -15,6 +15,7 @@ module ActionCable
15
15
  autoload :StreamEventLoop
16
16
  autoload :Subscriptions
17
17
  autoload :TaggedLoggerProxy
18
+ autoload :TestCase
18
19
  autoload :WebSocket
19
20
  end
20
21
  end
@@ -7,10 +7,10 @@ module ActionCable
7
7
  end
8
8
 
9
9
  module VERSION
10
- MAJOR = 5
11
- MINOR = 2
12
- TINY = 8
13
- PRE = "1"
10
+ MAJOR = 6
11
+ MINOR = 0
12
+ TINY = 0
13
+ PRE = "beta1"
14
14
 
15
15
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
16
16
  end