anyt-core 1.3.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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +38 -0
- data/bin/anyt +17 -0
- data/bin/console +7 -0
- data/bin/setup +8 -0
- data/lib/anyt/cli.rb +211 -0
- data/lib/anyt/client.rb +146 -0
- data/lib/anyt/command.rb +83 -0
- data/lib/anyt/config.rb +41 -0
- data/lib/anyt/dummy/application.rb +86 -0
- data/lib/anyt/dummy/config.ru +16 -0
- data/lib/anyt/dummy/routes.rb +4 -0
- data/lib/anyt/dummy/tmp/development_secret.txt +1 -0
- data/lib/anyt/dummy/tmp/local_secret.txt +1 -0
- data/lib/anyt/ext/minitest.rb +151 -0
- data/lib/anyt/remote_control.rb +33 -0
- data/lib/anyt/rpc.rb +44 -0
- data/lib/anyt/tests/core/ping_test.rb +23 -0
- data/lib/anyt/tests/core/welcome_test.rb +10 -0
- data/lib/anyt/tests/features/channel_state_test.rb +81 -0
- data/lib/anyt/tests/features/remote_disconnect_test.rb +35 -0
- data/lib/anyt/tests/features/server_restart_test.rb +30 -0
- data/lib/anyt/tests/request/channel_test.rb +28 -0
- data/lib/anyt/tests/request/connection_test.rb +54 -0
- data/lib/anyt/tests/request/disconnect_reasons_test.rb +25 -0
- data/lib/anyt/tests/request/disconnection_test.rb +148 -0
- data/lib/anyt/tests/streams/broadcast_test.rb +65 -0
- data/lib/anyt/tests/streams/multiple_clients_test.rb +61 -0
- data/lib/anyt/tests/streams/multiple_test.rb +60 -0
- data/lib/anyt/tests/streams/single_test.rb +83 -0
- data/lib/anyt/tests/streams/stop_test.rb +57 -0
- data/lib/anyt/tests/subscriptions/ack_test.rb +39 -0
- data/lib/anyt/tests/subscriptions/params_test.rb +29 -0
- data/lib/anyt/tests/subscriptions/perform_test.rb +60 -0
- data/lib/anyt/tests/subscriptions/transmissions_test.rb +32 -0
- data/lib/anyt/tests.rb +62 -0
- data/lib/anyt/utils/async_helpers.rb +16 -0
- data/lib/anyt/utils.rb +3 -0
- data/lib/anyt/version.rb +5 -0
- data/lib/anyt.rb +14 -0
- metadata +167 -0
@@ -0,0 +1,151 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
ENV["TERM"] = "#{ENV["TERM"]}color" unless ENV["TERM"]&.match?(/color/)
|
4
|
+
require "minitest/spec"
|
5
|
+
require "minitest/unit"
|
6
|
+
require "minitest/reporters"
|
7
|
+
|
8
|
+
module Anyt
|
9
|
+
# Common tests helpers
|
10
|
+
module TestHelpers
|
11
|
+
def self.included(base)
|
12
|
+
base.let(:client) { build_client(ignore: %w[ping welcome]) }
|
13
|
+
base.after { @clients&.each { |client| client.close(allow_messages: true) } }
|
14
|
+
end
|
15
|
+
|
16
|
+
def build_client(**args)
|
17
|
+
@clients ||= []
|
18
|
+
Anyt::Client.new(**args).tap do |client|
|
19
|
+
@clients << client
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def restart_server!
|
24
|
+
if Anyt.config.use_action_cable
|
25
|
+
remote_client.restart_action_cable
|
26
|
+
else
|
27
|
+
Command.restart
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def remote_client
|
32
|
+
@remote_client ||= RemoteControl::Client.connect(Anyt.config.remote_control_port)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Verifies that the actual message Hash is a subset of the expected one
|
36
|
+
# (so we can ignore some irrelevant fields)
|
37
|
+
def assert_message(expected, actual)
|
38
|
+
assert_equal expected, actual.slice(*expected.keys)
|
39
|
+
end
|
40
|
+
|
41
|
+
def assert_includes_message(collection, expected)
|
42
|
+
found = collection.find do |el|
|
43
|
+
el.slice(*expected.keys) == expected
|
44
|
+
end
|
45
|
+
|
46
|
+
assert found, "Expecte #{collection} to include a message matching #{expected}"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
module Anyt
|
52
|
+
# Namespace for test channels
|
53
|
+
module TestChannels; end
|
54
|
+
|
55
|
+
# Custom #connect handlers management
|
56
|
+
module ConnectHandlers
|
57
|
+
class << self
|
58
|
+
def call(connection)
|
59
|
+
handlers_for(connection).each do |(_, handler)|
|
60
|
+
connection.reject_unauthorized_connection unless
|
61
|
+
connection.instance_eval(&handler)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def add(tag, &block)
|
66
|
+
handlers << [tag, block]
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def handlers_for(connection)
|
72
|
+
handlers.select do |(tag, _)|
|
73
|
+
connection.params["test"] == tag
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def handlers
|
78
|
+
@handlers ||= []
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# Kernel extensions
|
85
|
+
module Kernel
|
86
|
+
## Wraps `describe` and include shared helpers
|
87
|
+
private def feature(*args, &block)
|
88
|
+
cls = describe(*args, &block)
|
89
|
+
cls.include Anyt::TestHelpers
|
90
|
+
cls
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
# Extend Minitest Spec DSL with custom methodss
|
95
|
+
module Minitest::Spec::DSL
|
96
|
+
# Simplified version of `it` which doesn't care
|
97
|
+
# about unique method names
|
98
|
+
def scenario(desc, &block)
|
99
|
+
block ||= proc { skip "(no tests defined)" }
|
100
|
+
|
101
|
+
define_method "test_ #{desc}", &block
|
102
|
+
|
103
|
+
desc
|
104
|
+
end
|
105
|
+
|
106
|
+
# Generates Channel class dynamically and
|
107
|
+
# add memoized helper to access its name
|
108
|
+
def channel(id = nil, &block)
|
109
|
+
class_name = @name.gsub(/\s+/, "_")
|
110
|
+
class_name += "_#{id}" if id
|
111
|
+
class_name += "_channel"
|
112
|
+
|
113
|
+
cls = Class.new(ApplicationCable::Channel, &block)
|
114
|
+
|
115
|
+
Anyt::TestChannels.const_set(class_name.classify, cls)
|
116
|
+
|
117
|
+
helper_name = id ? "#{id}_channel" : "channel"
|
118
|
+
|
119
|
+
let(helper_name) { cls.name }
|
120
|
+
end
|
121
|
+
|
122
|
+
# Add new #connect handler
|
123
|
+
def connect_handler(tag, &block)
|
124
|
+
Anyt::ConnectHandlers.add(tag, &block)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
module Anyt
|
129
|
+
# Patch Minitest load_plugins to disable Rails plugin
|
130
|
+
# See: https://github.com/kern/minitest-reporters/issues/230
|
131
|
+
module MinitestPatch
|
132
|
+
def load_plugins
|
133
|
+
super
|
134
|
+
extensions.delete("rails")
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
# Patch Spec reporter
|
139
|
+
module ReporterPatch # :nodoc:
|
140
|
+
def record_print_status(test)
|
141
|
+
test_name = test.name.gsub(/^test_/, "").strip
|
142
|
+
print(magenta { pad_test(test_name) })
|
143
|
+
print_colored_status(test)
|
144
|
+
print(" (%.2fs)" % test.time) unless test.time.nil?
|
145
|
+
puts
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
Minitest::Reporters::SpecReporter.prepend Anyt::ReporterPatch
|
151
|
+
Minitest.singleton_class.prepend Anyt::MinitestPatch
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "drb/drb"
|
4
|
+
|
5
|
+
module Anyt
|
6
|
+
# Invoke commands within the running Ruby (Action Cable) server
|
7
|
+
module RemoteControl
|
8
|
+
class Server
|
9
|
+
class << self
|
10
|
+
alias_method :start, :new
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(port)
|
14
|
+
DRb.start_service(
|
15
|
+
"druby://localhost:#{port}",
|
16
|
+
Client.new
|
17
|
+
)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class Client
|
22
|
+
def self.connect(port)
|
23
|
+
DRb.start_service
|
24
|
+
|
25
|
+
DRbObject.new_with_uri("druby://localhost:#{port}")
|
26
|
+
end
|
27
|
+
|
28
|
+
def restart_action_cable
|
29
|
+
ActionCable.server.restart
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
data/lib/anyt/rpc.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "anyt/dummy/application"
|
4
|
+
require "anycable-rails"
|
5
|
+
require "redis"
|
6
|
+
|
7
|
+
module Anyt # :nodoc:
|
8
|
+
# Runs AnyCable RPC server in the background
|
9
|
+
class RPC
|
10
|
+
using AsyncHelpers
|
11
|
+
|
12
|
+
attr_accessor :running
|
13
|
+
attr_reader :server
|
14
|
+
|
15
|
+
def start
|
16
|
+
AnyCable.logger.debug "Starting RPC server ..."
|
17
|
+
|
18
|
+
AnyCable.server_callbacks.each(&:call)
|
19
|
+
|
20
|
+
@server = AnyCable::GRPC::Server.new(
|
21
|
+
host: AnyCable.config.rpc_host,
|
22
|
+
**AnyCable.config.to_grpc_params
|
23
|
+
)
|
24
|
+
|
25
|
+
if defined?(::AnyCable::Middlewares::EnvSid)
|
26
|
+
AnyCable.middleware.use(::AnyCable::Middlewares::EnvSid)
|
27
|
+
end
|
28
|
+
|
29
|
+
AnyCable.middleware.freeze
|
30
|
+
|
31
|
+
server.start
|
32
|
+
|
33
|
+
AnyCable.logger.debug "RPC server started"
|
34
|
+
end
|
35
|
+
# rubocop: enable Metrics/AbcSize,Metrics/MethodLength
|
36
|
+
|
37
|
+
def stop
|
38
|
+
server&.stop
|
39
|
+
end
|
40
|
+
|
41
|
+
AnyCable.connection_factory = ActionCable.server.config.connection_class.call
|
42
|
+
AnyCable.config.log_level = :fatal unless AnyCable.config.debug
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
feature "Ping" do
|
4
|
+
scenario %(
|
5
|
+
Client receives pings with timestamps
|
6
|
+
) do
|
7
|
+
client = build_client(ignore: ["welcome"])
|
8
|
+
|
9
|
+
previous_stamp = 0
|
10
|
+
|
11
|
+
2.times do
|
12
|
+
ping = client.receive
|
13
|
+
|
14
|
+
current_stamp = ping["message"]
|
15
|
+
|
16
|
+
assert_equal "ping", ping["type"]
|
17
|
+
assert_kind_of Integer, current_stamp
|
18
|
+
refute_operator previous_stamp, :>=, current_stamp
|
19
|
+
|
20
|
+
previous_stamp = current_stamp
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
feature "Channel state" do
|
4
|
+
channel do
|
5
|
+
state_attr_accessor :user, :count
|
6
|
+
|
7
|
+
def subscribed
|
8
|
+
self.user = {name: params["name"]}
|
9
|
+
self.count = 1
|
10
|
+
|
11
|
+
stream_from "state_counts"
|
12
|
+
end
|
13
|
+
|
14
|
+
def tick
|
15
|
+
self.count += 2
|
16
|
+
transmit({count: count, name: user[:name]})
|
17
|
+
end
|
18
|
+
|
19
|
+
def unsubscribed
|
20
|
+
return unless params["notify_disconnect"]
|
21
|
+
|
22
|
+
ActionCable.server.broadcast("state_counts", {data: "user left: #{user[:name]}"})
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
let(:identifier) { {channel: channel, name: "chipolino"}.to_json }
|
27
|
+
|
28
|
+
let(:client2) { build_client(ignore: %w[ping welcome]) }
|
29
|
+
let(:identifier2) { {channel: channel, name: "chipollone", notify_disconnect: true}.to_json }
|
30
|
+
|
31
|
+
before do
|
32
|
+
subscribe_request = {command: "subscribe", identifier: identifier}
|
33
|
+
|
34
|
+
client.send(subscribe_request)
|
35
|
+
|
36
|
+
ack = {
|
37
|
+
"identifier" => identifier, "type" => "confirm_subscription"
|
38
|
+
}
|
39
|
+
|
40
|
+
assert_message ack, client.receive
|
41
|
+
end
|
42
|
+
|
43
|
+
scenario %(
|
44
|
+
Channel state is kept between commands
|
45
|
+
) do
|
46
|
+
perform_request = {
|
47
|
+
:command => "message",
|
48
|
+
:identifier => identifier,
|
49
|
+
"data" => {"action" => "tick"}.to_json
|
50
|
+
}
|
51
|
+
|
52
|
+
client.send(perform_request)
|
53
|
+
|
54
|
+
msg = {"identifier" => identifier, "message" => {"count" => 3, "name" => "chipolino"}}
|
55
|
+
|
56
|
+
assert_message msg, client.receive
|
57
|
+
end
|
58
|
+
|
59
|
+
scenario %(
|
60
|
+
Channel state is available in #unsubscribe callbacks
|
61
|
+
) do
|
62
|
+
subscribe_request = {command: "subscribe", identifier: identifier2}
|
63
|
+
|
64
|
+
client2.send(subscribe_request)
|
65
|
+
|
66
|
+
ack = {
|
67
|
+
"identifier" => identifier2, "type" => "confirm_subscription"
|
68
|
+
}
|
69
|
+
|
70
|
+
assert_message ack, client2.receive
|
71
|
+
|
72
|
+
client2.close
|
73
|
+
|
74
|
+
msg = {
|
75
|
+
"identifier" => identifier,
|
76
|
+
"message" => {"data" => "user left: chipollone"}
|
77
|
+
}
|
78
|
+
|
79
|
+
assert_message msg, client.receive
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
feature "Remote disconnect" do
|
4
|
+
connect_handler("uid") do
|
5
|
+
self.uid = request.params["uid"]
|
6
|
+
uid.present?
|
7
|
+
end
|
8
|
+
|
9
|
+
scenario %(
|
10
|
+
Close single connection by id
|
11
|
+
) do
|
12
|
+
client = build_client(qs: "test=uid&uid=26", ignore: %w[ping])
|
13
|
+
assert_message({"type" => "welcome"}, client.receive)
|
14
|
+
|
15
|
+
# Prevent race conditions when we send disconnect before internal channel subscription has been made
|
16
|
+
# (only for Action Cable)
|
17
|
+
sleep 1
|
18
|
+
ActionCable.server.remote_connections.where(uid: "26").disconnect
|
19
|
+
|
20
|
+
# Waiting for https://github.com/rails/rails/pull/39544
|
21
|
+
unless Anyt.config.use_action_cable
|
22
|
+
assert_message(
|
23
|
+
{
|
24
|
+
"type" => "disconnect",
|
25
|
+
"reconnect" => true,
|
26
|
+
"reason" => "remote"
|
27
|
+
},
|
28
|
+
client.receive
|
29
|
+
)
|
30
|
+
end
|
31
|
+
|
32
|
+
client.wait_for_close
|
33
|
+
assert client.closed?
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
feature "Server restart" do
|
4
|
+
connect_handler("reasons") do
|
5
|
+
next false if request.params[:reason] == "unauthorized"
|
6
|
+
true
|
7
|
+
end
|
8
|
+
|
9
|
+
scenario %(
|
10
|
+
Client receives disconnect message
|
11
|
+
) do
|
12
|
+
client = build_client(
|
13
|
+
qs: "test=reasons&reason=server_restart",
|
14
|
+
ignore: %(ping)
|
15
|
+
)
|
16
|
+
|
17
|
+
assert_message({"type" => "welcome"}, client.receive)
|
18
|
+
|
19
|
+
restart_server!
|
20
|
+
|
21
|
+
assert_message(
|
22
|
+
{
|
23
|
+
"type" => "disconnect",
|
24
|
+
"reconnect" => true,
|
25
|
+
"reason" => "server_restart"
|
26
|
+
},
|
27
|
+
client.receive
|
28
|
+
)
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
feature "Request" do
|
4
|
+
channel do
|
5
|
+
delegate :params, to: :connection
|
6
|
+
|
7
|
+
def subscribed
|
8
|
+
reject unless params[:token] == "secret"
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
scenario %(
|
13
|
+
Channel has access to request
|
14
|
+
) do
|
15
|
+
client = build_client(qs: "token=secret", ignore: %w[ping])
|
16
|
+
assert_message({"type" => "welcome"}, client.receive)
|
17
|
+
|
18
|
+
subscribe_request = {command: "subscribe", identifier: {channel: channel}.to_json}
|
19
|
+
|
20
|
+
client.send(subscribe_request)
|
21
|
+
|
22
|
+
ack = {
|
23
|
+
"identifier" => {channel: channel}.to_json, "type" => "confirm_subscription"
|
24
|
+
}
|
25
|
+
|
26
|
+
assert_message ack, client.receive
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
feature "Request" do
|
4
|
+
target_host = URI.parse(Anyt.config.target_url).host
|
5
|
+
|
6
|
+
connect_handler("request_url") do
|
7
|
+
request.url =~ /test=request_url/ && request.host == target_host
|
8
|
+
end
|
9
|
+
|
10
|
+
scenario %(
|
11
|
+
Url is set during connection
|
12
|
+
) do
|
13
|
+
client = build_client(qs: "test=request_url")
|
14
|
+
assert_message({"type" => "welcome"}, client.receive)
|
15
|
+
end
|
16
|
+
|
17
|
+
connect_handler("cookies") do
|
18
|
+
cookies[:username] == "john green"
|
19
|
+
end
|
20
|
+
|
21
|
+
scenario %(
|
22
|
+
Reject when required cookies are not set
|
23
|
+
) do
|
24
|
+
client = build_client(qs: "test=cookies")
|
25
|
+
client.wait_for_close
|
26
|
+
assert client.closed?
|
27
|
+
end
|
28
|
+
|
29
|
+
scenario %(
|
30
|
+
Accepts when required cookies are set
|
31
|
+
) do
|
32
|
+
client = build_client(qs: "test=cookies", cookies: "username=john green")
|
33
|
+
assert_message({"type" => "welcome"}, client.receive)
|
34
|
+
end
|
35
|
+
|
36
|
+
connect_handler("headers") do
|
37
|
+
request.headers["X-API-TOKEN"] == "abc"
|
38
|
+
end
|
39
|
+
|
40
|
+
scenario %(
|
41
|
+
Reject when required header is missing
|
42
|
+
) do
|
43
|
+
client = build_client(qs: "test=headers")
|
44
|
+
client.wait_for_close
|
45
|
+
assert client.closed?
|
46
|
+
end
|
47
|
+
|
48
|
+
scenario %(
|
49
|
+
Accepts when required header is set
|
50
|
+
) do
|
51
|
+
client = build_client(qs: "test=headers", headers: {"x-api-token" => "abc"})
|
52
|
+
assert_message({"type" => "welcome"}, client.receive)
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
feature "Request" do
|
4
|
+
connect_handler("reasons") do
|
5
|
+
next false if request.params[:reason] == "unauthorized"
|
6
|
+
true
|
7
|
+
end
|
8
|
+
|
9
|
+
scenario %(
|
10
|
+
Receives disconnect message when rejected
|
11
|
+
) do
|
12
|
+
client = build_client(qs: "test=reasons&reason=unauthorized")
|
13
|
+
assert_message(
|
14
|
+
{
|
15
|
+
"type" => "disconnect",
|
16
|
+
"reconnect" => false,
|
17
|
+
"reason" => "unauthorized"
|
18
|
+
},
|
19
|
+
client.receive
|
20
|
+
)
|
21
|
+
|
22
|
+
client.wait_for_close
|
23
|
+
assert client.closed?
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,148 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
feature "Request" do
|
4
|
+
channel(:a) do
|
5
|
+
def subscribed
|
6
|
+
stream_from "request_a"
|
7
|
+
end
|
8
|
+
|
9
|
+
def unsubscribed
|
10
|
+
ActionCable.server.broadcast("request_a", {data: "user left"})
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
channel(:b) do
|
15
|
+
def subscribed
|
16
|
+
stream_from "request_b"
|
17
|
+
end
|
18
|
+
|
19
|
+
def unsubscribed
|
20
|
+
ActionCable.server.broadcast("request_b", {data: "user left"})
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
channel(:c) do
|
25
|
+
def subscribed
|
26
|
+
stream_from "request_c"
|
27
|
+
end
|
28
|
+
|
29
|
+
def unsubscribed
|
30
|
+
ActionCable.server.broadcast("request_c", {data: "user left#{params[:id].presence}"})
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
let(:client2) { build_client(ignore: %w[ping welcome]) }
|
35
|
+
|
36
|
+
scenario %(
|
37
|
+
Client disconnect invokes #unsubscribe callbacks
|
38
|
+
for different channels
|
39
|
+
) do
|
40
|
+
subscribe_request = {command: "subscribe", identifier: {channel: a_channel}.to_json}
|
41
|
+
|
42
|
+
client.send(subscribe_request)
|
43
|
+
|
44
|
+
ack = {
|
45
|
+
"identifier" => {channel: a_channel}.to_json, "type" => "confirm_subscription"
|
46
|
+
}
|
47
|
+
|
48
|
+
assert_message ack, client.receive
|
49
|
+
|
50
|
+
subscribe_request = {command: "subscribe", identifier: {channel: b_channel}.to_json}
|
51
|
+
|
52
|
+
client.send(subscribe_request)
|
53
|
+
|
54
|
+
ack = {
|
55
|
+
"identifier" => {channel: b_channel}.to_json, "type" => "confirm_subscription"
|
56
|
+
}
|
57
|
+
|
58
|
+
assert_message ack, client.receive
|
59
|
+
|
60
|
+
subscribe_request = {command: "subscribe", identifier: {channel: a_channel}.to_json}
|
61
|
+
|
62
|
+
client2.send(subscribe_request)
|
63
|
+
|
64
|
+
ack = {
|
65
|
+
"identifier" => {channel: a_channel}.to_json, "type" => "confirm_subscription"
|
66
|
+
}
|
67
|
+
|
68
|
+
assert_message ack, client2.receive
|
69
|
+
|
70
|
+
subscribe_request = {command: "subscribe", identifier: {channel: b_channel}.to_json}
|
71
|
+
|
72
|
+
client2.send(subscribe_request)
|
73
|
+
|
74
|
+
ack = {
|
75
|
+
"identifier" => {channel: b_channel}.to_json, "type" => "confirm_subscription"
|
76
|
+
}
|
77
|
+
|
78
|
+
assert_message ack, client2.receive
|
79
|
+
|
80
|
+
client2.close
|
81
|
+
|
82
|
+
msg = {
|
83
|
+
"identifier" => {channel: a_channel}.to_json,
|
84
|
+
"message" => {"data" => "user left"}
|
85
|
+
}
|
86
|
+
msg2 = {
|
87
|
+
"identifier" => {channel: b_channel}.to_json,
|
88
|
+
"message" => {"data" => "user left"}
|
89
|
+
}
|
90
|
+
|
91
|
+
msgs = [client.receive, client.receive]
|
92
|
+
|
93
|
+
assert_includes_message msgs, msg
|
94
|
+
assert_includes_message msgs, msg2
|
95
|
+
end
|
96
|
+
|
97
|
+
scenario %(
|
98
|
+
Client disconnect invokes #unsubscribe callbacks
|
99
|
+
for multiple subscriptions from the same channel
|
100
|
+
) do
|
101
|
+
subscribe_request = {command: "subscribe", identifier: {channel: c_channel}.to_json}
|
102
|
+
|
103
|
+
client.send(subscribe_request)
|
104
|
+
|
105
|
+
ack = {
|
106
|
+
"identifier" => {channel: c_channel}.to_json, "type" => "confirm_subscription"
|
107
|
+
}
|
108
|
+
|
109
|
+
assert_message ack, client.receive
|
110
|
+
|
111
|
+
subscribe_request = {command: "subscribe", identifier: {channel: c_channel, id: 1}.to_json}
|
112
|
+
|
113
|
+
client2.send(subscribe_request)
|
114
|
+
|
115
|
+
ack = {
|
116
|
+
"identifier" => {channel: c_channel, id: 1}.to_json, "type" => "confirm_subscription"
|
117
|
+
}
|
118
|
+
|
119
|
+
assert_message ack, client2.receive
|
120
|
+
|
121
|
+
subscribe_request = {command: "subscribe", identifier: {channel: c_channel, id: 2}.to_json}
|
122
|
+
|
123
|
+
client2.send(subscribe_request)
|
124
|
+
|
125
|
+
ack = {
|
126
|
+
"identifier" => {channel: c_channel, id: 2}.to_json, "type" => "confirm_subscription"
|
127
|
+
}
|
128
|
+
|
129
|
+
assert_message ack, client2.receive
|
130
|
+
|
131
|
+
client2.close
|
132
|
+
|
133
|
+
msg = {
|
134
|
+
"identifier" => {channel: c_channel}.to_json,
|
135
|
+
"message" => {"data" => "user left1"}
|
136
|
+
}
|
137
|
+
|
138
|
+
msg2 = {
|
139
|
+
"identifier" => {channel: c_channel}.to_json,
|
140
|
+
"message" => {"data" => "user left2"}
|
141
|
+
}
|
142
|
+
|
143
|
+
msgs = [client.receive, client.receive]
|
144
|
+
|
145
|
+
assert_includes_message msgs, msg
|
146
|
+
assert_includes_message msgs, msg2
|
147
|
+
end
|
148
|
+
end
|