actioncable 5.2.5 → 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 -67
  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.rb +15 -7
  7. data/lib/action_cable/channel.rb +1 -0
  8. data/lib/action_cable/channel/base.rb +5 -1
  9. data/lib/action_cable/channel/test_case.rb +312 -0
  10. data/lib/action_cable/connection.rb +1 -0
  11. data/lib/action_cable/connection/authorization.rb +1 -1
  12. data/lib/action_cable/connection/base.rb +9 -7
  13. data/lib/action_cable/connection/message_buffer.rb +1 -4
  14. data/lib/action_cable/connection/stream.rb +4 -2
  15. data/lib/action_cable/connection/subscriptions.rb +1 -5
  16. data/lib/action_cable/connection/test_case.rb +236 -0
  17. data/lib/action_cable/connection/web_socket.rb +1 -3
  18. data/lib/action_cable/gem_version.rb +4 -4
  19. data/lib/action_cable/server/base.rb +3 -1
  20. data/lib/action_cable/server/worker.rb +5 -7
  21. data/lib/action_cable/subscription_adapter.rb +1 -0
  22. data/lib/action_cable/subscription_adapter/postgresql.rb +24 -8
  23. data/lib/action_cable/subscription_adapter/redis.rb +2 -1
  24. data/lib/action_cable/subscription_adapter/test.rb +40 -0
  25. data/lib/action_cable/test_case.rb +11 -0
  26. data/lib/action_cable/test_helper.rb +133 -0
  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 +21 -14
  35. data/lib/assets/compiled/action_cable.js +0 -601
  36. data/lib/rails/generators/channel/templates/assets/channel.coffee.tt +0 -14
@@ -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
@@ -7,10 +7,10 @@ module ActionCable
7
7
  end
8
8
 
9
9
  module VERSION
10
- MAJOR = 5
11
- MINOR = 2
12
- TINY = 5
13
- PRE = nil
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
@@ -36,7 +36,9 @@ module ActionCable
36
36
  end
37
37
 
38
38
  def restart
39
- connections.each(&:close)
39
+ connections.each do |connection|
40
+ connection.close(reason: ActionCable::INTERNAL[:disconnect_reasons][:server_restart])
41
+ end
40
42
 
41
43
  @mutex.synchronize do
42
44
  # Shutdown the worker pool
@@ -56,14 +56,12 @@ module ActionCable
56
56
 
57
57
  def invoke(receiver, method, *args, connection:, &block)
58
58
  work(connection) do
59
- begin
60
- receiver.send method, *args, &block
61
- rescue Exception => e
62
- logger.error "There was an exception - #{e.class}(#{e.message})"
63
- logger.error e.backtrace.join("\n")
59
+ receiver.send method, *args, &block
60
+ rescue Exception => e
61
+ logger.error "There was an exception - #{e.class}(#{e.message})"
62
+ logger.error e.backtrace.join("\n")
64
63
 
65
- receiver.handle_exception if receiver.respond_to?(:handle_exception)
66
- end
64
+ receiver.handle_exception if receiver.respond_to?(:handle_exception)
67
65
  end
68
66
  end
69
67
 
@@ -5,6 +5,7 @@ module ActionCable
5
5
  extend ActiveSupport::Autoload
6
6
 
7
7
  autoload :Base
8
+ autoload :Test
8
9
  autoload :SubscriberMap
9
10
  autoload :ChannelPrefix
10
11
  end
@@ -14,7 +14,7 @@ module ActionCable
14
14
  end
15
15
 
16
16
  def broadcast(channel, payload)
17
- with_connection do |pg_conn|
17
+ with_broadcast_connection do |pg_conn|
18
18
  pg_conn.exec("NOTIFY #{pg_conn.escape_identifier(channel_identifier(channel))}, '#{pg_conn.escape_string(payload)}'")
19
19
  end
20
20
  end
@@ -31,14 +31,24 @@ module ActionCable
31
31
  listener.shutdown
32
32
  end
33
33
 
34
- def with_connection(&block) # :nodoc:
35
- ActiveRecord::Base.connection_pool.with_connection do |ar_conn|
36
- pg_conn = ar_conn.raw_connection
34
+ def with_subscriptions_connection(&block) # :nodoc:
35
+ ar_conn = ActiveRecord::Base.connection_pool.checkout.tap do |conn|
36
+ # Action Cable is taking ownership over this database connection, and
37
+ # will perform the necessary cleanup tasks
38
+ ActiveRecord::Base.connection_pool.remove(conn)
39
+ end
40
+ pg_conn = ar_conn.raw_connection
37
41
 
38
- unless pg_conn.is_a?(PG::Connection)
39
- raise "The Active Record database must be PostgreSQL in order to use the PostgreSQL Action Cable storage adapter"
40
- end
42
+ verify!(pg_conn)
43
+ yield pg_conn
44
+ ensure
45
+ ar_conn.disconnect!
46
+ end
41
47
 
48
+ def with_broadcast_connection(&block) # :nodoc:
49
+ ActiveRecord::Base.connection_pool.with_connection do |ar_conn|
50
+ pg_conn = ar_conn.raw_connection
51
+ verify!(pg_conn)
42
52
  yield pg_conn
43
53
  end
44
54
  end
@@ -52,6 +62,12 @@ module ActionCable
52
62
  @listener || @server.mutex.synchronize { @listener ||= Listener.new(self, @server.event_loop) }
53
63
  end
54
64
 
65
+ def verify!(pg_conn)
66
+ unless pg_conn.is_a?(PG::Connection)
67
+ raise "The Active Record database must be PostgreSQL in order to use the PostgreSQL Action Cable storage adapter"
68
+ end
69
+ end
70
+
55
71
  class Listener < SubscriberMap
56
72
  def initialize(adapter, event_loop)
57
73
  super()
@@ -67,7 +83,7 @@ module ActionCable
67
83
  end
68
84
 
69
85
  def listen
70
- @adapter.with_connection do |pg_conn|
86
+ @adapter.with_subscriptions_connection do |pg_conn|
71
87
  catch :shutdown do
72
88
  loop do
73
89
  until @queue.empty?
@@ -13,7 +13,8 @@ module ActionCable
13
13
  # Overwrite this factory method for Redis connections if you want to use a different Redis library than the redis gem.
14
14
  # This is needed, for example, when using Makara proxies for distributed Redis.
15
15
  cattr_accessor :redis_connector, default: ->(config) do
16
- ::Redis.new(config.slice(:url, :host, :port, :db, :password))
16
+ config[:id] ||= "ActionCable-PID-#{$$}"
17
+ ::Redis.new(config.slice(:url, :host, :port, :db, :password, :id))
17
18
  end
18
19
 
19
20
  def initialize(*)
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "async"
4
+
5
+ module ActionCable
6
+ module SubscriptionAdapter
7
+ # == Test adapter for Action Cable
8
+ #
9
+ # The test adapter should be used only in testing. Along with
10
+ # <tt>ActionCable::TestHelper</tt> it makes a great tool to test your Rails application.
11
+ #
12
+ # To use the test adapter set +adapter+ value to +test+ in your +config/cable.yml+ file.
13
+ #
14
+ # NOTE: Test adapter extends the <tt>ActionCable::SubscriptionsAdapter::Async</tt> adapter,
15
+ # so it could be used in system tests too.
16
+ class Test < Async
17
+ def broadcast(channel, payload)
18
+ broadcasts(channel) << payload
19
+ super
20
+ end
21
+
22
+ def broadcasts(channel)
23
+ channels_data[channel] ||= []
24
+ end
25
+
26
+ def clear_messages(channel)
27
+ channels_data[channel] = []
28
+ end
29
+
30
+ def clear
31
+ @channels_data = nil
32
+ end
33
+
34
+ private
35
+ def channels_data
36
+ @channels_data ||= {}
37
+ end
38
+ end
39
+ end
40
+ end