ably 0.6.2 → 0.7.0

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