omg-actioncable 8.0.0.alpha2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/MIT-LICENSE +20 -0
- data/README.md +24 -0
- data/app/assets/javascripts/action_cable.js +511 -0
- data/app/assets/javascripts/actioncable.esm.js +512 -0
- data/app/assets/javascripts/actioncable.js +510 -0
- data/lib/action_cable/channel/base.rb +335 -0
- data/lib/action_cable/channel/broadcasting.rb +50 -0
- data/lib/action_cable/channel/callbacks.rb +76 -0
- data/lib/action_cable/channel/naming.rb +28 -0
- data/lib/action_cable/channel/periodic_timers.rb +78 -0
- data/lib/action_cable/channel/streams.rb +215 -0
- data/lib/action_cable/channel/test_case.rb +356 -0
- data/lib/action_cable/connection/authorization.rb +18 -0
- data/lib/action_cable/connection/base.rb +294 -0
- data/lib/action_cable/connection/callbacks.rb +57 -0
- data/lib/action_cable/connection/client_socket.rb +159 -0
- data/lib/action_cable/connection/identification.rb +51 -0
- data/lib/action_cable/connection/internal_channel.rb +50 -0
- data/lib/action_cable/connection/message_buffer.rb +57 -0
- data/lib/action_cable/connection/stream.rb +117 -0
- data/lib/action_cable/connection/stream_event_loop.rb +136 -0
- data/lib/action_cable/connection/subscriptions.rb +85 -0
- data/lib/action_cable/connection/tagged_logger_proxy.rb +47 -0
- data/lib/action_cable/connection/test_case.rb +246 -0
- data/lib/action_cable/connection/web_socket.rb +45 -0
- data/lib/action_cable/deprecator.rb +9 -0
- data/lib/action_cable/engine.rb +98 -0
- data/lib/action_cable/gem_version.rb +19 -0
- data/lib/action_cable/helpers/action_cable_helper.rb +45 -0
- data/lib/action_cable/remote_connections.rb +82 -0
- data/lib/action_cable/server/base.rb +109 -0
- data/lib/action_cable/server/broadcasting.rb +62 -0
- data/lib/action_cable/server/configuration.rb +70 -0
- data/lib/action_cable/server/connections.rb +44 -0
- data/lib/action_cable/server/worker/active_record_connection_management.rb +23 -0
- data/lib/action_cable/server/worker.rb +75 -0
- data/lib/action_cable/subscription_adapter/async.rb +29 -0
- data/lib/action_cable/subscription_adapter/base.rb +36 -0
- data/lib/action_cable/subscription_adapter/channel_prefix.rb +30 -0
- data/lib/action_cable/subscription_adapter/inline.rb +39 -0
- data/lib/action_cable/subscription_adapter/postgresql.rb +134 -0
- data/lib/action_cable/subscription_adapter/redis.rb +256 -0
- data/lib/action_cable/subscription_adapter/subscriber_map.rb +61 -0
- data/lib/action_cable/subscription_adapter/test.rb +41 -0
- data/lib/action_cable/test_case.rb +13 -0
- data/lib/action_cable/test_helper.rb +163 -0
- data/lib/action_cable/version.rb +12 -0
- data/lib/action_cable.rb +80 -0
- data/lib/rails/generators/channel/USAGE +19 -0
- data/lib/rails/generators/channel/channel_generator.rb +127 -0
- data/lib/rails/generators/channel/templates/application_cable/channel.rb.tt +4 -0
- data/lib/rails/generators/channel/templates/application_cable/connection.rb.tt +4 -0
- data/lib/rails/generators/channel/templates/channel.rb.tt +16 -0
- data/lib/rails/generators/channel/templates/javascript/channel.js.tt +20 -0
- data/lib/rails/generators/channel/templates/javascript/consumer.js.tt +6 -0
- data/lib/rails/generators/channel/templates/javascript/index.js.tt +1 -0
- data/lib/rails/generators/test_unit/channel_generator.rb +22 -0
- data/lib/rails/generators/test_unit/templates/channel_test.rb.tt +8 -0
- metadata +181 -0
@@ -0,0 +1,215 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# :markup: markdown
|
4
|
+
|
5
|
+
module ActionCable
|
6
|
+
module Channel
|
7
|
+
# # Action Cable Channel Streams
|
8
|
+
#
|
9
|
+
# Streams allow channels to route broadcastings to the subscriber. A
|
10
|
+
# broadcasting is, as discussed elsewhere, a pubsub queue where any data placed
|
11
|
+
# into it is automatically sent to the clients that are connected at that time.
|
12
|
+
# It's purely an online queue, though. If you're not streaming a broadcasting at
|
13
|
+
# the very moment it sends out an update, you will not get that update, even if
|
14
|
+
# you connect after it has been sent.
|
15
|
+
#
|
16
|
+
# Most commonly, the streamed broadcast is sent straight to the subscriber on
|
17
|
+
# the client-side. The channel just acts as a connector between the two parties
|
18
|
+
# (the broadcaster and the channel subscriber). Here's an example of a channel
|
19
|
+
# that allows subscribers to get all new comments on a given page:
|
20
|
+
#
|
21
|
+
# class CommentsChannel < ApplicationCable::Channel
|
22
|
+
# def follow(data)
|
23
|
+
# stream_from "comments_for_#{data['recording_id']}"
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
# def unfollow
|
27
|
+
# stop_all_streams
|
28
|
+
# end
|
29
|
+
# end
|
30
|
+
#
|
31
|
+
# Based on the above example, the subscribers of this channel will get whatever
|
32
|
+
# data is put into the, let's say, `comments_for_45` broadcasting as soon as
|
33
|
+
# it's put there.
|
34
|
+
#
|
35
|
+
# An example broadcasting for this channel looks like so:
|
36
|
+
#
|
37
|
+
# ActionCable.server.broadcast "comments_for_45", { author: 'DHH', content: 'Rails is just swell' }
|
38
|
+
#
|
39
|
+
# If you have a stream that is related to a model, then the broadcasting used
|
40
|
+
# can be generated from the model and channel. The following example would
|
41
|
+
# subscribe to a broadcasting like `comments:Z2lkOi8vVGVzdEFwcC9Qb3N0LzE`.
|
42
|
+
#
|
43
|
+
# class CommentsChannel < ApplicationCable::Channel
|
44
|
+
# def subscribed
|
45
|
+
# post = Post.find(params[:id])
|
46
|
+
# stream_for post
|
47
|
+
# end
|
48
|
+
# end
|
49
|
+
#
|
50
|
+
# You can then broadcast to this channel using:
|
51
|
+
#
|
52
|
+
# CommentsChannel.broadcast_to(@post, @comment)
|
53
|
+
#
|
54
|
+
# If you don't just want to parlay the broadcast unfiltered to the subscriber,
|
55
|
+
# you can also supply a callback that lets you alter what is sent out. The below
|
56
|
+
# example shows how you can use this to provide performance introspection in the
|
57
|
+
# process:
|
58
|
+
#
|
59
|
+
# class ChatChannel < ApplicationCable::Channel
|
60
|
+
# def subscribed
|
61
|
+
# @room = Chat::Room[params[:room_number]]
|
62
|
+
#
|
63
|
+
# stream_for @room, coder: ActiveSupport::JSON do |message|
|
64
|
+
# if message['originated_at'].present?
|
65
|
+
# elapsed_time = (Time.now.to_f - message['originated_at']).round(2)
|
66
|
+
#
|
67
|
+
# ActiveSupport::Notifications.instrument :performance, measurement: 'Chat.message_delay', value: elapsed_time, action: :timing
|
68
|
+
# logger.info "Message took #{elapsed_time}s to arrive"
|
69
|
+
# end
|
70
|
+
#
|
71
|
+
# transmit message
|
72
|
+
# end
|
73
|
+
# end
|
74
|
+
# end
|
75
|
+
#
|
76
|
+
# You can stop streaming from all broadcasts by calling #stop_all_streams.
|
77
|
+
module Streams
|
78
|
+
extend ActiveSupport::Concern
|
79
|
+
|
80
|
+
included do
|
81
|
+
on_unsubscribe :stop_all_streams
|
82
|
+
end
|
83
|
+
|
84
|
+
# Start streaming from the named `broadcasting` pubsub queue. Optionally, you
|
85
|
+
# can pass a `callback` that'll be used instead of the default of just
|
86
|
+
# transmitting the updates straight to the subscriber. Pass `coder:
|
87
|
+
# ActiveSupport::JSON` to decode messages as JSON before passing to the
|
88
|
+
# callback. Defaults to `coder: nil` which does no decoding, passes raw
|
89
|
+
# messages.
|
90
|
+
def stream_from(broadcasting, callback = nil, coder: nil, &block)
|
91
|
+
broadcasting = String(broadcasting)
|
92
|
+
|
93
|
+
# Don't send the confirmation until pubsub#subscribe is successful
|
94
|
+
defer_subscription_confirmation!
|
95
|
+
|
96
|
+
# Build a stream handler by wrapping the user-provided callback with a decoder
|
97
|
+
# or defaulting to a JSON-decoding retransmitter.
|
98
|
+
handler = worker_pool_stream_handler(broadcasting, callback || block, coder: coder)
|
99
|
+
streams[broadcasting] = handler
|
100
|
+
|
101
|
+
connection.server.event_loop.post do
|
102
|
+
pubsub.subscribe(broadcasting, handler, lambda do
|
103
|
+
ensure_confirmation_sent
|
104
|
+
logger.info "#{self.class.name} is streaming from #{broadcasting}"
|
105
|
+
end)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
# Start streaming the pubsub queue for the `model` in this channel. Optionally,
|
110
|
+
# you can pass a `callback` that'll be used instead of the default of just
|
111
|
+
# transmitting the updates straight to the subscriber.
|
112
|
+
#
|
113
|
+
# Pass `coder: ActiveSupport::JSON` to decode messages as JSON before passing to
|
114
|
+
# the callback. Defaults to `coder: nil` which does no decoding, passes raw
|
115
|
+
# messages.
|
116
|
+
def stream_for(model, callback = nil, coder: nil, &block)
|
117
|
+
stream_from(broadcasting_for(model), callback || block, coder: coder)
|
118
|
+
end
|
119
|
+
|
120
|
+
# Unsubscribes streams from the named `broadcasting`.
|
121
|
+
def stop_stream_from(broadcasting)
|
122
|
+
callback = streams.delete(broadcasting)
|
123
|
+
if callback
|
124
|
+
pubsub.unsubscribe(broadcasting, callback)
|
125
|
+
logger.info "#{self.class.name} stopped streaming from #{broadcasting}"
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
# Unsubscribes streams for the `model`.
|
130
|
+
def stop_stream_for(model)
|
131
|
+
stop_stream_from(broadcasting_for(model))
|
132
|
+
end
|
133
|
+
|
134
|
+
# Unsubscribes all streams associated with this channel from the pubsub queue.
|
135
|
+
def stop_all_streams
|
136
|
+
streams.each do |broadcasting, callback|
|
137
|
+
pubsub.unsubscribe broadcasting, callback
|
138
|
+
logger.info "#{self.class.name} stopped streaming from #{broadcasting}"
|
139
|
+
end.clear
|
140
|
+
end
|
141
|
+
|
142
|
+
# Calls stream_for with the given `model` if it's present to start streaming,
|
143
|
+
# otherwise rejects the subscription.
|
144
|
+
def stream_or_reject_for(model)
|
145
|
+
if model
|
146
|
+
stream_for model
|
147
|
+
else
|
148
|
+
reject
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
private
|
153
|
+
delegate :pubsub, to: :connection
|
154
|
+
|
155
|
+
def streams
|
156
|
+
@_streams ||= {}
|
157
|
+
end
|
158
|
+
|
159
|
+
# Always wrap the outermost handler to invoke the user handler on the worker
|
160
|
+
# pool rather than blocking the event loop.
|
161
|
+
def worker_pool_stream_handler(broadcasting, user_handler, coder: nil)
|
162
|
+
handler = stream_handler(broadcasting, user_handler, coder: coder)
|
163
|
+
|
164
|
+
-> message do
|
165
|
+
connection.worker_pool.async_invoke handler, :call, message, connection: connection
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
# May be overridden to add instrumentation, logging, specialized error handling,
|
170
|
+
# or other forms of handler decoration.
|
171
|
+
#
|
172
|
+
# TODO: Tests demonstrating this.
|
173
|
+
def stream_handler(broadcasting, user_handler, coder: nil)
|
174
|
+
if user_handler
|
175
|
+
stream_decoder user_handler, coder: coder
|
176
|
+
else
|
177
|
+
default_stream_handler broadcasting, coder: coder
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
# May be overridden to change the default stream handling behavior which decodes
|
182
|
+
# JSON and transmits to the client.
|
183
|
+
#
|
184
|
+
# TODO: Tests demonstrating this.
|
185
|
+
#
|
186
|
+
# TODO: Room for optimization. Update transmit API to be coder-aware so we can
|
187
|
+
# no-op when pubsub and connection are both JSON-encoded. Then we can skip
|
188
|
+
# decode+encode if we're just proxying messages.
|
189
|
+
def default_stream_handler(broadcasting, coder:)
|
190
|
+
coder ||= ActiveSupport::JSON
|
191
|
+
stream_transmitter stream_decoder(coder: coder), broadcasting: broadcasting
|
192
|
+
end
|
193
|
+
|
194
|
+
def stream_decoder(handler = identity_handler, coder:)
|
195
|
+
if coder
|
196
|
+
-> message { handler.(coder.decode(message)) }
|
197
|
+
else
|
198
|
+
handler
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
def stream_transmitter(handler = identity_handler, broadcasting:)
|
203
|
+
via = "streamed from #{broadcasting}"
|
204
|
+
|
205
|
+
-> (message) do
|
206
|
+
transmit handler.(message), via: via
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
def identity_handler
|
211
|
+
-> message { message }
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
@@ -0,0 +1,356 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# :markup: markdown
|
4
|
+
|
5
|
+
require "active_support"
|
6
|
+
require "active_support/test_case"
|
7
|
+
require "active_support/core_ext/hash/indifferent_access"
|
8
|
+
require "json"
|
9
|
+
|
10
|
+
module ActionCable
|
11
|
+
module Channel
|
12
|
+
class NonInferrableChannelError < ::StandardError
|
13
|
+
def initialize(name)
|
14
|
+
super "Unable to determine the channel to test from #{name}. " +
|
15
|
+
"You'll need to specify it using `tests YourChannel` in your " +
|
16
|
+
"test case definition."
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# # Action Cable Channel Stub
|
21
|
+
#
|
22
|
+
# Stub `stream_from` to track streams for the channel. Add public aliases for
|
23
|
+
# `subscription_confirmation_sent?` and `subscription_rejected?`.
|
24
|
+
module ChannelStub
|
25
|
+
def confirmed?
|
26
|
+
subscription_confirmation_sent?
|
27
|
+
end
|
28
|
+
|
29
|
+
def rejected?
|
30
|
+
subscription_rejected?
|
31
|
+
end
|
32
|
+
|
33
|
+
def stream_from(broadcasting, *)
|
34
|
+
streams << broadcasting
|
35
|
+
end
|
36
|
+
|
37
|
+
def stop_all_streams
|
38
|
+
@_streams = []
|
39
|
+
end
|
40
|
+
|
41
|
+
def streams
|
42
|
+
@_streams ||= []
|
43
|
+
end
|
44
|
+
|
45
|
+
# Make periodic timers no-op
|
46
|
+
def start_periodic_timers; end
|
47
|
+
alias stop_periodic_timers start_periodic_timers
|
48
|
+
end
|
49
|
+
|
50
|
+
class ConnectionStub
|
51
|
+
attr_reader :server, :transmissions, :identifiers, :subscriptions, :logger
|
52
|
+
|
53
|
+
delegate :pubsub, :config, to: :server
|
54
|
+
|
55
|
+
def initialize(identifiers = {})
|
56
|
+
@server = ActionCable.server
|
57
|
+
@transmissions = []
|
58
|
+
|
59
|
+
identifiers.each do |identifier, val|
|
60
|
+
define_singleton_method(identifier) { val }
|
61
|
+
end
|
62
|
+
|
63
|
+
@subscriptions = ActionCable::Connection::Subscriptions.new(self)
|
64
|
+
@identifiers = identifiers.keys
|
65
|
+
@logger = ActiveSupport::TaggedLogging.new ActiveSupport::Logger.new(StringIO.new)
|
66
|
+
end
|
67
|
+
|
68
|
+
def transmit(cable_message)
|
69
|
+
transmissions << cable_message.with_indifferent_access
|
70
|
+
end
|
71
|
+
|
72
|
+
def connection_identifier
|
73
|
+
@connection_identifier ||= connection_gid(identifiers.filter_map { |id| send(id.to_sym) if id })
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
def connection_gid(ids)
|
78
|
+
ids.map do |o|
|
79
|
+
if o.respond_to?(:to_gid_param)
|
80
|
+
o.to_gid_param
|
81
|
+
else
|
82
|
+
o.to_s
|
83
|
+
end
|
84
|
+
end.sort.join(":")
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# Superclass for Action Cable channel functional tests.
|
89
|
+
#
|
90
|
+
# ## Basic example
|
91
|
+
#
|
92
|
+
# Functional tests are written as follows:
|
93
|
+
# 1. First, one uses the `subscribe` method to simulate subscription creation.
|
94
|
+
# 2. Then, one asserts whether the current state is as expected. "State" can be
|
95
|
+
# anything: transmitted messages, subscribed streams, etc.
|
96
|
+
#
|
97
|
+
#
|
98
|
+
# For example:
|
99
|
+
#
|
100
|
+
# class ChatChannelTest < ActionCable::Channel::TestCase
|
101
|
+
# def test_subscribed_with_room_number
|
102
|
+
# # Simulate a subscription creation
|
103
|
+
# subscribe room_number: 1
|
104
|
+
#
|
105
|
+
# # Asserts that the subscription was successfully created
|
106
|
+
# assert subscription.confirmed?
|
107
|
+
#
|
108
|
+
# # Asserts that the channel subscribes connection to a stream
|
109
|
+
# assert_has_stream "chat_1"
|
110
|
+
#
|
111
|
+
# # Asserts that the channel subscribes connection to a specific
|
112
|
+
# # stream created for a model
|
113
|
+
# assert_has_stream_for Room.find(1)
|
114
|
+
# end
|
115
|
+
#
|
116
|
+
# def test_does_not_stream_with_incorrect_room_number
|
117
|
+
# subscribe room_number: -1
|
118
|
+
#
|
119
|
+
# # Asserts that not streams was started
|
120
|
+
# assert_no_streams
|
121
|
+
# end
|
122
|
+
#
|
123
|
+
# def test_does_not_subscribe_without_room_number
|
124
|
+
# subscribe
|
125
|
+
#
|
126
|
+
# # Asserts that the subscription was rejected
|
127
|
+
# assert subscription.rejected?
|
128
|
+
# end
|
129
|
+
# end
|
130
|
+
#
|
131
|
+
# You can also perform actions:
|
132
|
+
# def test_perform_speak
|
133
|
+
# subscribe room_number: 1
|
134
|
+
#
|
135
|
+
# perform :speak, message: "Hello, Rails!"
|
136
|
+
#
|
137
|
+
# assert_equal "Hello, Rails!", transmissions.last["text"]
|
138
|
+
# end
|
139
|
+
#
|
140
|
+
# ## Special methods
|
141
|
+
#
|
142
|
+
# ActionCable::Channel::TestCase will also automatically provide the following
|
143
|
+
# instance methods for use in the tests:
|
144
|
+
#
|
145
|
+
# connection
|
146
|
+
# : An ActionCable::Channel::ConnectionStub, representing the current HTTP
|
147
|
+
# connection.
|
148
|
+
#
|
149
|
+
# subscription
|
150
|
+
# : An instance of the current channel, created when you call `subscribe`.
|
151
|
+
#
|
152
|
+
# transmissions
|
153
|
+
# : A list of all messages that have been transmitted into the channel.
|
154
|
+
#
|
155
|
+
#
|
156
|
+
# ## Channel is automatically inferred
|
157
|
+
#
|
158
|
+
# ActionCable::Channel::TestCase will automatically infer the channel under test
|
159
|
+
# from the test class name. If the channel cannot be inferred from the test
|
160
|
+
# class name, you can explicitly set it with `tests`.
|
161
|
+
#
|
162
|
+
# class SpecialEdgeCaseChannelTest < ActionCable::Channel::TestCase
|
163
|
+
# tests SpecialChannel
|
164
|
+
# end
|
165
|
+
#
|
166
|
+
# ## Specifying connection identifiers
|
167
|
+
#
|
168
|
+
# You need to set up your connection manually to provide values for the
|
169
|
+
# identifiers. To do this just use:
|
170
|
+
#
|
171
|
+
# stub_connection(user: users(:john))
|
172
|
+
#
|
173
|
+
# ## Testing broadcasting
|
174
|
+
#
|
175
|
+
# ActionCable::Channel::TestCase enhances ActionCable::TestHelper assertions
|
176
|
+
# (e.g. `assert_broadcasts`) to handle broadcasting to models:
|
177
|
+
#
|
178
|
+
# # in your channel
|
179
|
+
# def speak(data)
|
180
|
+
# broadcast_to room, text: data["message"]
|
181
|
+
# end
|
182
|
+
#
|
183
|
+
# def test_speak
|
184
|
+
# subscribe room_id: rooms(:chat).id
|
185
|
+
#
|
186
|
+
# assert_broadcast_on(rooms(:chat), text: "Hello, Rails!") do
|
187
|
+
# perform :speak, message: "Hello, Rails!"
|
188
|
+
# end
|
189
|
+
# end
|
190
|
+
class TestCase < ActiveSupport::TestCase
|
191
|
+
module Behavior
|
192
|
+
extend ActiveSupport::Concern
|
193
|
+
|
194
|
+
include ActiveSupport::Testing::ConstantLookup
|
195
|
+
include ActionCable::TestHelper
|
196
|
+
|
197
|
+
CHANNEL_IDENTIFIER = "test_stub"
|
198
|
+
|
199
|
+
included do
|
200
|
+
class_attribute :_channel_class
|
201
|
+
|
202
|
+
attr_reader :connection, :subscription
|
203
|
+
|
204
|
+
ActiveSupport.run_load_hooks(:action_cable_channel_test_case, self)
|
205
|
+
end
|
206
|
+
|
207
|
+
module ClassMethods
|
208
|
+
def tests(channel)
|
209
|
+
case channel
|
210
|
+
when String, Symbol
|
211
|
+
self._channel_class = channel.to_s.camelize.constantize
|
212
|
+
when Module
|
213
|
+
self._channel_class = channel
|
214
|
+
else
|
215
|
+
raise NonInferrableChannelError.new(channel)
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
def channel_class
|
220
|
+
if channel = self._channel_class
|
221
|
+
channel
|
222
|
+
else
|
223
|
+
tests determine_default_channel(name)
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
def determine_default_channel(name)
|
228
|
+
channel = determine_constant_from_test_name(name) do |constant|
|
229
|
+
Class === constant && constant < ActionCable::Channel::Base
|
230
|
+
end
|
231
|
+
raise NonInferrableChannelError.new(name) if channel.nil?
|
232
|
+
channel
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
# Set up test connection with the specified identifiers:
|
237
|
+
#
|
238
|
+
# class ApplicationCable < ActionCable::Connection::Base
|
239
|
+
# identified_by :user, :token
|
240
|
+
# end
|
241
|
+
#
|
242
|
+
# stub_connection(user: users[:john], token: 'my-secret-token')
|
243
|
+
def stub_connection(identifiers = {})
|
244
|
+
@connection = ConnectionStub.new(identifiers)
|
245
|
+
end
|
246
|
+
|
247
|
+
# Subscribe to the channel under test. Optionally pass subscription parameters
|
248
|
+
# as a Hash.
|
249
|
+
def subscribe(params = {})
|
250
|
+
@connection ||= stub_connection
|
251
|
+
@subscription = self.class.channel_class.new(connection, CHANNEL_IDENTIFIER, params.with_indifferent_access)
|
252
|
+
@subscription.singleton_class.include(ChannelStub)
|
253
|
+
@subscription.subscribe_to_channel
|
254
|
+
@subscription
|
255
|
+
end
|
256
|
+
|
257
|
+
# Unsubscribe the subscription under test.
|
258
|
+
def unsubscribe
|
259
|
+
check_subscribed!
|
260
|
+
subscription.unsubscribe_from_channel
|
261
|
+
end
|
262
|
+
|
263
|
+
# Perform action on a channel.
|
264
|
+
#
|
265
|
+
# NOTE: Must be subscribed.
|
266
|
+
def perform(action, data = {})
|
267
|
+
check_subscribed!
|
268
|
+
subscription.perform_action(data.stringify_keys.merge("action" => action.to_s))
|
269
|
+
end
|
270
|
+
|
271
|
+
# Returns messages transmitted into channel
|
272
|
+
def transmissions
|
273
|
+
# Return only directly sent message (via #transmit)
|
274
|
+
connection.transmissions.filter_map { |data| data["message"] }
|
275
|
+
end
|
276
|
+
|
277
|
+
# Enhance TestHelper assertions to handle non-String broadcastings
|
278
|
+
def assert_broadcasts(stream_or_object, *args)
|
279
|
+
super(broadcasting_for(stream_or_object), *args)
|
280
|
+
end
|
281
|
+
|
282
|
+
def assert_broadcast_on(stream_or_object, *args)
|
283
|
+
super(broadcasting_for(stream_or_object), *args)
|
284
|
+
end
|
285
|
+
|
286
|
+
# Asserts that no streams have been started.
|
287
|
+
#
|
288
|
+
# def test_assert_no_started_stream
|
289
|
+
# subscribe
|
290
|
+
# assert_no_streams
|
291
|
+
# end
|
292
|
+
#
|
293
|
+
def assert_no_streams
|
294
|
+
assert subscription.streams.empty?, "No streams started was expected, but #{subscription.streams.count} found"
|
295
|
+
end
|
296
|
+
|
297
|
+
# Asserts that the specified stream has been started.
|
298
|
+
#
|
299
|
+
# def test_assert_started_stream
|
300
|
+
# subscribe
|
301
|
+
# assert_has_stream 'messages'
|
302
|
+
# end
|
303
|
+
#
|
304
|
+
def assert_has_stream(stream)
|
305
|
+
assert subscription.streams.include?(stream), "Stream #{stream} has not been started"
|
306
|
+
end
|
307
|
+
|
308
|
+
# Asserts that the specified stream for a model has started.
|
309
|
+
#
|
310
|
+
# def test_assert_started_stream_for
|
311
|
+
# subscribe id: 42
|
312
|
+
# assert_has_stream_for User.find(42)
|
313
|
+
# end
|
314
|
+
#
|
315
|
+
def assert_has_stream_for(object)
|
316
|
+
assert_has_stream(broadcasting_for(object))
|
317
|
+
end
|
318
|
+
|
319
|
+
# Asserts that the specified stream has not been started.
|
320
|
+
#
|
321
|
+
# def test_assert_no_started_stream
|
322
|
+
# subscribe
|
323
|
+
# assert_has_no_stream 'messages'
|
324
|
+
# end
|
325
|
+
#
|
326
|
+
def assert_has_no_stream(stream)
|
327
|
+
assert subscription.streams.exclude?(stream), "Stream #{stream} has been started"
|
328
|
+
end
|
329
|
+
|
330
|
+
# Asserts that the specified stream for a model has not started.
|
331
|
+
#
|
332
|
+
# def test_assert_no_started_stream_for
|
333
|
+
# subscribe id: 41
|
334
|
+
# assert_has_no_stream_for User.find(42)
|
335
|
+
# end
|
336
|
+
#
|
337
|
+
def assert_has_no_stream_for(object)
|
338
|
+
assert_has_no_stream(broadcasting_for(object))
|
339
|
+
end
|
340
|
+
|
341
|
+
private
|
342
|
+
def check_subscribed!
|
343
|
+
raise "Must be subscribed!" if subscription.nil? || subscription.rejected?
|
344
|
+
end
|
345
|
+
|
346
|
+
def broadcasting_for(stream_or_object)
|
347
|
+
return stream_or_object if stream_or_object.is_a?(String)
|
348
|
+
|
349
|
+
self.class.channel_class.broadcasting_for(stream_or_object)
|
350
|
+
end
|
351
|
+
end
|
352
|
+
|
353
|
+
include Behavior
|
354
|
+
end
|
355
|
+
end
|
356
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# :markup: markdown
|
4
|
+
|
5
|
+
module ActionCable
|
6
|
+
module Connection
|
7
|
+
module Authorization
|
8
|
+
class UnauthorizedError < StandardError; end
|
9
|
+
|
10
|
+
# Closes the WebSocket connection if it is open and returns an "unauthorized"
|
11
|
+
# reason.
|
12
|
+
def reject_unauthorized_connection
|
13
|
+
logger.error "An unauthorized connection attempt was rejected"
|
14
|
+
raise UnauthorizedError
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|