actioncable 5.2.4.4 → 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 -52
  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 = 4
13
- PRE = "4"
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