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
@@ -1,12 +1,12 @@
1
- require "spec_helper"
2
- require "securerandom"
1
+ require 'spec_helper'
2
+ require 'securerandom'
3
3
 
4
- describe "REST" do
4
+ describe 'REST' do
5
5
  let(:client) do
6
6
  Ably::Rest::Client.new(api_key: api_key, environment: environment)
7
7
  end
8
8
 
9
- describe "protocol" do
9
+ describe 'protocol' do
10
10
  include Ably::Modules::Conversions
11
11
 
12
12
  let(:client_options) { {} }
@@ -71,8 +71,8 @@ describe "REST" do
71
71
  end
72
72
  end
73
73
 
74
- describe "invalid requests in middleware" do
75
- it "should raise an InvalidRequest exception with a valid message" do
74
+ describe 'invalid requests in middleware' do
75
+ it 'should raise an InvalidRequest exception with a valid message' do
76
76
  invalid_client = Ably::Rest::Client.new(api_key: 'appid.keyuid:keysecret', environment: environment)
77
77
  expect { invalid_client.channel('test').publish('foo', 'choo') }.to raise_error do |error|
78
78
  expect(error).to be_a(Ably::Exceptions::InvalidToken)
@@ -82,7 +82,7 @@ describe "REST" do
82
82
  end
83
83
  end
84
84
 
85
- describe "server error with JSON response", webmock: true do
85
+ describe 'server error with JSON response', webmock: true do
86
86
  let(:error_response) { '{ "error": { "statusCode": 500, "code": 50000, "message": "Internal error" } }' }
87
87
 
88
88
  before do
@@ -90,18 +90,18 @@ describe "REST" do
90
90
  to_return(:status => 500, :body => error_response, :headers => { 'Content-Type' => 'application/json' })
91
91
  end
92
92
 
93
- it "should raise a ServerError exception" do
93
+ it 'should raise a ServerError exception' do
94
94
  expect { client.time }.to raise_error(Ably::Exceptions::ServerError, /Internal error/)
95
95
  end
96
96
  end
97
97
 
98
- describe "server error", webmock: true do
98
+ describe 'server error', webmock: true do
99
99
  before do
100
100
  stub_request(:get, "#{client.endpoint}/time").
101
101
  to_return(:status => 500, :headers => { 'Content-Type' => 'application/json' })
102
102
  end
103
103
 
104
- it "should raise a ServerError exception" do
104
+ it 'should raise a ServerError exception' do
105
105
  expect { client.time }.to raise_error(Ably::Exceptions::ServerError, /Unknown/)
106
106
  end
107
107
  end
@@ -1,7 +1,7 @@
1
- require "spec_helper"
2
- require "securerandom"
1
+ require 'spec_helper'
2
+ require 'securerandom'
3
3
 
4
- describe "REST" do
4
+ describe Ably::Rest::Channel do
5
5
  include Ably::Modules::Conversions
6
6
 
7
7
  [:msgpack, :json].each do |protocol|
@@ -10,44 +10,60 @@ describe "REST" do
10
10
  Ably::Rest::Client.new(api_key: api_key, environment: environment, protocol: protocol)
11
11
  end
12
12
 
13
- describe "publishing messages" do
14
- let(:channel) { client.channel("test") }
15
- let(:event) { "foo" }
16
- let(:message) { "woop!" }
13
+ describe 'publishing messages' do
14
+ let(:channel) { client.channel('test') }
15
+ let(:event) { 'foo' }
16
+ let(:message) { 'woop!' }
17
17
 
18
- it "should publish the message ok" do
18
+ it 'should publish the message ok' do
19
19
  expect(channel.publish(event, message)).to eql(true)
20
20
  end
21
21
  end
22
22
 
23
- describe "fetching channel history" do
23
+ describe 'fetching channel history' do
24
24
  let(:channel) { client.channel("persisted:#{SecureRandom.hex(4)}") }
25
25
  let(:expected_history) do
26
26
  [
27
- { :name => "test1", :data => "foo" },
28
- { :name => "test2", :data => "bar" },
29
- { :name => "test3", :data => "baz" }
27
+ { :name => 'test1', :data => 'foo' },
28
+ { :name => 'test2', :data => 'bar' },
29
+ { :name => 'test3', :data => 'baz' }
30
30
  ]
31
31
  end
32
+ let!(:before_published) { client.time }
32
33
 
33
34
  before(:each) do
34
35
  expected_history.each do |message|
35
- channel.publish(message[:name], message[:data]) || raise("Unable to publish message")
36
+ channel.publish(message[:name], message[:data]) || raise('Unable to publish message')
36
37
  end
37
38
  end
38
39
 
39
- it "should return all the history for the channel" do
40
+ it 'should return all the history for the channel' do
40
41
  actual_history = channel.history
41
42
 
42
43
  expect(actual_history.size).to eql(3)
43
44
 
44
45
  expected_history.each do |message|
45
- expect(actual_history).to include(Ably::Models::Message.new(message))
46
- expect(actual_history.map(&:hash)).to include(message)
46
+ message_name, message_data = message[:name], message[:data]
47
+ matching_message = actual_history.find { |message| message.name == message_name && message.data == message_data }
48
+ expect(matching_message).to be_a(Ably::Models::Message)
47
49
  end
48
50
  end
49
51
 
50
- it "should return paged history" do
52
+ context 'timestamps' do
53
+ it 'should be greater than the time before the messages were published' do
54
+ channel.history.each do |message|
55
+ expect(before_published.to_f).to be < message.timestamp.to_f
56
+ end
57
+ end
58
+ end
59
+
60
+ it 'should return messages with unique IDs' do
61
+ message_ids = channel.history.map(&:id).compact
62
+ expect(message_ids.count).to eql(3)
63
+ expect(message_ids.uniq.count).to eql(3)
64
+ end
65
+
66
+ it 'should return paged history' do
51
67
  page_1 = channel.history(limit: 1)
52
68
  page_2 = page_1.next_page
53
69
  page_3 = page_2.next_page
@@ -71,7 +87,7 @@ describe "REST" do
71
87
  end
72
88
  end
73
89
 
74
- describe "options" do
90
+ describe 'history options' do
75
91
  let(:channel_name) { "persisted:#{SecureRandom.hex(4)}" }
76
92
  let(:channel) { client.channel(channel_name) }
77
93
  let(:endpoint) do
@@ -82,7 +98,7 @@ describe "REST" do
82
98
  end
83
99
 
84
100
  [:start, :end].each do |option|
85
- describe ":{option}", webmock: true do
101
+ describe ":#{option}", webmock: true do
86
102
  let!(:history_stub) {
87
103
  stub_request(:get, "#{endpoint}/channels/#{CGI.escape(channel_name)}/messages?live=true&#{option}=#{milliseconds}").
88
104
  to_return(:body => '{}', :headers => { 'Content-Type' => 'application/json' })
@@ -1,5 +1,5 @@
1
- require "spec_helper"
2
- require "securerandom"
1
+ require 'spec_helper'
2
+ require 'securerandom'
3
3
 
4
4
  describe Ably::Rest::Channels do
5
5
  [:msgpack, :json].each do |protocol|
@@ -10,30 +10,30 @@ describe Ably::Rest::Channels do
10
10
  let(:channel_name) { SecureRandom.hex }
11
11
  let(:options) { { key: 'value' } }
12
12
 
13
- shared_examples "a channel" do
14
- it "should access a channel" do
13
+ shared_examples 'a channel' do
14
+ it 'should access a channel' do
15
15
  expect(channel).to be_a Ably::Rest::Channel
16
16
  expect(channel.name).to eql(channel_name)
17
17
  end
18
18
 
19
- it "should allow options to be set on a channel" do
19
+ it 'should allow options to be set on a channel' do
20
20
  expect(channel_with_options.options).to eql(options)
21
21
  end
22
22
  end
23
23
 
24
- describe "using shortcut method on client" do
24
+ describe 'using shortcut method on client' do
25
25
  let(:channel) { client.channel(channel_name) }
26
26
  let(:channel_with_options) { client.channel(channel_name, options) }
27
27
  it_behaves_like 'a channel'
28
28
  end
29
29
 
30
- describe "using documented .get method on client.channels" do
30
+ describe 'using documented .get method on client.channels' do
31
31
  let(:channel) { client.channels.get(channel_name) }
32
32
  let(:channel_with_options) { client.channels.get(channel_name, options) }
33
33
  it_behaves_like 'a channel'
34
34
  end
35
35
 
36
- describe "using undocumented [] method on client.channels" do
36
+ describe 'using undocumented [] method on client.channels' do
37
37
  let(:channel) { client.channels[channel_name] }
38
38
  let(:channel_with_options) { client.channels[channel_name, options] }
39
39
  it_behaves_like 'a channel'
@@ -0,0 +1,224 @@
1
+ require 'spec_helper'
2
+ require 'securerandom'
3
+
4
+ describe 'Ably::Rest Message' do
5
+ include Ably::Modules::Conversions
6
+
7
+ [:msgpack, :json].each do |protocol|
8
+ context "over #{protocol}" do
9
+ let(:default_client_options) { { api_key: api_key, environment: environment, protocol: protocol } }
10
+ let(:client) { Ably::Rest::Client.new(default_client_options) }
11
+ let(:other_client) { Ably::Rest::Client.new(default_client_options) }
12
+
13
+ describe 'encryption and encoding' do
14
+ let(:channel_name) { "persisted:#{SecureRandom.hex(4)}" }
15
+ let(:cipher_options) { { key: SecureRandom.hex(32) } }
16
+ let(:encrypted_channel) { client.channel(channel_name, encrypted: true, cipher_params: cipher_options) }
17
+
18
+ context 'encoding and decoding encrypted messages' do
19
+ shared_examples 'an Ably encrypter and decrypter' do |item, data|
20
+ let(:algorithm) { data['algorithm'].upcase }
21
+ let(:mode) { data['mode'].upcase }
22
+ let(:key_length) { data['keylength'] }
23
+ let(:secret_key) { Base64.decode64(data['key']) }
24
+ let(:iv) { Base64.decode64(data['iv']) }
25
+
26
+ let(:cipher_options) { { key: secret_key, iv: iv, algorithm: algorithm, mode: mode, key_length: key_length } }
27
+
28
+ context 'publish & subscribe' do
29
+ let(:encoded) { item['encoded'] }
30
+ let(:encoded_data) { encoded['data'] }
31
+ let(:encoded_encoding) { encoded['encoding'] }
32
+ let(:encoded_data_decoded) do
33
+ if encoded_encoding == 'json'
34
+ JSON.parse(encoded_data)
35
+ elsif encoded_encoding == 'base64'
36
+ Base64.decode64(encoded_data)
37
+ else
38
+ encoded_data
39
+ end
40
+ end
41
+
42
+ let(:encrypted) { item['encrypted'] }
43
+ let(:encrypted_data) { encrypted['data'] }
44
+ let(:encrypted_encoding) { encrypted['encoding'] }
45
+ let(:encrypted_data_decoded) do
46
+ if encrypted_encoding.match(%r{/base64$})
47
+ Base64.decode64(encrypted_data)
48
+ else
49
+ encrypted_data
50
+ end
51
+ end
52
+
53
+ it 'encrypts message automatically when published' do
54
+ expect(client).to receive(:post) do |path, message|
55
+ if protocol == :json
56
+ expect(message['encoding']).to eql(encrypted_encoding)
57
+ expect(Base64.decode64(message['data'])).to eql(encrypted_data_decoded)
58
+ else
59
+ # Messages sent over binary protocol will not have Base64 encoded data
60
+ expect(message['encoding']).to eql(encrypted_encoding.gsub(%r{/base64$}, ''))
61
+ expect(message['data']).to eql(encrypted_data_decoded)
62
+ end
63
+ end.and_return(double('Response', status: 201))
64
+
65
+ encrypted_channel.publish 'example', encoded_data_decoded
66
+ end
67
+
68
+ it 'sends and receives messages that are encrypted & decrypted by the Ably library' do
69
+ encrypted_channel.publish 'example', encoded_data_decoded
70
+
71
+ message = encrypted_channel.history.first
72
+ expect(message.data).to eql(encoded_data_decoded)
73
+ expect(message.encoding).to be_nil
74
+ end
75
+ end
76
+ end
77
+
78
+ resources_root = File.expand_path('../../../resources', __FILE__)
79
+
80
+ def self.add_tests_for_data(data)
81
+ data['items'].each_with_index do |item, index|
82
+ context "item #{index} with encrypted encoding #{item['encrypted']['encoding']}" do
83
+ it_behaves_like 'an Ably encrypter and decrypter', item, data
84
+ end
85
+ end
86
+ end
87
+
88
+ context 'with AES-128-CBC' do
89
+ data = JSON.parse(File.read(File.join(resources_root, 'crypto-data-128.json')))
90
+ add_tests_for_data data
91
+ end
92
+
93
+ context 'with AES-256-CBC' do
94
+ data = JSON.parse(File.read(File.join(resources_root, 'crypto-data-256.json')))
95
+ add_tests_for_data data
96
+ end
97
+
98
+ context 'multiple messages' do
99
+ let(:data) { MessagePack.pack({ 'key' => SecureRandom.hex }) }
100
+ let(:message_count) { 20 }
101
+
102
+ it 'encrypt and decrypt messages' do
103
+ message_count.times do |index|
104
+ encrypted_channel.publish index.to_s, "#{index}-#{data}"
105
+ end
106
+
107
+ messages = encrypted_channel.history
108
+
109
+ expect(messages.count).to eql(message_count)
110
+ messages.each do |message|
111
+ expect(message.data).to eql("#{message.name}-#{data}")
112
+ expect(message.encoding).to be_nil
113
+ end
114
+ end
115
+ end
116
+
117
+ context "sending using protocol #{protocol} and retrieving with a different protocol" do
118
+ let(:other_protocol) { protocol == :msgpack ? :json : :msgpack }
119
+ let(:other_client) { Ably::Rest::Client.new(default_client_options.merge(protocol: other_protocol)) }
120
+ let(:other_client_channel) { other_client.channel(channel_name, encrypted: true, cipher_params: cipher_options) }
121
+
122
+ before do
123
+ expect(other_client.protocol_binary?).to_not eql(client.protocol_binary?)
124
+ end
125
+
126
+ [MessagePack.pack({ 'key' => SecureRandom.hex }), '€ unicode', { 'key' => SecureRandom.hex }].each do |payload|
127
+ payload_description = "#{payload.class}#{" #{payload.encoding}" if payload.kind_of?(String)}"
128
+
129
+ specify "delivers a #{payload_description} payload to the receiver" do
130
+ encrypted_channel.publish 'example', payload
131
+
132
+ message = other_client_channel.history.first
133
+ expect(message.data).to eql(payload)
134
+ expect(message.encoding).to be_nil
135
+ end
136
+ end
137
+ end
138
+
139
+ context 'publishing on an unencrypted channel and retrieving on an encrypted channel' do
140
+ let(:unencrypted_channel) { client.channel(channel_name) }
141
+ let(:other_client_encrypted_channel) { other_client.channel(channel_name, encrypted: true, cipher_params: cipher_options) }
142
+
143
+ let(:payload) { MessagePack.pack({ 'key' => SecureRandom.hex }) }
144
+
145
+ it 'does not attempt to decrypt the message' do
146
+ unencrypted_channel.publish 'example', payload
147
+
148
+ message = other_client_encrypted_channel.history.first
149
+ expect(message.data).to eql(payload)
150
+ expect(message.encoding).to be_nil
151
+ end
152
+ end
153
+
154
+ context 'publishing on an encrypted channel and retrieving on an unencrypted channel' do
155
+ let(:encrypted_channel) { client.channel(channel_name, encrypted: true, cipher_params: cipher_options) }
156
+ let(:other_client_unencrypted_channel) { other_client.channel(channel_name) }
157
+
158
+ let(:payload) { MessagePack.pack({ 'key' => SecureRandom.hex }) }
159
+
160
+ skip 'delivers the message but still encrypted' do
161
+ # TODO: Decide if we should raise an exception or allow the message through
162
+ encrypted_channel.publish 'example', payload
163
+
164
+ message = other_client_unencrypted_channel.history.first
165
+ expect(message.data).to_not eql(payload)
166
+ expect(message.encoding).to match(/^cipher\+aes-256-cbc/)
167
+ end
168
+
169
+ it 'triggers a Cipher exception' do
170
+ encrypted_channel.publish 'example', payload
171
+ expect { other_client_unencrypted_channel.history }.to raise_error Ably::Exceptions::CipherError, /Message cannot be decrypted/
172
+ end
173
+ end
174
+
175
+ context 'publishing on an encrypted channel and subscribing with a different algorithm on another client' do
176
+ let(:cipher_options_client1) { { key: SecureRandom.hex(32), algorithm: 'aes', mode: 'cbc', key_length: 256 } }
177
+ let(:encrypted_channel_client1) { client.channel(channel_name, encrypted: true, cipher_params: cipher_options_client1) }
178
+ let(:cipher_options_client2) { { key: SecureRandom.hex(32), algorithm: 'aes', mode: 'cbc', key_length: 128 } }
179
+ let(:encrypted_channel_client2) { other_client.channel(channel_name, encrypted: true, cipher_params: cipher_options_client2) }
180
+
181
+ let(:payload) { MessagePack.pack({ 'key' => SecureRandom.hex }) }
182
+
183
+ skip 'delivers the message but still encrypted' do
184
+ # TODO: Decide if we should raise an exception or allow the message through
185
+ encrypted_channel.publish 'example', payload
186
+
187
+ message = other_client_unencrypted_channel.history.first
188
+ expect(message.data).to_not eql(payload)
189
+ expect(message.encoding).to match(/^cipher\+aes-256-cbc/)
190
+ end
191
+
192
+ it 'triggers a Cipher exception' do
193
+ encrypted_channel_client1.publish 'example', payload
194
+ expect { encrypted_channel_client2.history }.to raise_error Ably::Exceptions::CipherError, /Cipher algorithm [\w\d-]+ does not match/
195
+ end
196
+ end
197
+
198
+ context 'publishing on an encrypted channel and subscribing with a different key on another client' do
199
+ let(:cipher_options_client1) { { key: SecureRandom.hex(32), algorithm: 'aes', mode: 'cbc', key_length: 256 } }
200
+ let(:encrypted_channel_client1) { client.channel(channel_name, encrypted: true, cipher_params: cipher_options_client1) }
201
+ let(:cipher_options_client2) { { key: SecureRandom.hex(32), algorithm: 'aes', mode: 'cbc', key_length: 256 } }
202
+ let(:encrypted_channel_client2) { other_client.channel(channel_name, encrypted: true, cipher_params: cipher_options_client2) }
203
+
204
+ let(:payload) { MessagePack.pack({ 'key' => SecureRandom.hex }) }
205
+
206
+ skip 'delivers the message but still encrypted' do
207
+ # TODO: Decide if we should raise an exception or allow the message through
208
+ encrypted_channel.publish 'example', payload
209
+
210
+ message = other_client_unencrypted_channel.history.first
211
+ expect(message.data).to_not eql(payload)
212
+ expect(message.encoding).to match(/^cipher\+aes-256-cbc/)
213
+ end
214
+
215
+ it 'triggers a Cipher exception' do
216
+ encrypted_channel_client1.publish 'example', payload
217
+ expect { encrypted_channel_client2.history }.to raise_error Ably::Exceptions::CipherError, /CipherError decrypting data/
218
+ end
219
+ end
220
+ end
221
+ end
222
+ end
223
+ end
224
+ end
@@ -1,7 +1,7 @@
1
- require "spec_helper"
2
- require "securerandom"
1
+ require 'spec_helper'
2
+ require 'securerandom'
3
3
 
4
- describe "REST" do
4
+ describe Ably::Rest::Presence do
5
5
  include Ably::Modules::Conversions
6
6
 
7
7
  [:msgpack, :json].each do |protocol|
@@ -12,54 +12,73 @@ describe "REST" do
12
12
 
13
13
  let(:fixtures) do
14
14
  TestApp::APP_SPEC['channels'].first['presence'].map do |fixture|
15
- IdiomaticRubyWrapper(fixture, stop_at: [:client_data])
15
+ IdiomaticRubyWrapper(fixture, stop_at: [:data])
16
16
  end
17
17
  end
18
18
 
19
- describe "fetching presence" do
20
- let(:channel) { client.channel("persisted:presence_fixtures") }
19
+ describe '#get presence' do
20
+ let(:channel) { client.channel('persisted:presence_fixtures') }
21
21
  let(:presence) { channel.presence.get }
22
22
 
23
- it "returns current members on the channel" do
23
+ it 'returns current members on the channel' do
24
24
  expect(presence.size).to eql(4)
25
25
 
26
26
  fixtures.each do |fixture|
27
27
  presence_message = presence.find { |client| client.client_id == fixture[:client_id] }
28
- expect(presence_message.client_data).to eq(fixture[:client_data])
28
+ expect(presence_message.data).to eq(fixture[:data])
29
29
  end
30
30
  end
31
31
  end
32
32
 
33
- describe "presence history" do
34
- let(:channel) { client.channel("persisted:presence_fixtures") }
35
- let(:history) { channel.presence.history }
33
+ describe 'presence #history' do
34
+ let(:channel) { client.channel('persisted:presence_fixtures') }
35
+ let(:presence_history) { channel.presence.history }
36
36
 
37
- it "returns recent presence activity" do
38
- expect(history.size).to eql(4)
37
+ it 'returns recent presence activity' do
38
+ expect(presence_history.size).to eql(4)
39
39
 
40
40
  fixtures.each do |fixture|
41
- presence_message = history.find { |client| client.client_id == fixture['clientId'] }
42
- expect(presence_message.client_data).to eq(fixture[:client_data])
41
+ presence_message = presence_history.find { |client| client.client_id == fixture['clientId'] }
42
+ expect(presence_message.data).to eq(fixture[:data])
43
43
  end
44
44
  end
45
45
 
46
46
  context 'with options' do
47
47
  let(:page_size) { 2 }
48
- let(:paged_history_forward) { channel.presence.history(limit: page_size, direction: :forwards) }
49
48
 
50
- it "returns recent presence activity with options passsed to Ably" do
51
- expect(paged_history_forward).to be_a(Ably::Models::PaginatedResource)
52
- expect(paged_history_forward.size).to eql(2)
49
+ context 'forwards' do
50
+ let(:presence_history) { channel.presence.history(direction: :forwards) }
51
+ let(:paged_history_forward) { channel.presence.history(limit: page_size, direction: :forwards) }
53
52
 
54
- next_page = paged_history_forward.next_page
53
+ it 'returns recent presence activity with options passed to Ably' do
54
+ expect(paged_history_forward).to be_a(Ably::Models::PaginatedResource)
55
+ expect(paged_history_forward.size).to eql(2)
55
56
 
56
- expect(paged_history_forward.first.id).to eql(history.last.id)
57
- expect(next_page.first.id).to eql(history[page_size].id)
57
+ next_page = paged_history_forward.next_page
58
+
59
+ expect(paged_history_forward.first.id).to eql(presence_history.first.id)
60
+ expect(next_page.first.id).to eql(presence_history[page_size].id)
61
+ end
62
+ end
63
+
64
+ context 'backwards' do
65
+ let(:presence_history) { channel.presence.history(direction: :backwards) }
66
+ let(:paged_history_backward) { channel.presence.history(limit: page_size, direction: :backwards) }
67
+
68
+ it 'returns recent presence activity with options passed to Ably' do
69
+ expect(paged_history_backward).to be_a(Ably::Models::PaginatedResource)
70
+ expect(paged_history_backward.size).to eql(2)
71
+
72
+ next_page = paged_history_backward.next_page
73
+
74
+ expect(paged_history_backward.first.id).to eql(presence_history.first.id)
75
+ expect(next_page.first.id).to eql(presence_history[page_size].id)
76
+ end
58
77
  end
59
78
  end
60
79
  end
61
80
 
62
- describe "options" do
81
+ describe 'options' do
63
82
  let(:channel_name) { "persisted:#{SecureRandom.hex(4)}" }
64
83
  let(:presence) { client.channel(channel_name).presence }
65
84
  let(:user) { 'appid.keyuid' }
@@ -106,6 +125,123 @@ describe "REST" do
106
125
  end
107
126
  end
108
127
  end
128
+
129
+ describe 'decoding', webmock: true do
130
+ let(:user) { 'appid.keyuid' }
131
+ let(:secret) { SecureRandom.hex(8) }
132
+ let(:endpoint) do
133
+ client.endpoint.tap do |client_end_point|
134
+ client_end_point.user = user
135
+ client_end_point.password = secret
136
+ end
137
+ end
138
+ let(:client) do
139
+ Ably::Rest::Client.new(api_key: "#{user}:#{secret}", environment: environment, protocol: protocol)
140
+ end
141
+
142
+ let(:data) { SecureRandom.hex(32) }
143
+ let(:channel_name) { "persisted:#{SecureRandom.hex(4)}" }
144
+ let(:cipher_options) { { key: SecureRandom.hex(32), algorithm: 'aes', mode: 'cbc', key_length: 256 } }
145
+ let(:presence) { client.channel(channel_name, encrypted: true, cipher_params: cipher_options).presence }
146
+
147
+ let(:crypto) { Ably::Util::Crypto.new(cipher_options) }
148
+
149
+ let(:content_type) do
150
+ if protocol == :msgpack
151
+ 'application/x-msgpack'
152
+ else
153
+ 'application/json'
154
+ end
155
+ end
156
+
157
+ context 'valid decodeable content' do
158
+ let(:serialized_encoded_message) do
159
+ if protocol == :msgpack
160
+ msg = Ably::Models::PresenceMessage.new({ action: :enter, data: crypto.encrypt(data), encoding: 'utf-8/cipher+aes-256-cbc' })
161
+ MessagePack.pack([msg.as_json])
162
+ else
163
+ msg = Ably::Models::PresenceMessage.new({ action: :enter, data: Base64.encode64(crypto.encrypt(data)), encoding: 'utf-8/cipher+aes-256-cbc/base64' })
164
+ [msg].to_json
165
+ end
166
+ end
167
+
168
+ context '#get' do
169
+ let!(:get_stub) {
170
+ stub_request(:get, "#{endpoint}/channels/#{CGI.escape(channel_name)}/presence").
171
+ to_return(:body => serialized_encoded_message, :headers => { 'Content-Type' => content_type })
172
+ }
173
+
174
+ after do
175
+ expect(get_stub).to have_been_requested
176
+ end
177
+
178
+ it 'automaticaly decodes presence messages' do
179
+ present = presence.get
180
+ expect(present.first.encoding).to be_nil
181
+ expect(present.first.data).to eql(data)
182
+ end
183
+ end
184
+
185
+ context '#history' do
186
+ let!(:history_stub) {
187
+ stub_request(:get, "#{endpoint}/channels/#{CGI.escape(channel_name)}/presence/history?live=true").
188
+ to_return(:body => serialized_encoded_message, :headers => { 'Content-Type' => content_type })
189
+ }
190
+
191
+ after do
192
+ expect(history_stub).to have_been_requested
193
+ end
194
+
195
+ it 'automaticaly decodes presence messages' do
196
+ history = presence.history
197
+ expect(history.first.encoding).to be_nil
198
+ expect(history.first.data).to eql(data)
199
+ end
200
+ end
201
+ end
202
+
203
+ context 'invalid data' do
204
+ let(:serialized_encoded_message_with_invalid_encoding) do
205
+ if protocol == :msgpack
206
+ msg = Ably::Models::PresenceMessage.new({ action: :enter, data: crypto.encrypt(data), encoding: 'utf-8/cipher+aes-128-cbc' })
207
+ MessagePack.pack([msg.as_json])
208
+ else
209
+ msg = Ably::Models::PresenceMessage.new({ action: :enter, data: Base64.encode64(crypto.encrypt(data)), encoding: 'utf-8/cipher+aes-128-cbc/base64' })
210
+ [msg].to_json
211
+ end
212
+ end
213
+
214
+ context '#get' do
215
+ let!(:get_stub) {
216
+ stub_request(:get, "#{endpoint}/channels/#{CGI.escape(channel_name)}/presence").
217
+ to_return(:body => serialized_encoded_message_with_invalid_encoding, :headers => { 'Content-Type' => content_type })
218
+ }
219
+
220
+ after do
221
+ expect(get_stub).to have_been_requested
222
+ end
223
+
224
+ it 'raises a cipher error' do
225
+ expect { presence.get }.to raise_error Ably::Exceptions::CipherError
226
+ end
227
+ end
228
+
229
+ context '#history' do
230
+ let!(:history_stub) {
231
+ stub_request(:get, "#{endpoint}/channels/#{CGI.escape(channel_name)}/presence/history?live=true").
232
+ to_return(:body => serialized_encoded_message_with_invalid_encoding, :headers => { 'Content-Type' => content_type })
233
+ }
234
+
235
+ after do
236
+ expect(history_stub).to have_been_requested
237
+ end
238
+
239
+ it 'raises a cipher error' do
240
+ expect { presence.history }.to raise_error Ably::Exceptions::CipherError
241
+ end
242
+ end
243
+ end
244
+ end
109
245
  end
110
246
  end
111
247
  end