ably 0.1.6 → 0.2.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 +4 -4
- data/.gitignore +2 -0
- data/.travis.yml +9 -0
- data/LICENSE.txt +1 -1
- data/README.md +8 -1
- data/Rakefile +10 -0
- data/ably.gemspec +18 -18
- data/lib/ably.rb +6 -5
- data/lib/ably/auth.rb +11 -14
- data/lib/ably/exceptions.rb +18 -15
- data/lib/ably/logger.rb +102 -0
- data/lib/ably/models/error_info.rb +1 -1
- data/lib/ably/models/message.rb +19 -5
- data/lib/ably/models/message_encoders/base.rb +107 -0
- data/lib/ably/models/message_encoders/base64.rb +39 -0
- data/lib/ably/models/message_encoders/cipher.rb +80 -0
- data/lib/ably/models/message_encoders/json.rb +33 -0
- data/lib/ably/models/message_encoders/utf8.rb +33 -0
- data/lib/ably/models/paginated_resource.rb +23 -6
- data/lib/ably/models/presence_message.rb +19 -7
- data/lib/ably/models/protocol_message.rb +5 -4
- data/lib/ably/models/token.rb +2 -2
- data/lib/ably/modules/channels_collection.rb +0 -3
- data/lib/ably/modules/conversions.rb +3 -3
- data/lib/ably/modules/encodeable.rb +68 -0
- data/lib/ably/modules/event_emitter.rb +10 -4
- data/lib/ably/modules/event_machine_helpers.rb +6 -4
- data/lib/ably/modules/http_helpers.rb +7 -2
- data/lib/ably/modules/model_common.rb +2 -0
- data/lib/ably/modules/state_emitter.rb +10 -1
- data/lib/ably/realtime.rb +19 -12
- data/lib/ably/realtime/channel.rb +26 -13
- data/lib/ably/realtime/client.rb +31 -7
- data/lib/ably/realtime/client/incoming_message_dispatcher.rb +14 -3
- data/lib/ably/realtime/client/outgoing_message_dispatcher.rb +13 -4
- data/lib/ably/realtime/connection.rb +152 -46
- data/lib/ably/realtime/connection/connection_manager.rb +168 -0
- data/lib/ably/realtime/connection/connection_state_machine.rb +56 -33
- data/lib/ably/realtime/connection/websocket_transport.rb +56 -29
- data/lib/ably/{models → realtime/models}/nil_channel.rb +1 -1
- data/lib/ably/realtime/presence.rb +38 -13
- data/lib/ably/rest.rb +7 -5
- data/lib/ably/rest/channel.rb +24 -3
- data/lib/ably/rest/client.rb +56 -17
- data/lib/ably/rest/middleware/encoder.rb +49 -0
- data/lib/ably/rest/middleware/exceptions.rb +3 -2
- data/lib/ably/rest/middleware/logger.rb +37 -0
- data/lib/ably/rest/presence.rb +10 -2
- data/lib/ably/util/crypto.rb +57 -29
- data/lib/ably/util/pub_sub.rb +11 -0
- data/lib/ably/version.rb +1 -1
- data/spec/acceptance/realtime/channel_spec.rb +65 -7
- data/spec/acceptance/realtime/connection_spec.rb +123 -27
- data/spec/acceptance/realtime/message_spec.rb +319 -34
- data/spec/acceptance/realtime/presence_history_spec.rb +58 -0
- data/spec/acceptance/realtime/presence_spec.rb +160 -18
- data/spec/acceptance/rest/auth_spec.rb +93 -49
- data/spec/acceptance/rest/base_spec.rb +10 -10
- data/spec/acceptance/rest/channel_spec.rb +35 -19
- data/spec/acceptance/rest/channels_spec.rb +8 -8
- data/spec/acceptance/rest/message_spec.rb +224 -0
- data/spec/acceptance/rest/presence_spec.rb +159 -23
- data/spec/acceptance/rest/stats_spec.rb +5 -5
- data/spec/acceptance/rest/time_spec.rb +4 -4
- data/spec/integration/rest/auth.rb +1 -1
- data/spec/resources/crypto-data-128.json +56 -0
- data/spec/resources/crypto-data-256.json +56 -0
- data/spec/rspec_config.rb +39 -0
- data/spec/spec_helper.rb +4 -42
- data/spec/support/api_helper.rb +1 -1
- data/spec/support/event_machine_helper.rb +0 -5
- data/spec/support/protocol_msgbus_helper.rb +3 -3
- data/spec/support/test_app.rb +3 -3
- data/spec/unit/logger_spec.rb +135 -0
- data/spec/unit/models/message_encoders/base64_spec.rb +181 -0
- data/spec/unit/models/message_encoders/cipher_spec.rb +260 -0
- data/spec/unit/models/message_encoders/json_spec.rb +135 -0
- data/spec/unit/models/message_encoders/utf8_spec.rb +100 -0
- data/spec/unit/models/message_spec.rb +16 -1
- data/spec/unit/models/paginated_resource_spec.rb +46 -0
- data/spec/unit/models/presence_message_spec.rb +18 -5
- data/spec/unit/models/token_spec.rb +1 -1
- data/spec/unit/modules/event_emitter_spec.rb +24 -10
- data/spec/unit/realtime/channel_spec.rb +3 -3
- data/spec/unit/realtime/channels_spec.rb +1 -1
- data/spec/unit/realtime/client_spec.rb +44 -2
- data/spec/unit/realtime/connection_spec.rb +2 -2
- data/spec/unit/realtime/incoming_message_dispatcher_spec.rb +4 -4
- data/spec/unit/realtime/presence_spec.rb +1 -1
- data/spec/unit/realtime/realtime_spec.rb +3 -3
- data/spec/unit/realtime/websocket_transport_spec.rb +24 -0
- data/spec/unit/rest/channels_spec.rb +1 -1
- data/spec/unit/rest/client_spec.rb +45 -10
- data/spec/unit/util/crypto_spec.rb +82 -0
- data/spec/unit/{modules → util}/pub_sub_spec.rb +13 -1
- metadata +43 -12
- data/spec/acceptance/crypto.rb +0 -63
data/lib/ably/util/pub_sub.rb
CHANGED
@@ -19,6 +19,17 @@ module Ably::Util
|
|
19
19
|
class PubSub
|
20
20
|
include Ably::Modules::EventEmitter
|
21
21
|
|
22
|
+
# Ensure new PubSub object does not share class instance variables
|
23
|
+
def self.new(options = {})
|
24
|
+
Class.new(PubSub).allocate.tap do |pub_sub_object|
|
25
|
+
pub_sub_object.send(:initialize, options)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def inspect
|
30
|
+
"<#PubSub: @event_emitter_coerce_proc: #{self.class.event_emitter_coerce_proc.inspect}\n @callbacks: #{callbacks}>"
|
31
|
+
end
|
32
|
+
|
22
33
|
def initialize(options = {})
|
23
34
|
self.class.instance_eval do
|
24
35
|
configure_event_emitter options
|
data/lib/ably/version.rb
CHANGED
@@ -8,13 +8,11 @@ describe Ably::Realtime::Channel do
|
|
8
8
|
context "over #{protocol}" do
|
9
9
|
let(:default_options) { { api_key: api_key, environment: environment, protocol: protocol } }
|
10
10
|
|
11
|
-
let(:client)
|
12
|
-
Ably::Realtime::Client.new(default_options)
|
13
|
-
end
|
11
|
+
let(:client) { Ably::Realtime::Client.new(default_options) }
|
14
12
|
let(:channel_name) { SecureRandom.hex(2) }
|
15
|
-
let(:payload)
|
16
|
-
let(:channel)
|
17
|
-
let(:messages)
|
13
|
+
let(:payload) { SecureRandom.hex(4) }
|
14
|
+
let(:channel) { client.channel(channel_name) }
|
15
|
+
let(:messages) { [] }
|
18
16
|
|
19
17
|
it 'attaches to a channel' do
|
20
18
|
run_reactor do
|
@@ -135,6 +133,66 @@ describe Ably::Realtime::Channel do
|
|
135
133
|
end
|
136
134
|
end
|
137
135
|
|
136
|
+
it 'opens many connections and then many channels simultaneously' do
|
137
|
+
run_reactor(15) do
|
138
|
+
count, connected_ids = 25, []
|
139
|
+
|
140
|
+
clients = count.times.map do
|
141
|
+
Ably::Realtime::Client.new(default_options)
|
142
|
+
end
|
143
|
+
|
144
|
+
channels_opened = 0
|
145
|
+
open_channels_on_clients = Proc.new do
|
146
|
+
5.times.each do |channel|
|
147
|
+
clients.each do |client|
|
148
|
+
client.channel("channel-#{channel}").attach do
|
149
|
+
channels_opened += 1
|
150
|
+
if channels_opened == clients.count * 5
|
151
|
+
expect(channels_opened).to eql(clients.count * 5)
|
152
|
+
stop_reactor
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
clients.each do |client|
|
160
|
+
client.connection.on(:connected) do
|
161
|
+
connected_ids << client.connection.id
|
162
|
+
|
163
|
+
if connected_ids.count == 25
|
164
|
+
expect(connected_ids.uniq.count).to eql(25)
|
165
|
+
open_channels_on_clients.call
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
it 'opens many connections and attaches to channels before connected' do
|
173
|
+
run_reactor(15) do
|
174
|
+
count, connected_ids = 25, []
|
175
|
+
|
176
|
+
clients = count.times.map do
|
177
|
+
Ably::Realtime::Client.new(default_options)
|
178
|
+
end
|
179
|
+
|
180
|
+
channels_opened = 0
|
181
|
+
|
182
|
+
clients.each do |client|
|
183
|
+
5.times.each do |channel|
|
184
|
+
client.channel("channel-#{channel}").attach do
|
185
|
+
channels_opened += 1
|
186
|
+
if channels_opened == clients.count * 5
|
187
|
+
expect(channels_opened).to eql(clients.count * 5)
|
188
|
+
stop_reactor
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
138
196
|
context 'attach failure' do
|
139
197
|
let(:restricted_client) do
|
140
198
|
Ably::Realtime::Client.new(default_options.merge(api_key: restricted_api_key))
|
@@ -146,7 +204,7 @@ describe Ably::Realtime::Channel do
|
|
146
204
|
restricted_channel.attach
|
147
205
|
restricted_channel.on(:failed) do |error|
|
148
206
|
expect(restricted_channel.state).to eq(:failed)
|
149
|
-
expect(error.
|
207
|
+
expect(error.status).to eq(401)
|
150
208
|
stop_reactor
|
151
209
|
end
|
152
210
|
end
|
@@ -3,19 +3,43 @@ require 'spec_helper'
|
|
3
3
|
describe Ably::Realtime::Connection do
|
4
4
|
include RSpec::EventMachine
|
5
5
|
|
6
|
-
|
6
|
+
let(:connection) { client.connection }
|
7
|
+
|
8
|
+
[:json, :msgpack].each do |protocol|
|
7
9
|
context "over #{protocol}" do
|
10
|
+
let(:default_options) do
|
11
|
+
{ api_key: api_key, environment: environment, protocol: protocol }
|
12
|
+
end
|
13
|
+
|
8
14
|
let(:client) do
|
9
|
-
Ably::Realtime::Client.new(
|
15
|
+
Ably::Realtime::Client.new(default_options)
|
10
16
|
end
|
11
17
|
|
12
|
-
|
18
|
+
context 'with API key' do
|
19
|
+
it 'connects automatically' do
|
20
|
+
run_reactor do
|
21
|
+
connection.on(:connected) do
|
22
|
+
expect(connection.state).to eq(:connected)
|
23
|
+
expect(client.auth.auth_params[:key_id]).to_not be_nil
|
24
|
+
expect(client.auth.auth_params[:access_token]).to be_nil
|
25
|
+
stop_reactor
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
13
30
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
31
|
+
context 'with client_id resulting in token auth' do
|
32
|
+
let(:default_options) do
|
33
|
+
{ api_key: api_key, environment: environment, protocol: protocol, client_id: SecureRandom.hex, log_level: :debug }
|
34
|
+
end
|
35
|
+
it 'connects automatically' do
|
36
|
+
run_reactor do
|
37
|
+
connection.on(:connected) do
|
38
|
+
expect(connection.state).to eq(:connected)
|
39
|
+
expect(client.auth.auth_params[:access_token]).to_not be_nil
|
40
|
+
expect(client.auth.auth_params[:key_id]).to be_nil
|
41
|
+
stop_reactor
|
42
|
+
end
|
19
43
|
end
|
20
44
|
end
|
21
45
|
end
|
@@ -32,7 +56,7 @@ describe Ably::Realtime::Connection do
|
|
32
56
|
|
33
57
|
run_reactor do
|
34
58
|
phases.each do |phase|
|
35
|
-
|
59
|
+
connection.on(phase) do
|
36
60
|
events_triggered << phase
|
37
61
|
test_expectation.call if events_triggered.length == phases.length
|
38
62
|
end
|
@@ -41,24 +65,38 @@ describe Ably::Realtime::Connection do
|
|
41
65
|
end
|
42
66
|
end
|
43
67
|
|
44
|
-
skip '#
|
68
|
+
skip '#close disconnects, closes the connection immediately and changes the connection state to closed'
|
45
69
|
|
46
|
-
specify '#
|
70
|
+
specify '#close(graceful: true) gracefully waits for the server to close the connection' do
|
47
71
|
run_reactor(8) do
|
48
|
-
|
49
|
-
|
50
|
-
expect(
|
72
|
+
connection.close
|
73
|
+
connection.on(:closed) do
|
74
|
+
expect(connection.state).to eq(:closed)
|
51
75
|
stop_reactor
|
52
76
|
end
|
53
77
|
end
|
54
78
|
end
|
55
79
|
|
56
|
-
it '
|
57
|
-
run_reactor
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
80
|
+
it 'echoes a heart beat with #ping' do
|
81
|
+
run_reactor do
|
82
|
+
connection.on(:connected) do
|
83
|
+
connection.ping do |time_elapsed|
|
84
|
+
expect(time_elapsed).to be > 0
|
85
|
+
stop_reactor
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
skip 'connects, closes gracefully and reconnects on #connect'
|
92
|
+
|
93
|
+
it 'connects, closes the connection, and then reconnects with a new connection ID' do
|
94
|
+
run_reactor(15) do
|
95
|
+
connection.connect do
|
96
|
+
connection_id = connection.id
|
97
|
+
connection.close do
|
98
|
+
connection.connect do
|
99
|
+
expect(connection.id).to_not eql(connection_id)
|
62
100
|
stop_reactor
|
63
101
|
end
|
64
102
|
end
|
@@ -66,15 +104,73 @@ describe Ably::Realtime::Connection do
|
|
66
104
|
end
|
67
105
|
end
|
68
106
|
|
69
|
-
|
107
|
+
context 'failures' do
|
108
|
+
context 'with invalid app part of the key' do
|
109
|
+
let(:missing_key) { 'not_an_app.invalid_key_id:invalid_key_value' }
|
110
|
+
let(:client) do
|
111
|
+
Ably::Realtime::Client.new(default_options.merge(api_key: missing_key))
|
112
|
+
end
|
70
113
|
|
71
|
-
|
114
|
+
it 'enters the failed state and returns a not found error' do
|
115
|
+
run_reactor do
|
116
|
+
connection.on(:failed) do |error|
|
117
|
+
expect(connection.state).to eq(:failed)
|
118
|
+
expect(error.status).to eq(404)
|
119
|
+
stop_reactor
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
context 'with invalid key ID part of the key' do
|
126
|
+
let(:invalid_key) { "#{app_id}.invalid_key_id:invalid_key_value" }
|
127
|
+
let(:client) do
|
128
|
+
Ably::Realtime::Client.new(default_options.merge(api_key: invalid_key))
|
129
|
+
end
|
130
|
+
|
131
|
+
it 'enters the failed state and returns an authorization error' do
|
132
|
+
run_reactor do
|
133
|
+
connection.on(:failed) do |error|
|
134
|
+
expect(connection.state).to eq(:failed)
|
135
|
+
expect(error.status).to eq(401)
|
136
|
+
stop_reactor
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
context 'with invalid WebSocket host' do
|
143
|
+
let(:client) do
|
144
|
+
Ably::Realtime::Client.new(default_options.merge(ws_host: 'non.existent.host'))
|
145
|
+
end
|
146
|
+
|
147
|
+
it 'enters the failed state and returns an authorization error' do
|
148
|
+
run_reactor do
|
149
|
+
connection.on(:failed) do |error|
|
150
|
+
expect(connection.state).to eq(:failed)
|
151
|
+
expect(error.code).to eq(80000)
|
152
|
+
expect(error.status).to be_nil
|
153
|
+
stop_reactor
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
it 'opens many connections simultaneously' do
|
72
161
|
run_reactor(15) do
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
162
|
+
count, connected_ids = 25, []
|
163
|
+
|
164
|
+
clients = count.times.map do
|
165
|
+
Ably::Realtime::Client.new(default_options)
|
166
|
+
end
|
167
|
+
|
168
|
+
clients.each do |client|
|
169
|
+
client.connection.on(:connected) do
|
170
|
+
connected_ids << client.connection.id
|
171
|
+
|
172
|
+
if connected_ids.count == 25
|
173
|
+
expect(connected_ids.uniq.count).to eql(25)
|
78
174
|
stop_reactor
|
79
175
|
end
|
80
176
|
end
|
@@ -1,5 +1,7 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
require 'securerandom'
|
3
|
+
require 'json'
|
4
|
+
require 'base64'
|
3
5
|
|
4
6
|
describe 'Ably::Realtime::Channel Messages' do
|
5
7
|
include RSpec::EventMachine
|
@@ -18,8 +20,8 @@ describe 'Ably::Realtime::Channel Messages' do
|
|
18
20
|
let(:other_client_channel) { other_client.channel(channel_name) }
|
19
21
|
|
20
22
|
let(:channel_name) { 'subscribe_send_text' }
|
21
|
-
let(:options)
|
22
|
-
let(:payload)
|
23
|
+
let(:options) { { :protocol => :json } }
|
24
|
+
let(:payload) { 'Test message (subscribe_send_text)' }
|
23
25
|
|
24
26
|
it 'sends a string message' do
|
25
27
|
run_reactor do
|
@@ -52,7 +54,7 @@ describe 'Ably::Realtime::Channel Messages' do
|
|
52
54
|
let(:no_echo_channel) { no_echo_client.channel(channel_name) }
|
53
55
|
|
54
56
|
it 'sends a single message without a reply yet the messages is echoed on another normal connection' do
|
55
|
-
run_reactor do
|
57
|
+
run_reactor(10) do
|
56
58
|
channel.attach do |echo_channel|
|
57
59
|
no_echo_channel.attach do
|
58
60
|
no_echo_channel.publish 'test_event', payload
|
@@ -63,7 +65,9 @@ describe 'Ably::Realtime::Channel Messages' do
|
|
63
65
|
|
64
66
|
echo_channel.subscribe('test_event') do |message|
|
65
67
|
expect(message.data).to eql(payload)
|
66
|
-
EventMachine.add_timer(1
|
68
|
+
EventMachine.add_timer(1) do
|
69
|
+
stop_reactor
|
70
|
+
end
|
67
71
|
end
|
68
72
|
end
|
69
73
|
end
|
@@ -72,9 +76,9 @@ describe 'Ably::Realtime::Channel Messages' do
|
|
72
76
|
end
|
73
77
|
|
74
78
|
context 'with multiple messages' do
|
75
|
-
let(:send_count)
|
79
|
+
let(:send_count) { 15 }
|
76
80
|
let(:expected_echos) { send_count * 2 }
|
77
|
-
let(:channel_name)
|
81
|
+
let(:channel_name) { SecureRandom.hex }
|
78
82
|
let(:echos) do
|
79
83
|
{ client: 0, other: 0 }
|
80
84
|
end
|
@@ -82,37 +86,28 @@ describe 'Ably::Realtime::Channel Messages' do
|
|
82
86
|
{ client: 0, other: 0 }
|
83
87
|
end
|
84
88
|
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
expect(echos[:other]).to eql(expected_echos)
|
99
|
-
expect(callbacks[:client]).to eql(send_count)
|
100
|
-
expect(callbacks[:other]).to eql(send_count)
|
101
|
-
stop_reactor
|
102
|
-
end
|
89
|
+
it 'sends and receives the messages on both opened connections and calls the callbacks (expects twice number of messages due to local echos)' do
|
90
|
+
run_reactor(8) do
|
91
|
+
check_message_and_callback_counts = Proc.new do
|
92
|
+
if echos[:client] == expected_echos && echos[:other] == expected_echos
|
93
|
+
# Wait for message backlog to clear
|
94
|
+
EventMachine.add_timer(0.5) do
|
95
|
+
expect(echos[:client]).to eql(expected_echos)
|
96
|
+
expect(echos[:other]).to eql(expected_echos)
|
97
|
+
|
98
|
+
expect(callbacks[:client]).to eql(send_count)
|
99
|
+
expect(callbacks[:other]).to eql(send_count)
|
100
|
+
|
101
|
+
EventMachine.stop
|
103
102
|
end
|
104
103
|
end
|
105
104
|
end
|
106
|
-
end
|
107
|
-
end
|
108
105
|
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
other_client_channel.attach
|
106
|
+
published = false
|
107
|
+
attach_callback = Proc.new do
|
108
|
+
next if published
|
113
109
|
|
114
|
-
|
115
|
-
other_client_channel.on(:attached) do
|
110
|
+
if channel.attached? && other_client_channel.attached?
|
116
111
|
send_count.times do |index|
|
117
112
|
channel.publish('test_event', "#{index}: #{payload}") do
|
118
113
|
callbacks[:client] += 1
|
@@ -121,9 +116,22 @@ describe 'Ably::Realtime::Channel Messages' do
|
|
121
116
|
callbacks[:other] += 1
|
122
117
|
end
|
123
118
|
end
|
124
|
-
|
119
|
+
|
120
|
+
published = true
|
125
121
|
end
|
126
122
|
end
|
123
|
+
|
124
|
+
channel.subscribe('test_event') do |message|
|
125
|
+
echos[:client] += 1
|
126
|
+
check_message_and_callback_counts.call
|
127
|
+
end
|
128
|
+
other_client_channel.subscribe('test_event') do |message|
|
129
|
+
echos[:other] += 1
|
130
|
+
check_message_and_callback_counts.call
|
131
|
+
end
|
132
|
+
|
133
|
+
channel.attach &attach_callback
|
134
|
+
other_client_channel.attach &attach_callback
|
127
135
|
end
|
128
136
|
end
|
129
137
|
end
|
@@ -133,7 +141,7 @@ describe 'Ably::Realtime::Channel Messages' do
|
|
133
141
|
Ably::Realtime::Client.new(options.merge(api_key: restricted_api_key, environment: environment, protocol: protocol))
|
134
142
|
end
|
135
143
|
let(:restricted_channel) { restricted_client.channel("cansubscribe:example") }
|
136
|
-
let(:payload)
|
144
|
+
let(:payload) { 'Test message without permission to publish' }
|
137
145
|
|
138
146
|
it 'calls the error callback' do
|
139
147
|
run_reactor do
|
@@ -153,6 +161,283 @@ describe 'Ably::Realtime::Channel Messages' do
|
|
153
161
|
end
|
154
162
|
end
|
155
163
|
end
|
164
|
+
|
165
|
+
context 'encoding and decoding encrypted messages' do
|
166
|
+
shared_examples 'an Ably encrypter and decrypter' do |item, data|
|
167
|
+
let(:algorithm) { data['algorithm'].upcase }
|
168
|
+
let(:mode) { data['mode'].upcase }
|
169
|
+
let(:key_length) { data['keylength'] }
|
170
|
+
let(:secret_key) { Base64.decode64(data['key']) }
|
171
|
+
let(:iv) { Base64.decode64(data['iv']) }
|
172
|
+
|
173
|
+
let(:cipher_options) { { key: secret_key, iv: iv, algorithm: algorithm, mode: mode, key_length: key_length } }
|
174
|
+
|
175
|
+
context 'publish & subscribe' do
|
176
|
+
let(:encoded) { item['encoded'] }
|
177
|
+
let(:encoded_data) { encoded['data'] }
|
178
|
+
let(:encoded_encoding) { encoded['encoding'] }
|
179
|
+
let(:encoded_data_decoded) do
|
180
|
+
if encoded_encoding == 'json'
|
181
|
+
JSON.parse(encoded_data)
|
182
|
+
elsif encoded_encoding == 'base64'
|
183
|
+
Base64.decode64(encoded_data)
|
184
|
+
else
|
185
|
+
encoded_data
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
let(:encrypted) { item['encrypted'] }
|
190
|
+
let(:encrypted_data) { encrypted['data'] }
|
191
|
+
let(:encrypted_encoding) { encrypted['encoding'] }
|
192
|
+
let(:encrypted_data_decoded) do
|
193
|
+
if encrypted_encoding.match(%r{/base64$})
|
194
|
+
Base64.decode64(encrypted_data)
|
195
|
+
else
|
196
|
+
encrypted_data
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
let(:encrypted_channel) { client.channel(channel_name, encrypted: true, cipher_params: cipher_options) }
|
201
|
+
|
202
|
+
it 'encrypts message automatically when published' do
|
203
|
+
run_reactor do
|
204
|
+
encrypted_channel.__incoming_msgbus__.unsubscribe # remove all subscribe callbacks that could decrypt the message
|
205
|
+
|
206
|
+
encrypted_channel.__incoming_msgbus__.subscribe(:message) do |message|
|
207
|
+
if protocol == :json
|
208
|
+
expect(message['encoding']).to eql(encrypted_encoding)
|
209
|
+
expect(message['data']).to eql(encrypted_data)
|
210
|
+
else
|
211
|
+
# Messages received over binary protocol will not have Base64 encoded data
|
212
|
+
expect(message['encoding']).to eql(encrypted_encoding.gsub(%r{/base64$}, ''))
|
213
|
+
expect(message['data']).to eql(encrypted_data_decoded)
|
214
|
+
end
|
215
|
+
stop_reactor
|
216
|
+
end
|
217
|
+
|
218
|
+
encrypted_channel.publish 'example', encoded_data_decoded
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
it 'sends and receives messages that are encrypted & decrypted by the Ably library' do
|
223
|
+
run_reactor do
|
224
|
+
encrypted_channel.publish 'example', encoded_data_decoded
|
225
|
+
encrypted_channel.subscribe do |message|
|
226
|
+
expect(message.data).to eql(encoded_data_decoded)
|
227
|
+
expect(message.encoding).to be_nil
|
228
|
+
stop_reactor
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
resources_root = File.expand_path('../../../resources', __FILE__)
|
236
|
+
|
237
|
+
def self.add_tests_for_data(data)
|
238
|
+
data['items'].each_with_index do |item, index|
|
239
|
+
context "item #{index} with encrypted encoding #{item['encrypted']['encoding']}" do
|
240
|
+
it_behaves_like 'an Ably encrypter and decrypter', item, data
|
241
|
+
end
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
context 'with AES-128-CBC' do
|
246
|
+
data = JSON.parse(File.read(File.join(resources_root, 'crypto-data-128.json')))
|
247
|
+
add_tests_for_data data
|
248
|
+
end
|
249
|
+
|
250
|
+
context 'with AES-256-CBC' do
|
251
|
+
data = JSON.parse(File.read(File.join(resources_root, 'crypto-data-256.json')))
|
252
|
+
add_tests_for_data data
|
253
|
+
end
|
254
|
+
|
255
|
+
context 'multiple sends from one client to another' do
|
256
|
+
let(:cipher_options) { { key: SecureRandom.hex(32) } }
|
257
|
+
let(:encrypted_channel_client1) { client.channel(channel_name, encrypted: true, cipher_params: cipher_options) }
|
258
|
+
let(:encrypted_channel_client2) { other_client.channel(channel_name, encrypted: true, cipher_params: cipher_options) }
|
259
|
+
|
260
|
+
let(:data) { MessagePack.pack({ 'key' => SecureRandom.hex }) }
|
261
|
+
let(:message_count) { 50 }
|
262
|
+
|
263
|
+
it 'encrypt and decrypt messages' do
|
264
|
+
messages_received = {
|
265
|
+
decrypted: 0,
|
266
|
+
encrypted: 0
|
267
|
+
}
|
268
|
+
|
269
|
+
run_reactor do
|
270
|
+
encrypted_channel_client2.attach do
|
271
|
+
encrypted_channel_client2.subscribe do |message|
|
272
|
+
expect(message.data).to eql("#{message.name}-#{data}")
|
273
|
+
expect(message.encoding).to be_nil
|
274
|
+
messages_received[:decrypted] += 1
|
275
|
+
stop_reactor if messages_received[:decrypted] == message_count
|
276
|
+
end
|
277
|
+
|
278
|
+
encrypted_channel_client1.__incoming_msgbus__.subscribe(:message) do |message|
|
279
|
+
expect(message['encoding']).to match(/cipher\+/)
|
280
|
+
messages_received[:encrypted] += 1
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|
284
|
+
message_count.times do |index|
|
285
|
+
encrypted_channel_client2.publish index.to_s, "#{index}-#{data}"
|
286
|
+
end
|
287
|
+
end
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
context "sending using protocol #{protocol} and subscribing with a different protocol" do
|
292
|
+
let(:other_protocol) { protocol == :msgpack ? :json : :msgpack }
|
293
|
+
let(:other_client) do
|
294
|
+
Ably::Realtime::Client.new(default_options.merge(protocol: other_protocol))
|
295
|
+
end
|
296
|
+
|
297
|
+
let(:cipher_options) { { key: SecureRandom.hex(32), algorithm: 'aes', mode: 'cbc', key_length: 256 } }
|
298
|
+
let(:encrypted_channel_client1) { client.channel(channel_name, encrypted: true, cipher_params: cipher_options) }
|
299
|
+
let(:encrypted_channel_client2) { other_client.channel(channel_name, encrypted: true, cipher_params: cipher_options) }
|
300
|
+
|
301
|
+
before do
|
302
|
+
expect(other_client.protocol_binary?).to_not eql(client.protocol_binary?)
|
303
|
+
end
|
304
|
+
|
305
|
+
[MessagePack.pack({ 'key' => SecureRandom.hex }), '€ unicode', { 'key' => SecureRandom.hex }].each do |payload|
|
306
|
+
payload_description = "#{payload.class}#{" #{payload.encoding}" if payload.kind_of?(String)}"
|
307
|
+
|
308
|
+
specify "delivers a #{payload_description} payload to the receiver" do
|
309
|
+
run_reactor do
|
310
|
+
encrypted_channel_client1.publish 'example', payload
|
311
|
+
encrypted_channel_client2.subscribe do |message|
|
312
|
+
expect(message.data).to eql(payload)
|
313
|
+
expect(message.encoding).to be_nil
|
314
|
+
stop_reactor
|
315
|
+
end
|
316
|
+
end
|
317
|
+
end
|
318
|
+
end
|
319
|
+
end
|
320
|
+
|
321
|
+
context 'publishing on an unencrypted channel and subscribing on an encrypted channel with another client' do
|
322
|
+
let(:cipher_options) { { key: SecureRandom.hex(32), algorithm: 'aes', mode: 'cbc', key_length: 256 } }
|
323
|
+
let(:unencrypted_channel_client1) { client.channel(channel_name) }
|
324
|
+
let(:encrypted_channel_client2) { other_client.channel(channel_name, encrypted: true, cipher_params: cipher_options) }
|
325
|
+
|
326
|
+
let(:payload) { MessagePack.pack({ 'key' => SecureRandom.hex }) }
|
327
|
+
|
328
|
+
it 'does not attempt to decrypt the message' do
|
329
|
+
run_reactor do
|
330
|
+
unencrypted_channel_client1.publish 'example', payload
|
331
|
+
encrypted_channel_client2.subscribe do |message|
|
332
|
+
expect(message.data).to eql(payload)
|
333
|
+
expect(message.encoding).to be_nil
|
334
|
+
stop_reactor
|
335
|
+
end
|
336
|
+
end
|
337
|
+
end
|
338
|
+
end
|
339
|
+
|
340
|
+
context 'publishing on an encrypted channel and subscribing on an unencrypted channel with another client' do
|
341
|
+
let(:cipher_options) { { key: SecureRandom.hex(32), algorithm: 'aes', mode: 'cbc', key_length: 256 } }
|
342
|
+
let(:encrypted_channel_client1) { client.channel(channel_name, encrypted: true, cipher_params: cipher_options) }
|
343
|
+
let(:unencrypted_channel_client2) { other_client.channel(channel_name) }
|
344
|
+
|
345
|
+
let(:payload) { MessagePack.pack({ 'key' => SecureRandom.hex }) }
|
346
|
+
|
347
|
+
it 'delivers the message but still encrypted' do
|
348
|
+
run_reactor do
|
349
|
+
encrypted_channel_client1.publish 'example', payload
|
350
|
+
unencrypted_channel_client2.subscribe do |message|
|
351
|
+
expect(message.data).to_not eql(payload)
|
352
|
+
expect(message.encoding).to match(/^cipher\+aes-256-cbc/)
|
353
|
+
stop_reactor
|
354
|
+
end
|
355
|
+
end
|
356
|
+
end
|
357
|
+
|
358
|
+
it 'triggers a Cipher error on the channel' do
|
359
|
+
run_reactor do
|
360
|
+
unencrypted_channel_client2.attach do
|
361
|
+
encrypted_channel_client1.publish 'example', payload
|
362
|
+
unencrypted_channel_client2.on(:error) do |error|
|
363
|
+
expect(error).to be_a(Ably::Exceptions::CipherError)
|
364
|
+
expect(error.code).to eql(92001)
|
365
|
+
expect(error.message).to match(/Message cannot be decrypted/)
|
366
|
+
stop_reactor
|
367
|
+
end
|
368
|
+
end
|
369
|
+
end
|
370
|
+
end
|
371
|
+
end
|
372
|
+
|
373
|
+
context 'publishing on an encrypted channel and subscribing with a different algorithm on another client' do
|
374
|
+
let(:cipher_options_client1) { { key: SecureRandom.hex(32), algorithm: 'aes', mode: 'cbc', key_length: 256 } }
|
375
|
+
let(:encrypted_channel_client1) { client.channel(channel_name, encrypted: true, cipher_params: cipher_options_client1) }
|
376
|
+
let(:cipher_options_client2) { { key: SecureRandom.hex(32), algorithm: 'aes', mode: 'cbc', key_length: 128 } }
|
377
|
+
let(:encrypted_channel_client2) { other_client.channel(channel_name, encrypted: true, cipher_params: cipher_options_client2) }
|
378
|
+
|
379
|
+
let(:payload) { MessagePack.pack({ 'key' => SecureRandom.hex }) }
|
380
|
+
|
381
|
+
it 'delivers the message but still encrypted' do
|
382
|
+
run_reactor do
|
383
|
+
encrypted_channel_client1.publish 'example', payload
|
384
|
+
encrypted_channel_client2.subscribe do |message|
|
385
|
+
expect(message.data).to_not eql(payload)
|
386
|
+
expect(message.encoding).to match(/^cipher\+aes-256-cbc/)
|
387
|
+
stop_reactor
|
388
|
+
end
|
389
|
+
end
|
390
|
+
end
|
391
|
+
|
392
|
+
it 'triggers a Cipher error on the channel' do
|
393
|
+
run_reactor do
|
394
|
+
encrypted_channel_client2.attach do
|
395
|
+
encrypted_channel_client1.publish 'example', payload
|
396
|
+
encrypted_channel_client2.on(:error) do |error|
|
397
|
+
expect(error).to be_a(Ably::Exceptions::CipherError)
|
398
|
+
expect(error.code).to eql(92002)
|
399
|
+
expect(error.message).to match(/Cipher algorithm [\w\d-]+ does not match/)
|
400
|
+
stop_reactor
|
401
|
+
end
|
402
|
+
end
|
403
|
+
end
|
404
|
+
end
|
405
|
+
end
|
406
|
+
|
407
|
+
context 'publishing on an encrypted channel and subscribing with a different key on another client' do
|
408
|
+
let(:cipher_options_client1) { { key: SecureRandom.hex(32), algorithm: 'aes', mode: 'cbc', key_length: 256 } }
|
409
|
+
let(:encrypted_channel_client1) { client.channel(channel_name, encrypted: true, cipher_params: cipher_options_client1) }
|
410
|
+
let(:cipher_options_client2) { { key: SecureRandom.hex(32), algorithm: 'aes', mode: 'cbc', key_length: 256 } }
|
411
|
+
let(:encrypted_channel_client2) { other_client.channel(channel_name, encrypted: true, cipher_params: cipher_options_client2) }
|
412
|
+
|
413
|
+
let(:payload) { MessagePack.pack({ 'key' => SecureRandom.hex }) }
|
414
|
+
|
415
|
+
it 'delivers the message but still encrypted' do
|
416
|
+
run_reactor do
|
417
|
+
encrypted_channel_client1.publish 'example', payload
|
418
|
+
encrypted_channel_client2.subscribe do |message|
|
419
|
+
expect(message.data).to_not eql(payload)
|
420
|
+
expect(message.encoding).to match(/^cipher\+aes-256-cbc/)
|
421
|
+
stop_reactor
|
422
|
+
end
|
423
|
+
end
|
424
|
+
end
|
425
|
+
|
426
|
+
it 'triggers a Cipher error on the channel' do
|
427
|
+
run_reactor do
|
428
|
+
encrypted_channel_client2.attach do
|
429
|
+
encrypted_channel_client1.publish 'example', payload
|
430
|
+
encrypted_channel_client2.on(:error) do |error|
|
431
|
+
expect(error).to be_a(Ably::Exceptions::CipherError)
|
432
|
+
expect(error.code).to eql(92003)
|
433
|
+
expect(error.message).to match(/CipherError decrypting data/)
|
434
|
+
stop_reactor
|
435
|
+
end
|
436
|
+
end
|
437
|
+
end
|
438
|
+
end
|
439
|
+
end
|
440
|
+
end
|
156
441
|
end
|
157
442
|
end
|
158
443
|
end
|