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.
Files changed (97) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -0
  3. data/.travis.yml +9 -0
  4. data/LICENSE.txt +1 -1
  5. data/README.md +8 -1
  6. data/Rakefile +10 -0
  7. data/ably.gemspec +18 -18
  8. data/lib/ably.rb +6 -5
  9. data/lib/ably/auth.rb +11 -14
  10. data/lib/ably/exceptions.rb +18 -15
  11. data/lib/ably/logger.rb +102 -0
  12. data/lib/ably/models/error_info.rb +1 -1
  13. data/lib/ably/models/message.rb +19 -5
  14. data/lib/ably/models/message_encoders/base.rb +107 -0
  15. data/lib/ably/models/message_encoders/base64.rb +39 -0
  16. data/lib/ably/models/message_encoders/cipher.rb +80 -0
  17. data/lib/ably/models/message_encoders/json.rb +33 -0
  18. data/lib/ably/models/message_encoders/utf8.rb +33 -0
  19. data/lib/ably/models/paginated_resource.rb +23 -6
  20. data/lib/ably/models/presence_message.rb +19 -7
  21. data/lib/ably/models/protocol_message.rb +5 -4
  22. data/lib/ably/models/token.rb +2 -2
  23. data/lib/ably/modules/channels_collection.rb +0 -3
  24. data/lib/ably/modules/conversions.rb +3 -3
  25. data/lib/ably/modules/encodeable.rb +68 -0
  26. data/lib/ably/modules/event_emitter.rb +10 -4
  27. data/lib/ably/modules/event_machine_helpers.rb +6 -4
  28. data/lib/ably/modules/http_helpers.rb +7 -2
  29. data/lib/ably/modules/model_common.rb +2 -0
  30. data/lib/ably/modules/state_emitter.rb +10 -1
  31. data/lib/ably/realtime.rb +19 -12
  32. data/lib/ably/realtime/channel.rb +26 -13
  33. data/lib/ably/realtime/client.rb +31 -7
  34. data/lib/ably/realtime/client/incoming_message_dispatcher.rb +14 -3
  35. data/lib/ably/realtime/client/outgoing_message_dispatcher.rb +13 -4
  36. data/lib/ably/realtime/connection.rb +152 -46
  37. data/lib/ably/realtime/connection/connection_manager.rb +168 -0
  38. data/lib/ably/realtime/connection/connection_state_machine.rb +56 -33
  39. data/lib/ably/realtime/connection/websocket_transport.rb +56 -29
  40. data/lib/ably/{models → realtime/models}/nil_channel.rb +1 -1
  41. data/lib/ably/realtime/presence.rb +38 -13
  42. data/lib/ably/rest.rb +7 -5
  43. data/lib/ably/rest/channel.rb +24 -3
  44. data/lib/ably/rest/client.rb +56 -17
  45. data/lib/ably/rest/middleware/encoder.rb +49 -0
  46. data/lib/ably/rest/middleware/exceptions.rb +3 -2
  47. data/lib/ably/rest/middleware/logger.rb +37 -0
  48. data/lib/ably/rest/presence.rb +10 -2
  49. data/lib/ably/util/crypto.rb +57 -29
  50. data/lib/ably/util/pub_sub.rb +11 -0
  51. data/lib/ably/version.rb +1 -1
  52. data/spec/acceptance/realtime/channel_spec.rb +65 -7
  53. data/spec/acceptance/realtime/connection_spec.rb +123 -27
  54. data/spec/acceptance/realtime/message_spec.rb +319 -34
  55. data/spec/acceptance/realtime/presence_history_spec.rb +58 -0
  56. data/spec/acceptance/realtime/presence_spec.rb +160 -18
  57. data/spec/acceptance/rest/auth_spec.rb +93 -49
  58. data/spec/acceptance/rest/base_spec.rb +10 -10
  59. data/spec/acceptance/rest/channel_spec.rb +35 -19
  60. data/spec/acceptance/rest/channels_spec.rb +8 -8
  61. data/spec/acceptance/rest/message_spec.rb +224 -0
  62. data/spec/acceptance/rest/presence_spec.rb +159 -23
  63. data/spec/acceptance/rest/stats_spec.rb +5 -5
  64. data/spec/acceptance/rest/time_spec.rb +4 -4
  65. data/spec/integration/rest/auth.rb +1 -1
  66. data/spec/resources/crypto-data-128.json +56 -0
  67. data/spec/resources/crypto-data-256.json +56 -0
  68. data/spec/rspec_config.rb +39 -0
  69. data/spec/spec_helper.rb +4 -42
  70. data/spec/support/api_helper.rb +1 -1
  71. data/spec/support/event_machine_helper.rb +0 -5
  72. data/spec/support/protocol_msgbus_helper.rb +3 -3
  73. data/spec/support/test_app.rb +3 -3
  74. data/spec/unit/logger_spec.rb +135 -0
  75. data/spec/unit/models/message_encoders/base64_spec.rb +181 -0
  76. data/spec/unit/models/message_encoders/cipher_spec.rb +260 -0
  77. data/spec/unit/models/message_encoders/json_spec.rb +135 -0
  78. data/spec/unit/models/message_encoders/utf8_spec.rb +100 -0
  79. data/spec/unit/models/message_spec.rb +16 -1
  80. data/spec/unit/models/paginated_resource_spec.rb +46 -0
  81. data/spec/unit/models/presence_message_spec.rb +18 -5
  82. data/spec/unit/models/token_spec.rb +1 -1
  83. data/spec/unit/modules/event_emitter_spec.rb +24 -10
  84. data/spec/unit/realtime/channel_spec.rb +3 -3
  85. data/spec/unit/realtime/channels_spec.rb +1 -1
  86. data/spec/unit/realtime/client_spec.rb +44 -2
  87. data/spec/unit/realtime/connection_spec.rb +2 -2
  88. data/spec/unit/realtime/incoming_message_dispatcher_spec.rb +4 -4
  89. data/spec/unit/realtime/presence_spec.rb +1 -1
  90. data/spec/unit/realtime/realtime_spec.rb +3 -3
  91. data/spec/unit/realtime/websocket_transport_spec.rb +24 -0
  92. data/spec/unit/rest/channels_spec.rb +1 -1
  93. data/spec/unit/rest/client_spec.rb +45 -10
  94. data/spec/unit/util/crypto_spec.rb +82 -0
  95. data/spec/unit/{modules → util}/pub_sub_spec.rb +13 -1
  96. metadata +43 -12
  97. data/spec/acceptance/crypto.rb +0 -63
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Ably
2
- VERSION = "0.1.6"
2
+ VERSION = '0.2.0'
3
3
  end
@@ -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) do
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) { SecureRandom.hex(4) }
16
- let(:channel) { client.channel(channel_name) }
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.status_code).to eq(401)
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
- [:msgpack, :json].each do |protocol|
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(api_key: api_key, environment: environment, protocol: protocol)
15
+ Ably::Realtime::Client.new(default_options)
10
16
  end
11
17
 
12
- subject { client.connection }
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
- it 'connects automatically' do
15
- run_reactor do
16
- subject.on(:connected) do
17
- expect(subject.state).to eq(:connected)
18
- stop_reactor
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
- subject.on(phase) do
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 '#closed disconnects and closes the connection once timeout is reached'
68
+ skip '#close disconnects, closes the connection immediately and changes the connection state to closed'
45
69
 
46
- specify '#closed disconnects and closes the connection gracefully' do
70
+ specify '#close(graceful: true) gracefully waits for the server to close the connection' do
47
71
  run_reactor(8) do
48
- subject.close
49
- subject.on(:closed) do
50
- expect(subject.state).to eq(:closed)
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 'receives a heart beat' do
57
- run_reactor(20) do
58
- subject.on(:connected) do
59
- subject.__incoming_protocol_msgbus__.subscribe(:message) do |protocol_message|
60
- if protocol_message.action == :heartbeat
61
- expect(protocol_message.action).to eq(:heartbeat)
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
- skip 'connects, closes gracefully and reconnects on #connect'
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
- it 'connects, closes then connection when timeout is reaached and reconnects on #connect' do
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
- subject.connect do
74
- connection_id = subject.id
75
- subject.close do
76
- subject.connect do
77
- expect(subject.id).to_not eql(connection_id)
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) { { :protocol => :json } }
22
- let(:payload) { 'Test message (subscribe_send_text)' }
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.5) { stop_reactor }
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) { 15 }
79
+ let(:send_count) { 15 }
76
80
  let(:expected_echos) { send_count * 2 }
77
- let(:channel_name) { SecureRandom.hex }
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
- def expect_messages_to_be_echoed_on_both_connections
86
- {
87
- channel => :client,
88
- other_client_channel => :other
89
- }.each do |target_channel, echo_key|
90
- EventMachine.defer do
91
- target_channel.subscribe('test_event') do |message|
92
- echos[echo_key] += 1
93
-
94
- if echos[:client] == expected_echos && echos[:other] == expected_echos
95
- # Wait briefly before doing the final check in case additional messages received
96
- EventMachine.add_timer(0.5) do
97
- expect(echos[:client]).to eql(expected_echos)
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
- it 'sends and receives the messages on both opened connections (4 x send count due to local echos) and calls the callbacks' do
110
- run_reactor(10) do
111
- channel.attach
112
- other_client_channel.attach
106
+ published = false
107
+ attach_callback = Proc.new do
108
+ next if published
113
109
 
114
- channel.on(:attached) do
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
- expect_messages_to_be_echoed_on_both_connections
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) { 'Test message without permission to publish' }
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