anyt-core 1.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|