ably 0.6.2 → 0.7.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 (119) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +1 -0
  3. data/.ruby-version.old +1 -0
  4. data/.travis.yml +0 -2
  5. data/Rakefile +22 -4
  6. data/SPEC.md +1676 -0
  7. data/ably.gemspec +1 -1
  8. data/lib/ably.rb +0 -8
  9. data/lib/ably/auth.rb +54 -46
  10. data/lib/ably/exceptions.rb +19 -5
  11. data/lib/ably/logger.rb +1 -1
  12. data/lib/ably/models/error_info.rb +1 -1
  13. data/lib/ably/models/idiomatic_ruby_wrapper.rb +11 -9
  14. data/lib/ably/models/message.rb +15 -12
  15. data/lib/ably/models/message_encoders/base.rb +6 -5
  16. data/lib/ably/models/message_encoders/base64.rb +1 -0
  17. data/lib/ably/models/message_encoders/cipher.rb +6 -3
  18. data/lib/ably/models/message_encoders/json.rb +1 -0
  19. data/lib/ably/models/message_encoders/utf8.rb +2 -9
  20. data/lib/ably/models/nil_logger.rb +20 -0
  21. data/lib/ably/models/paginated_resource.rb +5 -2
  22. data/lib/ably/models/presence_message.rb +21 -12
  23. data/lib/ably/models/protocol_message.rb +22 -6
  24. data/lib/ably/modules/ably.rb +11 -0
  25. data/lib/ably/modules/async_wrapper.rb +2 -0
  26. data/lib/ably/modules/conversions.rb +23 -3
  27. data/lib/ably/modules/encodeable.rb +2 -1
  28. data/lib/ably/modules/enum.rb +2 -0
  29. data/lib/ably/modules/event_emitter.rb +7 -1
  30. data/lib/ably/modules/event_machine_helpers.rb +2 -0
  31. data/lib/ably/modules/http_helpers.rb +2 -0
  32. data/lib/ably/modules/model_common.rb +12 -2
  33. data/lib/ably/modules/state_emitter.rb +76 -0
  34. data/lib/ably/modules/state_machine.rb +53 -0
  35. data/lib/ably/modules/statesman_monkey_patch.rb +33 -0
  36. data/lib/ably/modules/uses_state_machine.rb +74 -0
  37. data/lib/ably/realtime.rb +4 -2
  38. data/lib/ably/realtime/channel.rb +51 -58
  39. data/lib/ably/realtime/channel/channel_manager.rb +91 -0
  40. data/lib/ably/realtime/channel/channel_state_machine.rb +68 -0
  41. data/lib/ably/realtime/client.rb +70 -26
  42. data/lib/ably/realtime/client/incoming_message_dispatcher.rb +31 -13
  43. data/lib/ably/realtime/client/outgoing_message_dispatcher.rb +1 -1
  44. data/lib/ably/realtime/connection.rb +135 -92
  45. data/lib/ably/realtime/connection/connection_manager.rb +216 -33
  46. data/lib/ably/realtime/connection/connection_state_machine.rb +30 -73
  47. data/lib/ably/realtime/models/nil_channel.rb +10 -1
  48. data/lib/ably/realtime/presence.rb +336 -92
  49. data/lib/ably/rest.rb +2 -2
  50. data/lib/ably/rest/channel.rb +13 -4
  51. data/lib/ably/rest/client.rb +138 -38
  52. data/lib/ably/rest/middleware/logger.rb +24 -3
  53. data/lib/ably/rest/presence.rb +12 -7
  54. data/lib/ably/version.rb +1 -1
  55. data/spec/acceptance/realtime/channel_history_spec.rb +101 -85
  56. data/spec/acceptance/realtime/channel_spec.rb +461 -120
  57. data/spec/acceptance/realtime/client_spec.rb +119 -0
  58. data/spec/acceptance/realtime/connection_failures_spec.rb +499 -0
  59. data/spec/acceptance/realtime/connection_spec.rb +571 -97
  60. data/spec/acceptance/realtime/message_spec.rb +347 -333
  61. data/spec/acceptance/realtime/presence_history_spec.rb +35 -40
  62. data/spec/acceptance/realtime/presence_spec.rb +769 -239
  63. data/spec/acceptance/realtime/stats_spec.rb +14 -22
  64. data/spec/acceptance/realtime/time_spec.rb +16 -20
  65. data/spec/acceptance/rest/auth_spec.rb +425 -364
  66. data/spec/acceptance/rest/base_spec.rb +108 -176
  67. data/spec/acceptance/rest/channel_spec.rb +89 -89
  68. data/spec/acceptance/rest/channels_spec.rb +30 -32
  69. data/spec/acceptance/rest/client_spec.rb +273 -0
  70. data/spec/acceptance/rest/encoders_spec.rb +185 -0
  71. data/spec/acceptance/rest/message_spec.rb +186 -163
  72. data/spec/acceptance/rest/presence_spec.rb +150 -111
  73. data/spec/acceptance/rest/stats_spec.rb +45 -40
  74. data/spec/acceptance/rest/time_spec.rb +8 -10
  75. data/spec/rspec_config.rb +10 -1
  76. data/spec/shared/client_initializer_behaviour.rb +212 -0
  77. data/spec/{support/model_helper.rb → shared/model_behaviour.rb} +6 -6
  78. data/spec/{support/protocol_msgbus_helper.rb → shared/protocol_msgbus_behaviour.rb} +1 -1
  79. data/spec/spec_helper.rb +9 -0
  80. data/spec/support/api_helper.rb +11 -0
  81. data/spec/support/event_machine_helper.rb +101 -3
  82. data/spec/support/markdown_spec_formatter.rb +90 -0
  83. data/spec/support/private_api_formatter.rb +36 -0
  84. data/spec/support/protocol_helper.rb +32 -0
  85. data/spec/support/random_helper.rb +15 -0
  86. data/spec/support/test_app.rb +4 -0
  87. data/spec/unit/auth_spec.rb +68 -0
  88. data/spec/unit/logger_spec.rb +77 -66
  89. data/spec/unit/models/error_info_spec.rb +1 -1
  90. data/spec/unit/models/idiomatic_ruby_wrapper_spec.rb +2 -3
  91. data/spec/unit/models/message_encoders/base64_spec.rb +2 -2
  92. data/spec/unit/models/message_encoders/cipher_spec.rb +2 -2
  93. data/spec/unit/models/message_encoders/utf8_spec.rb +2 -46
  94. data/spec/unit/models/message_spec.rb +160 -15
  95. data/spec/unit/models/paginated_resource_spec.rb +29 -27
  96. data/spec/unit/models/presence_message_spec.rb +163 -20
  97. data/spec/unit/models/protocol_message_spec.rb +43 -8
  98. data/spec/unit/modules/async_wrapper_spec.rb +2 -3
  99. data/spec/unit/modules/conversions_spec.rb +1 -1
  100. data/spec/unit/modules/enum_spec.rb +2 -3
  101. data/spec/unit/modules/event_emitter_spec.rb +62 -5
  102. data/spec/unit/modules/state_emitter_spec.rb +283 -0
  103. data/spec/unit/realtime/channel_spec.rb +107 -2
  104. data/spec/unit/realtime/channels_spec.rb +1 -0
  105. data/spec/unit/realtime/client_spec.rb +8 -48
  106. data/spec/unit/realtime/connection_spec.rb +3 -3
  107. data/spec/unit/realtime/incoming_message_dispatcher_spec.rb +2 -2
  108. data/spec/unit/realtime/presence_spec.rb +13 -4
  109. data/spec/unit/realtime/realtime_spec.rb +0 -11
  110. data/spec/unit/realtime/websocket_transport_spec.rb +2 -2
  111. data/spec/unit/rest/channel_spec.rb +109 -0
  112. data/spec/unit/rest/channels_spec.rb +4 -3
  113. data/spec/unit/rest/client_spec.rb +30 -125
  114. data/spec/unit/rest/rest_spec.rb +10 -0
  115. data/spec/unit/util/crypto_spec.rb +10 -5
  116. data/spec/unit/util/pub_sub_spec.rb +5 -5
  117. metadata +44 -12
  118. data/spec/integration/modules/state_emitter_spec.rb +0 -80
  119. data/spec/integration/rest/auth.rb +0 -9
@@ -1,221 +1,244 @@
1
+ # encoding: utf-8
1
2
  require 'spec_helper'
2
3
  require 'securerandom'
3
4
 
4
- describe 'Ably::Rest Message' do
5
+ describe Ably::Rest::Channel, 'messages' do
5
6
  include Ably::Modules::Conversions
6
7
 
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
8
+ vary_by_protocol do
9
+ let(:default_client_options) { { api_key: api_key, environment: environment, protocol: protocol } }
10
+ let(:client_options) { default_client_options }
11
+ let(:client) { Ably::Rest::Client.new(client_options) }
12
+ let(:other_client) { Ably::Rest::Client.new(client_options) }
13
+ let(:channel) { client.channel('test') }
14
+
15
+ context 'publishing with an ASCII_8BIT message name' do
16
+ let(:message_name) { random_str.encode(Encoding::ASCII_8BIT) }
17
+
18
+ it 'is converted into UTF_8' do
19
+ channel.publish message_name, 'example'
20
+ message = channel.history.first
21
+ expect(message.name.encoding).to eql(Encoding::UTF_8)
22
+ expect(message.name.encode(Encoding::ASCII_8BIT)).to eql(message_name)
23
+ end
24
+ end
70
25
 
71
- message = encrypted_channel.history.first
72
- expect(message.data).to eql(encoded_data_decoded)
73
- expect(message.encoding).to be_nil
74
- end
26
+ describe 'encryption and encoding' do
27
+ let(:channel_name) { "persisted:#{random_str}" }
28
+ let(:cipher_options) { { key: random_str(32) } }
29
+ let(:encrypted_channel) { client.channel(channel_name, encrypted: true, cipher_params: cipher_options) }
30
+
31
+ context 'with #publish and #history' do
32
+ shared_examples 'an Ably encrypter and decrypter' do |item, data|
33
+ let(:algorithm) { data['algorithm'].upcase }
34
+ let(:mode) { data['mode'].upcase }
35
+ let(:key_length) { data['keylength'] }
36
+ let(:secret_key) { Base64.decode64(data['key']) }
37
+ let(:iv) { Base64.decode64(data['iv']) }
38
+
39
+ let(:cipher_options) { { key: secret_key, iv: iv, algorithm: algorithm, mode: mode, key_length: key_length } }
40
+
41
+ let(:encoded) { item['encoded'] }
42
+ let(:encoded_data) { encoded['data'] }
43
+ let(:encoded_encoding) { encoded['encoding'] }
44
+ let(:encoded_data_decoded) do
45
+ if encoded_encoding == 'json'
46
+ JSON.parse(encoded_data)
47
+ elsif encoded_encoding == 'base64'
48
+ Base64.decode64(encoded_data)
49
+ else
50
+ encoded_data
75
51
  end
76
52
  end
77
53
 
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
54
+ let(:encrypted) { item['encrypted'] }
55
+ let(:encrypted_data) { encrypted['data'] }
56
+ let(:encrypted_encoding) { encrypted['encoding'] }
57
+ let(:encrypted_data_decoded) do
58
+ if encrypted_encoding.match(%r{/base64$})
59
+ Base64.decode64(encrypted_data)
60
+ else
61
+ encrypted_data
85
62
  end
86
63
  end
87
64
 
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
65
+ it 'encrypts message automatically when published' do
66
+ expect(client).to receive(:post) do |path, message|
67
+ if protocol == :json
68
+ expect(message['encoding']).to eql(encrypted_encoding)
69
+ expect(Base64.decode64(message['data'])).to eql(encrypted_data_decoded)
70
+ else
71
+ # Messages sent over binary protocol will not have Base64 encoded data
72
+ expect(message['encoding']).to eql(encrypted_encoding.gsub(%r{/base64$}, ''))
73
+ expect(message['data']).to eql(encrypted_data_decoded)
74
+ end
75
+ end.and_return(double('Response', status: 201))
92
76
 
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
77
+ encrypted_channel.publish 'example', encoded_data_decoded
96
78
  end
97
79
 
98
- context 'multiple messages' do
99
- let(:data) { MessagePack.pack({ 'key' => SecureRandom.hex }) }
100
- let(:message_count) { 20 }
80
+ it 'sends and retrieves messages that are encrypted & decrypted by the Ably library' do
81
+ encrypted_channel.publish 'example', encoded_data_decoded
101
82
 
102
- it 'encrypt and decrypt messages' do
103
- message_count.times do |index|
104
- encrypted_channel.publish index.to_s, "#{index}-#{data}"
105
- end
83
+ message = encrypted_channel.history.first
84
+ expect(message.data).to eql(encoded_data_decoded)
85
+ expect(message.encoding).to be_nil
86
+ end
87
+ end
106
88
 
107
- messages = encrypted_channel.history
89
+ resources_root = File.expand_path('../../../resources', __FILE__)
108
90
 
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
91
+ def self.add_tests_for_data(data)
92
+ data['items'].each_with_index do |item, index|
93
+ context "item #{index} with encrypted encoding #{item['encrypted']['encoding']}" do
94
+ it_behaves_like 'an Ably encrypter and decrypter', item, data
114
95
  end
115
96
  end
97
+ end
116
98
 
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) }
99
+ context 'with AES-128-CBC using crypto-data-128.json fixtures' do
100
+ data = JSON.parse(File.read(File.join(resources_root, 'crypto-data-128.json')))
101
+ add_tests_for_data data
102
+ end
121
103
 
122
- before do
123
- expect(other_client.protocol_binary?).to_not eql(client.protocol_binary?)
124
- end
104
+ context 'with AES-256-CBC using crypto-data-256.json fixtures' do
105
+ data = JSON.parse(File.read(File.join(resources_root, 'crypto-data-256.json')))
106
+ add_tests_for_data data
107
+ end
125
108
 
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)}"
109
+ context 'when publishing lots of messages' do
110
+ let(:data) { MessagePack.pack({ 'key' => random_str }) }
111
+ let(:message_count) { 20 }
128
112
 
129
- specify "delivers a #{payload_description} payload to the receiver" do
130
- encrypted_channel.publish 'example', payload
113
+ it 'encrypts on #publish and decrypts on #history' do
114
+ message_count.times do |index|
115
+ encrypted_channel.publish index.to_s, "#{index}-#{data}"
116
+ end
131
117
 
132
- message = other_client_channel.history.first
133
- expect(message.data).to eql(payload)
134
- expect(message.encoding).to be_nil
135
- end
118
+ messages = encrypted_channel.history
119
+
120
+ expect(messages.count).to eql(message_count)
121
+ messages.each do |message|
122
+ expect(message.data).to eql("#{message.name}-#{data}")
123
+ expect(message.encoding).to be_nil
136
124
  end
137
125
  end
126
+ end
138
127
 
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) }
128
+ context 'when retrieving #history with a different protocol' do
129
+ let(:other_protocol) { protocol == :msgpack ? :json : :msgpack }
130
+ let(:other_client) { Ably::Rest::Client.new(default_client_options.merge(protocol: other_protocol)) }
131
+ let(:other_client_channel) { other_client.channel(channel_name, encrypted: true, cipher_params: cipher_options) }
142
132
 
143
- let(:payload) { MessagePack.pack({ 'key' => SecureRandom.hex }) }
133
+ before do
134
+ expect(other_client.protocol_binary?).to_not eql(client.protocol_binary?)
135
+ end
136
+
137
+ [MessagePack.pack({ 'key' => SecureRandom.hex }), 'ã unicode', { 'key' => SecureRandom.hex }].each do |payload|
138
+ payload_description = "#{payload.class}#{" #{payload.encoding}" if payload.kind_of?(String)}"
144
139
 
145
- it 'does not attempt to decrypt the message' do
146
- unencrypted_channel.publish 'example', payload
140
+ specify "delivers a #{payload_description} payload to the receiver" do
141
+ encrypted_channel.publish 'example', payload
147
142
 
148
- message = other_client_encrypted_channel.history.first
143
+ message = other_client_channel.history.first
149
144
  expect(message.data).to eql(payload)
150
145
  expect(message.encoding).to be_nil
151
146
  end
152
147
  end
148
+ end
153
149
 
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) }
150
+ context 'when publishing on an unencrypted channel and retrieving with #history on an encrypted channel' do
151
+ let(:unencrypted_channel) { client.channel(channel_name) }
152
+ let(:other_client_encrypted_channel) { other_client.channel(channel_name, encrypted: true, cipher_params: cipher_options) }
157
153
 
158
- let(:payload) { MessagePack.pack({ 'key' => SecureRandom.hex }) }
154
+ let(:payload) { MessagePack.pack({ 'key' => random_str }) }
159
155
 
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
156
+ it 'does not attempt to decrypt the message' do
157
+ unencrypted_channel.publish 'example', payload
163
158
 
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
159
+ message = other_client_encrypted_channel.history.first
160
+ expect(message.data).to eql(payload)
161
+ expect(message.encoding).to be_nil
162
+ end
163
+ end
168
164
 
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/
165
+ context 'when publishing on an encrypted channel and retrieving with #history on an unencrypted channel' do
166
+ let(:client_options) { default_client_options.merge(log_level: :fatal) }
167
+ let(:cipher_options) { { key: random_str(32), algorithm: 'aes', mode: 'cbc', key_length: 256 } }
168
+ let(:encrypted_channel) { client.channel(channel_name, encrypted: true, cipher_params: cipher_options) }
169
+ let(:other_client_unencrypted_channel) { other_client.channel(channel_name) }
170
+
171
+ let(:payload) { MessagePack.pack({ 'key' => random_str }) }
172
+
173
+ before do
174
+ encrypted_channel.publish 'example', payload
175
+ end
176
+
177
+ it 'retrieves the message that remains encrypted with an encrypted encoding attribute' do
178
+ message = other_client_unencrypted_channel.history.first
179
+ expect(message.data).to_not eql(payload)
180
+ expect(message.encoding).to match(/^cipher\+aes-256-cbc/)
181
+ end
182
+
183
+ it 'logs a Cipher exception' do
184
+ expect(other_client.logger).to receive(:error) do |message|
185
+ expect(message).to match(/Message cannot be decrypted/)
172
186
  end
187
+ other_client_unencrypted_channel.history
173
188
  end
189
+ end
174
190
 
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) }
191
+ context 'publishing on an encrypted channel and retrieving #history with a different algorithm on another client' do
192
+ let(:client_options) { default_client_options.merge(log_level: :fatal) }
193
+ let(:cipher_options_client1) { { key: random_str(32), algorithm: 'aes', mode: 'cbc', key_length: 256 } }
194
+ let(:encrypted_channel_client1) { client.channel(channel_name, encrypted: true, cipher_params: cipher_options_client1) }
195
+ let(:cipher_options_client2) { { key: random_str(32), algorithm: 'aes', mode: 'cbc', key_length: 128 } }
196
+ let(:encrypted_channel_client2) { other_client.channel(channel_name, encrypted: true, cipher_params: cipher_options_client2) }
180
197
 
181
- let(:payload) { MessagePack.pack({ 'key' => SecureRandom.hex }) }
198
+ let(:payload) { MessagePack.pack({ 'key' => random_str }) }
182
199
 
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
200
+ before do
201
+ encrypted_channel_client1.publish 'example', payload
202
+ end
186
203
 
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
204
+ it 'retrieves the message that remains encrypted with an encrypted encoding attribute' do
205
+ message = encrypted_channel_client2.history.first
206
+ expect(message.data).to_not eql(payload)
207
+ expect(message.encoding).to match(/^cipher\+aes-256-cbc/)
208
+ end
191
209
 
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/
210
+ it 'logs a Cipher exception' do
211
+ expect(other_client.logger).to receive(:error) do |message|
212
+ expect(message).to match(/Cipher algorithm [\w-]+ does not match/)
195
213
  end
214
+ encrypted_channel_client2.history
196
215
  end
216
+ end
197
217
 
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) }
218
+ context 'publishing on an encrypted channel and subscribing with a different key on another client' do
219
+ let(:client_options) { default_client_options.merge(log_level: :fatal) }
220
+ let(:cipher_options_client1) { { key: random_str(32), algorithm: 'aes', mode: 'cbc', key_length: 256 } }
221
+ let(:encrypted_channel_client1) { client.channel(channel_name, encrypted: true, cipher_params: cipher_options_client1) }
222
+ let(:cipher_options_client2) { { key: random_str(32), algorithm: 'aes', mode: 'cbc', key_length: 256 } }
223
+ let(:encrypted_channel_client2) { other_client.channel(channel_name, encrypted: true, cipher_params: cipher_options_client2) }
203
224
 
204
- let(:payload) { MessagePack.pack({ 'key' => SecureRandom.hex }) }
225
+ let(:payload) { MessagePack.pack({ 'key' => random_str }) }
205
226
 
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
227
+ before do
228
+ encrypted_channel_client1.publish 'example', payload
229
+ end
209
230
 
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
231
+ it 'retrieves the message that remains encrypted with an encrypted encoding attribute' do
232
+ message = encrypted_channel_client2.history.first
233
+ expect(message.data).to_not eql(payload)
234
+ expect(message.encoding).to match(/^cipher\+aes-256-cbc/)
235
+ end
214
236
 
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/
237
+ it 'logs a Cipher exception' do
238
+ expect(other_client.logger).to receive(:error) do |message|
239
+ expect(message).to match(/CipherError decrypting data/)
218
240
  end
241
+ encrypted_channel_client2.history
219
242
  end
220
243
  end
221
244
  end
@@ -1,38 +1,57 @@
1
+ # encoding: utf-8
1
2
  require 'spec_helper'
2
- require 'securerandom'
3
3
 
4
4
  describe Ably::Rest::Presence do
5
5
  include Ably::Modules::Conversions
6
6
 
7
- [:msgpack, :json].each do |protocol|
8
- context "over #{protocol}" do
9
- let(:client) do
10
- Ably::Rest::Client.new(api_key: api_key, environment: environment, protocol: protocol)
7
+ vary_by_protocol do
8
+ let(:default_options) { { api_key: api_key, environment: environment, protocol: protocol } }
9
+ let(:client_options) { default_options }
10
+ let(:client) do
11
+ Ably::Rest::Client.new(client_options)
12
+ end
13
+
14
+ let(:fixtures) do
15
+ TestApp::APP_SPEC['channels'].first['presence'].map do |fixture|
16
+ IdiomaticRubyWrapper(fixture, stop_at: [:data])
11
17
  end
18
+ end
12
19
 
13
- let(:fixtures) do
14
- TestApp::APP_SPEC['channels'].first['presence'].map do |fixture|
15
- IdiomaticRubyWrapper(fixture, stop_at: [:data])
16
- end
20
+ context 'tested against presence fixture data set up in test app' do
21
+ before(:context) do
22
+ # When this test is run as a part of a test suite, the presence data injected in the test app may have expired
23
+ WebMock.disable!
24
+ TestApp.reload
17
25
  end
18
26
 
19
- describe '#get presence' do
27
+ describe '#get' do
20
28
  let(:channel) { client.channel('persisted:presence_fixtures') }
21
29
  let(:presence) { channel.presence.get }
22
30
 
23
- it 'returns current members on the channel' do
31
+ it 'returns current members on the channel with their action set to :present' do
24
32
  expect(presence.size).to eql(4)
25
33
 
26
34
  fixtures.each do |fixture|
27
35
  presence_message = presence.find { |client| client.client_id == fixture[:client_id] }
28
36
  expect(presence_message.data).to eq(fixture[:data])
37
+ expect(presence_message.action).to eq(:present)
29
38
  end
30
39
  end
31
40
 
32
- skip 'with options'
41
+ context 'with :limit option' do
42
+ let(:page_size) { 2 }
43
+ let(:presence) { channel.presence.get(limit: page_size) }
44
+
45
+ it 'returns a paged response limiting number of members per page' do
46
+ expect(presence.size).to eql(2)
47
+ next_page = presence.next_page
48
+ expect(next_page.size).to eql(2)
49
+ expect(next_page).to be_last_page
50
+ end
51
+ end
33
52
  end
34
53
 
35
- describe 'presence #history' do
54
+ describe '#history' do
36
55
  let(:channel) { client.channel('persisted:presence_fixtures') }
37
56
  let(:presence_history) { channel.presence.history }
38
57
 
@@ -48,11 +67,11 @@ describe Ably::Rest::Presence do
48
67
  context 'with options' do
49
68
  let(:page_size) { 2 }
50
69
 
51
- context 'forwards' do
70
+ context 'direction: :forwards' do
52
71
  let(:presence_history) { channel.presence.history(direction: :forwards) }
53
72
  let(:paged_history_forward) { channel.presence.history(limit: page_size, direction: :forwards) }
54
73
 
55
- it 'returns recent presence activity with options passed to Ably' do
74
+ it 'returns recent presence activity forwards with most recent history last' do
56
75
  expect(paged_history_forward).to be_a(Ably::Models::PaginatedResource)
57
76
  expect(paged_history_forward.size).to eql(2)
58
77
 
@@ -63,11 +82,11 @@ describe Ably::Rest::Presence do
63
82
  end
64
83
  end
65
84
 
66
- context 'backwards' do
85
+ context 'direction: :backwards' do
67
86
  let(:presence_history) { channel.presence.history(direction: :backwards) }
68
87
  let(:paged_history_backward) { channel.presence.history(limit: page_size, direction: :backwards) }
69
88
 
70
- it 'returns recent presence activity with options passed to Ably' do
89
+ it 'returns recent presence activity backwards with most recent history first' do
71
90
  expect(paged_history_backward).to be_a(Ably::Models::PaginatedResource)
72
91
  expect(paged_history_backward.size).to eql(2)
73
92
 
@@ -79,12 +98,14 @@ describe Ably::Rest::Presence do
79
98
  end
80
99
  end
81
100
  end
101
+ end
82
102
 
83
- describe 'options' do
84
- let(:channel_name) { "persisted:#{SecureRandom.hex(4)}" }
103
+ describe '#history' do
104
+ context 'with time range options' do
105
+ let(:channel_name) { "persisted:#{random_str(4)}" }
85
106
  let(:presence) { client.channel(channel_name).presence }
86
107
  let(:user) { 'appid.keyuid' }
87
- let(:secret) { SecureRandom.hex(8) }
108
+ let(:secret) { random_str(8) }
88
109
  let(:endpoint) do
89
110
  client.endpoint.tap do |client_end_point|
90
111
  client_end_point.user = user
@@ -96,9 +117,9 @@ describe Ably::Rest::Presence do
96
117
  end
97
118
 
98
119
  [:start, :end].each do |option|
99
- describe ":{option}", webmock: true do
120
+ describe ":#{option}", :webmock do
100
121
  let!(:history_stub) {
101
- stub_request(:get, "#{endpoint}/channels/#{CGI.escape(channel_name)}/presence/history?live=true&#{option}=#{milliseconds}").
122
+ stub_request(:get, "#{endpoint}/channels/#{CGI.escape(channel_name)}/presence/history?#{option}=#{milliseconds}").
102
123
  to_return(:body => '{}', :headers => { 'Content-Type' => 'application/json' })
103
124
  }
104
125
 
@@ -106,141 +127,159 @@ describe Ably::Rest::Presence do
106
127
  presence.history(options)
107
128
  end
108
129
 
109
- context 'with milliseconds since epoch' do
130
+ context 'with milliseconds since epoch value' do
110
131
  let(:milliseconds) { as_since_epoch(Time.now) }
111
132
  let(:options) { { option => milliseconds } }
112
133
 
113
- specify 'are left unchanged' do
134
+ it 'uses this value in the history request' do
114
135
  expect(history_stub).to have_been_requested
115
136
  end
116
137
  end
117
138
 
118
- context 'with Time' do
139
+ context 'with Time object value' do
119
140
  let(:time) { Time.now }
120
141
  let(:milliseconds) { as_since_epoch(time) }
121
142
  let(:options) { { option => time } }
122
143
 
123
- specify 'are left unchanged' do
144
+ it 'converts the value to milliseconds since epoch in the hisotry request' do
124
145
  expect(history_stub).to have_been_requested
125
146
  end
126
147
  end
127
148
  end
128
149
  end
129
150
  end
151
+ end
130
152
 
131
- describe 'decoding', webmock: true do
132
- let(:user) { 'appid.keyuid' }
133
- let(:secret) { SecureRandom.hex(8) }
134
- let(:endpoint) do
135
- client.endpoint.tap do |client_end_point|
136
- client_end_point.user = user
137
- client_end_point.password = secret
138
- end
139
- end
140
- let(:client) do
141
- Ably::Rest::Client.new(api_key: "#{user}:#{secret}", environment: environment, protocol: protocol)
153
+ describe 'decoding', :webmock do
154
+ let(:user) { 'appid.keyuid' }
155
+ let(:secret) { random_str(8) }
156
+ let(:endpoint) do
157
+ client.endpoint.tap do |client_end_point|
158
+ client_end_point.user = user
159
+ client_end_point.password = secret
142
160
  end
161
+ end
162
+ let(:client) do
163
+ Ably::Rest::Client.new(client_options.merge(api_key: "#{user}:#{secret}"))
164
+ end
165
+
166
+ let(:data) { random_str(32) }
167
+ let(:channel_name) { "persisted:#{random_str(4)}" }
168
+ let(:cipher_options) { { key: random_str(32), algorithm: 'aes', mode: 'cbc', key_length: 256 } }
169
+ let(:presence) { client.channel(channel_name, encrypted: true, cipher_params: cipher_options).presence }
143
170
 
144
- let(:data) { SecureRandom.hex(32) }
145
- let(:channel_name) { "persisted:#{SecureRandom.hex(4)}" }
146
- let(:cipher_options) { { key: SecureRandom.hex(32), algorithm: 'aes', mode: 'cbc', key_length: 256 } }
147
- let(:presence) { client.channel(channel_name, encrypted: true, cipher_params: cipher_options).presence }
171
+ let(:crypto) { Ably::Util::Crypto.new(cipher_options) }
148
172
 
149
- let(:crypto) { Ably::Util::Crypto.new(cipher_options) }
173
+ let(:content_type) do
174
+ if protocol == :msgpack
175
+ 'application/x-msgpack'
176
+ else
177
+ 'application/json'
178
+ end
179
+ end
150
180
 
151
- let(:content_type) do
181
+ context 'valid decodeable content' do
182
+ let(:serialized_encoded_message) do
152
183
  if protocol == :msgpack
153
- 'application/x-msgpack'
184
+ msg = Ably::Models::PresenceMessage.new({ action: :enter, data: crypto.encrypt(data), encoding: 'utf-8/cipher+aes-256-cbc' })
185
+ MessagePack.pack([msg.as_json])
154
186
  else
155
- 'application/json'
187
+ msg = Ably::Models::PresenceMessage.new({ action: :enter, data: Base64.encode64(crypto.encrypt(data)), encoding: 'utf-8/cipher+aes-256-cbc/base64' })
188
+ [msg].to_json
156
189
  end
157
190
  end
158
191
 
159
- context 'valid decodeable content' do
160
- let(:serialized_encoded_message) do
161
- if protocol == :msgpack
162
- msg = Ably::Models::PresenceMessage.new({ action: :enter, data: crypto.encrypt(data), encoding: 'utf-8/cipher+aes-256-cbc' })
163
- MessagePack.pack([msg.as_json])
164
- else
165
- msg = Ably::Models::PresenceMessage.new({ action: :enter, data: Base64.encode64(crypto.encrypt(data)), encoding: 'utf-8/cipher+aes-256-cbc/base64' })
166
- [msg].to_json
167
- end
168
- end
192
+ context '#get' do
193
+ let!(:get_stub) {
194
+ stub_request(:get, "#{endpoint}/channels/#{CGI.escape(channel_name)}/presence").
195
+ to_return(:body => serialized_encoded_message, :headers => { 'Content-Type' => content_type })
196
+ }
169
197
 
170
- context '#get' do
171
- let!(:get_stub) {
172
- stub_request(:get, "#{endpoint}/channels/#{CGI.escape(channel_name)}/presence").
173
- to_return(:body => serialized_encoded_message, :headers => { 'Content-Type' => content_type })
174
- }
175
-
176
- after do
177
- expect(get_stub).to have_been_requested
178
- end
198
+ after do
199
+ expect(get_stub).to have_been_requested
200
+ end
179
201
 
180
- it 'automaticaly decodes presence messages' do
181
- present = presence.get
182
- expect(present.first.encoding).to be_nil
183
- expect(present.first.data).to eql(data)
184
- end
202
+ it 'automaticaly decodes presence messages' do
203
+ present = presence.get
204
+ expect(present.first.encoding).to be_nil
205
+ expect(present.first.data).to eql(data)
185
206
  end
207
+ end
186
208
 
187
- context '#history' do
188
- let!(:history_stub) {
189
- stub_request(:get, "#{endpoint}/channels/#{CGI.escape(channel_name)}/presence/history?live=true").
190
- to_return(:body => serialized_encoded_message, :headers => { 'Content-Type' => content_type })
191
- }
209
+ context '#history' do
210
+ let!(:history_stub) {
211
+ stub_request(:get, "#{endpoint}/channels/#{CGI.escape(channel_name)}/presence/history").
212
+ to_return(:body => serialized_encoded_message, :headers => { 'Content-Type' => content_type })
213
+ }
192
214
 
193
- after do
194
- expect(history_stub).to have_been_requested
195
- end
215
+ after do
216
+ expect(history_stub).to have_been_requested
217
+ end
196
218
 
197
- it 'automaticaly decodes presence messages' do
198
- history = presence.history
199
- expect(history.first.encoding).to be_nil
200
- expect(history.first.data).to eql(data)
201
- end
219
+ it 'automaticaly decodes presence messages' do
220
+ history = presence.history
221
+ expect(history.first.encoding).to be_nil
222
+ expect(history.first.data).to eql(data)
202
223
  end
203
224
  end
225
+ end
204
226
 
205
- context 'invalid data' do
206
- let(:serialized_encoded_message_with_invalid_encoding) do
207
- if protocol == :msgpack
208
- msg = Ably::Models::PresenceMessage.new({ action: :enter, data: crypto.encrypt(data), encoding: 'utf-8/cipher+aes-128-cbc' })
209
- MessagePack.pack([msg.as_json])
210
- else
211
- msg = Ably::Models::PresenceMessage.new({ action: :enter, data: Base64.encode64(crypto.encrypt(data)), encoding: 'utf-8/cipher+aes-128-cbc/base64' })
212
- [msg].to_json
213
- end
227
+ context 'invalid data' do
228
+ let(:serialized_encoded_message_with_invalid_encoding) do
229
+ if protocol == :msgpack
230
+ msg = Ably::Models::PresenceMessage.new({ action: :enter, data: crypto.encrypt(data), encoding: 'utf-8/cipher+aes-128-cbc' })
231
+ MessagePack.pack([msg.as_json])
232
+ else
233
+ msg = Ably::Models::PresenceMessage.new({ action: :enter, data: Base64.encode64(crypto.encrypt(data)), encoding: 'utf-8/cipher+aes-128-cbc/base64' })
234
+ [msg].to_json
214
235
  end
236
+ end
215
237
 
216
- context '#get' do
217
- let!(:get_stub) {
218
- stub_request(:get, "#{endpoint}/channels/#{CGI.escape(channel_name)}/presence").
219
- to_return(:body => serialized_encoded_message_with_invalid_encoding, :headers => { 'Content-Type' => content_type })
220
- }
238
+ context '#get' do
239
+ let(:client_options) { default_options.merge(log_level: :fatal) }
240
+ let!(:get_stub) {
241
+ stub_request(:get, "#{endpoint}/channels/#{CGI.escape(channel_name)}/presence").
242
+ to_return(:body => serialized_encoded_message_with_invalid_encoding, :headers => { 'Content-Type' => content_type })
243
+ }
244
+ let(:presence_message) { presence.get.first }
221
245
 
222
- after do
223
- expect(get_stub).to have_been_requested
224
- end
246
+ after do
247
+ expect(get_stub).to have_been_requested
248
+ end
249
+
250
+ it 'returns the messages still encoded' do
251
+ expect(presence_message.encoding).to match(/cipher\+aes-128-cbc/)
252
+ end
225
253
 
226
- it 'raises a cipher error' do
227
- expect { presence.get }.to raise_error Ably::Exceptions::CipherError
254
+ it 'logs a cipher error' do
255
+ expect(client.logger).to receive(:error) do |message|
256
+ expect(message).to match(/Cipher algorithm [\w-]+ does not match/)
228
257
  end
258
+ presence.get
229
259
  end
260
+ end
230
261
 
231
- context '#history' do
232
- let!(:history_stub) {
233
- stub_request(:get, "#{endpoint}/channels/#{CGI.escape(channel_name)}/presence/history?live=true").
234
- to_return(:body => serialized_encoded_message_with_invalid_encoding, :headers => { 'Content-Type' => content_type })
235
- }
262
+ context '#history' do
263
+ let(:client_options) { default_options.merge(log_level: :fatal) }
264
+ let!(:history_stub) {
265
+ stub_request(:get, "#{endpoint}/channels/#{CGI.escape(channel_name)}/presence/history").
266
+ to_return(:body => serialized_encoded_message_with_invalid_encoding, :headers => { 'Content-Type' => content_type })
267
+ }
268
+ let(:presence_message) { presence.history.first }
236
269
 
237
- after do
238
- expect(history_stub).to have_been_requested
239
- end
270
+ after do
271
+ expect(history_stub).to have_been_requested
272
+ end
273
+
274
+ it 'returns the messages still encoded' do
275
+ expect(presence_message.encoding).to match(/cipher\+aes-128-cbc/)
276
+ end
240
277
 
241
- it 'raises a cipher error' do
242
- expect { presence.history }.to raise_error Ably::Exceptions::CipherError
278
+ it 'logs a cipher error' do
279
+ expect(client.logger).to receive(:error) do |message|
280
+ expect(message).to match(/Cipher algorithm [\w-]+ does not match/)
243
281
  end
282
+ presence.history
244
283
  end
245
284
  end
246
285
  end