actioncable 5.2.6.2 → 6.0.0.beta1

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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +58 -87
  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 = 6
13
- PRE = "2"
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