actioncable 6.0.0

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 (61) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +169 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.md +24 -0
  5. data/app/assets/javascripts/action_cable.js +517 -0
  6. data/lib/action_cable.rb +62 -0
  7. data/lib/action_cable/channel.rb +17 -0
  8. data/lib/action_cable/channel/base.rb +311 -0
  9. data/lib/action_cable/channel/broadcasting.rb +41 -0
  10. data/lib/action_cable/channel/callbacks.rb +37 -0
  11. data/lib/action_cable/channel/naming.rb +25 -0
  12. data/lib/action_cable/channel/periodic_timers.rb +78 -0
  13. data/lib/action_cable/channel/streams.rb +176 -0
  14. data/lib/action_cable/channel/test_case.rb +310 -0
  15. data/lib/action_cable/connection.rb +22 -0
  16. data/lib/action_cable/connection/authorization.rb +15 -0
  17. data/lib/action_cable/connection/base.rb +264 -0
  18. data/lib/action_cable/connection/client_socket.rb +157 -0
  19. data/lib/action_cable/connection/identification.rb +47 -0
  20. data/lib/action_cable/connection/internal_channel.rb +45 -0
  21. data/lib/action_cable/connection/message_buffer.rb +54 -0
  22. data/lib/action_cable/connection/stream.rb +117 -0
  23. data/lib/action_cable/connection/stream_event_loop.rb +136 -0
  24. data/lib/action_cable/connection/subscriptions.rb +79 -0
  25. data/lib/action_cable/connection/tagged_logger_proxy.rb +42 -0
  26. data/lib/action_cable/connection/test_case.rb +234 -0
  27. data/lib/action_cable/connection/web_socket.rb +41 -0
  28. data/lib/action_cable/engine.rb +79 -0
  29. data/lib/action_cable/gem_version.rb +17 -0
  30. data/lib/action_cable/helpers/action_cable_helper.rb +42 -0
  31. data/lib/action_cable/remote_connections.rb +71 -0
  32. data/lib/action_cable/server.rb +17 -0
  33. data/lib/action_cable/server/base.rb +94 -0
  34. data/lib/action_cable/server/broadcasting.rb +54 -0
  35. data/lib/action_cable/server/configuration.rb +56 -0
  36. data/lib/action_cable/server/connections.rb +36 -0
  37. data/lib/action_cable/server/worker.rb +75 -0
  38. data/lib/action_cable/server/worker/active_record_connection_management.rb +21 -0
  39. data/lib/action_cable/subscription_adapter.rb +12 -0
  40. data/lib/action_cable/subscription_adapter/async.rb +29 -0
  41. data/lib/action_cable/subscription_adapter/base.rb +30 -0
  42. data/lib/action_cable/subscription_adapter/channel_prefix.rb +28 -0
  43. data/lib/action_cable/subscription_adapter/inline.rb +37 -0
  44. data/lib/action_cable/subscription_adapter/postgresql.rb +132 -0
  45. data/lib/action_cable/subscription_adapter/redis.rb +181 -0
  46. data/lib/action_cable/subscription_adapter/subscriber_map.rb +59 -0
  47. data/lib/action_cable/subscription_adapter/test.rb +40 -0
  48. data/lib/action_cable/test_case.rb +11 -0
  49. data/lib/action_cable/test_helper.rb +133 -0
  50. data/lib/action_cable/version.rb +10 -0
  51. data/lib/rails/generators/channel/USAGE +13 -0
  52. data/lib/rails/generators/channel/channel_generator.rb +52 -0
  53. data/lib/rails/generators/channel/templates/application_cable/channel.rb.tt +4 -0
  54. data/lib/rails/generators/channel/templates/application_cable/connection.rb.tt +4 -0
  55. data/lib/rails/generators/channel/templates/channel.rb.tt +16 -0
  56. data/lib/rails/generators/channel/templates/javascript/channel.js.tt +20 -0
  57. data/lib/rails/generators/channel/templates/javascript/consumer.js.tt +6 -0
  58. data/lib/rails/generators/channel/templates/javascript/index.js.tt +5 -0
  59. data/lib/rails/generators/test_unit/channel_generator.rb +20 -0
  60. data/lib/rails/generators/test_unit/templates/channel_test.rb.tt +8 -0
  61. metadata +149 -0
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionCable
4
+ module Channel
5
+ module Naming
6
+ extend ActiveSupport::Concern
7
+
8
+ module ClassMethods
9
+ # Returns the name of the channel, underscored, without the <tt>Channel</tt> ending.
10
+ # If the channel is in a namespace, then the namespaces are represented by single
11
+ # colon separators in the channel name.
12
+ #
13
+ # ChatChannel.channel_name # => 'chat'
14
+ # Chats::AppearancesChannel.channel_name # => 'chats:appearances'
15
+ # FooChats::BarAppearancesChannel.channel_name # => 'foo_chats:bar_appearances'
16
+ def channel_name
17
+ @channel_name ||= name.sub(/Channel$/, "").gsub("::", ":").underscore
18
+ end
19
+ end
20
+
21
+ # Delegates to the class' <tt>channel_name</tt>
22
+ delegate :channel_name, to: :class
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionCable
4
+ module Channel
5
+ module PeriodicTimers
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ class_attribute :periodic_timers, instance_reader: false, default: []
10
+
11
+ after_subscribe :start_periodic_timers
12
+ after_unsubscribe :stop_periodic_timers
13
+ end
14
+
15
+ module ClassMethods
16
+ # Periodically performs a task on the channel, like updating an online
17
+ # user counter, polling a backend for new status messages, sending
18
+ # regular "heartbeat" messages, or doing some internal work and giving
19
+ # progress updates.
20
+ #
21
+ # Pass a method name or lambda argument or provide a block to call.
22
+ # Specify the calling period in seconds using the <tt>every:</tt>
23
+ # keyword argument.
24
+ #
25
+ # periodically :transmit_progress, every: 5.seconds
26
+ #
27
+ # periodically every: 3.minutes do
28
+ # transmit action: :update_count, count: current_count
29
+ # end
30
+ #
31
+ def periodically(callback_or_method_name = nil, every:, &block)
32
+ callback =
33
+ if block_given?
34
+ raise ArgumentError, "Pass a block or provide a callback arg, not both" if callback_or_method_name
35
+ block
36
+ else
37
+ case callback_or_method_name
38
+ when Proc
39
+ callback_or_method_name
40
+ when Symbol
41
+ -> { __send__ callback_or_method_name }
42
+ else
43
+ raise ArgumentError, "Expected a Symbol method name or a Proc, got #{callback_or_method_name.inspect}"
44
+ end
45
+ end
46
+
47
+ unless every.kind_of?(Numeric) && every > 0
48
+ raise ArgumentError, "Expected every: to be a positive number of seconds, got #{every.inspect}"
49
+ end
50
+
51
+ self.periodic_timers += [[ callback, every: every ]]
52
+ end
53
+ end
54
+
55
+ private
56
+ def active_periodic_timers
57
+ @active_periodic_timers ||= []
58
+ end
59
+
60
+ def start_periodic_timers
61
+ self.class.periodic_timers.each do |callback, options|
62
+ active_periodic_timers << start_periodic_timer(callback, every: options.fetch(:every))
63
+ end
64
+ end
65
+
66
+ def start_periodic_timer(callback, every:)
67
+ connection.server.event_loop.timer every do
68
+ connection.worker_pool.async_exec self, connection: connection, &callback
69
+ end
70
+ end
71
+
72
+ def stop_periodic_timers
73
+ active_periodic_timers.each { |timer| timer.shutdown }
74
+ active_periodic_timers.clear
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,176 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionCable
4
+ module Channel
5
+ # Streams allow channels to route broadcastings to the subscriber. A broadcasting is, as discussed elsewhere, a pubsub queue where any data
6
+ # placed into it is automatically sent to the clients that are connected at that time. It's purely an online queue, though. If you're not
7
+ # streaming a broadcasting at the very moment it sends out an update, you will not get that update, even if you connect after it has been sent.
8
+ #
9
+ # Most commonly, the streamed broadcast is sent straight to the subscriber on the client-side. The channel just acts as a connector between
10
+ # the two parties (the broadcaster and the channel subscriber). Here's an example of a channel that allows subscribers to get all new
11
+ # comments on a given page:
12
+ #
13
+ # class CommentsChannel < ApplicationCable::Channel
14
+ # def follow(data)
15
+ # stream_from "comments_for_#{data['recording_id']}"
16
+ # end
17
+ #
18
+ # def unfollow
19
+ # stop_all_streams
20
+ # end
21
+ # end
22
+ #
23
+ # Based on the above example, the subscribers of this channel will get whatever data is put into the,
24
+ # let's say, <tt>comments_for_45</tt> broadcasting as soon as it's put there.
25
+ #
26
+ # An example broadcasting for this channel looks like so:
27
+ #
28
+ # ActionCable.server.broadcast "comments_for_45", author: 'DHH', content: 'Rails is just swell'
29
+ #
30
+ # If you have a stream that is related to a model, then the broadcasting used can be generated from the model and channel.
31
+ # The following example would subscribe to a broadcasting like <tt>comments:Z2lkOi8vVGVzdEFwcC9Qb3N0LzE</tt>.
32
+ #
33
+ # class CommentsChannel < ApplicationCable::Channel
34
+ # def subscribed
35
+ # post = Post.find(params[:id])
36
+ # stream_for post
37
+ # end
38
+ # end
39
+ #
40
+ # You can then broadcast to this channel using:
41
+ #
42
+ # CommentsChannel.broadcast_to(@post, @comment)
43
+ #
44
+ # If you don't just want to parlay the broadcast unfiltered to the subscriber, you can also supply a callback that lets you alter what is sent out.
45
+ # The below example shows how you can use this to provide performance introspection in the process:
46
+ #
47
+ # class ChatChannel < ApplicationCable::Channel
48
+ # def subscribed
49
+ # @room = Chat::Room[params[:room_number]]
50
+ #
51
+ # stream_for @room, coder: ActiveSupport::JSON do |message|
52
+ # if message['originated_at'].present?
53
+ # elapsed_time = (Time.now.to_f - message['originated_at']).round(2)
54
+ #
55
+ # ActiveSupport::Notifications.instrument :performance, measurement: 'Chat.message_delay', value: elapsed_time, action: :timing
56
+ # logger.info "Message took #{elapsed_time}s to arrive"
57
+ # end
58
+ #
59
+ # transmit message
60
+ # end
61
+ # end
62
+ # end
63
+ #
64
+ # You can stop streaming from all broadcasts by calling #stop_all_streams.
65
+ module Streams
66
+ extend ActiveSupport::Concern
67
+
68
+ included do
69
+ on_unsubscribe :stop_all_streams
70
+ end
71
+
72
+ # Start streaming from the named <tt>broadcasting</tt> pubsub queue. Optionally, you can pass a <tt>callback</tt> that'll be used
73
+ # instead of the default of just transmitting the updates straight to the subscriber.
74
+ # Pass <tt>coder: ActiveSupport::JSON</tt> to decode messages as JSON before passing to the callback.
75
+ # Defaults to <tt>coder: nil</tt> which does no decoding, passes raw messages.
76
+ def stream_from(broadcasting, callback = nil, coder: nil, &block)
77
+ broadcasting = String(broadcasting)
78
+
79
+ # Don't send the confirmation until pubsub#subscribe is successful
80
+ defer_subscription_confirmation!
81
+
82
+ # Build a stream handler by wrapping the user-provided callback with
83
+ # a decoder or defaulting to a JSON-decoding retransmitter.
84
+ handler = worker_pool_stream_handler(broadcasting, callback || block, coder: coder)
85
+ streams << [ broadcasting, handler ]
86
+
87
+ connection.server.event_loop.post do
88
+ pubsub.subscribe(broadcasting, handler, lambda do
89
+ ensure_confirmation_sent
90
+ logger.info "#{self.class.name} is streaming from #{broadcasting}"
91
+ end)
92
+ end
93
+ end
94
+
95
+ # Start streaming the pubsub queue for the <tt>model</tt> in this channel. Optionally, you can pass a
96
+ # <tt>callback</tt> that'll be used instead of the default of just transmitting the updates straight
97
+ # to the subscriber.
98
+ #
99
+ # Pass <tt>coder: ActiveSupport::JSON</tt> to decode messages as JSON before passing to the callback.
100
+ # Defaults to <tt>coder: nil</tt> which does no decoding, passes raw messages.
101
+ def stream_for(model, callback = nil, coder: nil, &block)
102
+ stream_from(broadcasting_for(model), callback || block, coder: coder)
103
+ end
104
+
105
+ # Unsubscribes all streams associated with this channel from the pubsub queue.
106
+ def stop_all_streams
107
+ streams.each do |broadcasting, callback|
108
+ pubsub.unsubscribe broadcasting, callback
109
+ logger.info "#{self.class.name} stopped streaming from #{broadcasting}"
110
+ end.clear
111
+ end
112
+
113
+ private
114
+ delegate :pubsub, to: :connection
115
+
116
+ def streams
117
+ @_streams ||= []
118
+ end
119
+
120
+ # Always wrap the outermost handler to invoke the user handler on the
121
+ # worker pool rather than blocking the event loop.
122
+ def worker_pool_stream_handler(broadcasting, user_handler, coder: nil)
123
+ handler = stream_handler(broadcasting, user_handler, coder: coder)
124
+
125
+ -> message do
126
+ connection.worker_pool.async_invoke handler, :call, message, connection: connection
127
+ end
128
+ end
129
+
130
+ # May be overridden to add instrumentation, logging, specialized error
131
+ # handling, or other forms of handler decoration.
132
+ #
133
+ # TODO: Tests demonstrating this.
134
+ def stream_handler(broadcasting, user_handler, coder: nil)
135
+ if user_handler
136
+ stream_decoder user_handler, coder: coder
137
+ else
138
+ default_stream_handler broadcasting, coder: coder
139
+ end
140
+ end
141
+
142
+ # May be overridden to change the default stream handling behavior
143
+ # which decodes JSON and transmits to the client.
144
+ #
145
+ # TODO: Tests demonstrating this.
146
+ #
147
+ # TODO: Room for optimization. Update transmit API to be coder-aware
148
+ # so we can no-op when pubsub and connection are both JSON-encoded.
149
+ # Then we can skip decode+encode if we're just proxying messages.
150
+ def default_stream_handler(broadcasting, coder:)
151
+ coder ||= ActiveSupport::JSON
152
+ stream_transmitter stream_decoder(coder: coder), broadcasting: broadcasting
153
+ end
154
+
155
+ def stream_decoder(handler = identity_handler, coder:)
156
+ if coder
157
+ -> message { handler.(coder.decode(message)) }
158
+ else
159
+ handler
160
+ end
161
+ end
162
+
163
+ def stream_transmitter(handler = identity_handler, broadcasting:)
164
+ via = "streamed from #{broadcasting}"
165
+
166
+ -> (message) do
167
+ transmit handler.(message), via: via
168
+ end
169
+ end
170
+
171
+ def identity_handler
172
+ -> message { message }
173
+ end
174
+ end
175
+ end
176
+ end
@@ -0,0 +1,310 @@
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(stream_or_object)
304
+ end
305
+ end
306
+
307
+ include Behavior
308
+ end
309
+ end
310
+ end