spec/acceptance/realtime/message_spec.rb in ably-0.1.6 vs spec/acceptance/realtime/message_spec.rb in ably-0.2.0
- old
+ new
@@ -1,7 +1,9 @@
require 'spec_helper'
require 'securerandom'
+require 'json'
+require 'base64'
describe 'Ably::Realtime::Channel Messages' do
include RSpec::EventMachine
[:msgpack, :json].each do |protocol|
@@ -16,12 +18,12 @@
Ably::Realtime::Client.new(default_options)
end
let(:other_client_channel) { other_client.channel(channel_name) }
let(:channel_name) { 'subscribe_send_text' }
- let(:options) { { :protocol => :json } }
- let(:payload) { 'Test message (subscribe_send_text)' }
+ let(:options) { { :protocol => :json } }
+ let(:payload) { 'Test message (subscribe_send_text)' }
it 'sends a string message' do
run_reactor do
channel.attach
channel.on(:attached) do
@@ -50,92 +52,98 @@
Ably::Realtime::Client.new(default_options.merge(echo_messages: false))
end
let(:no_echo_channel) { no_echo_client.channel(channel_name) }
it 'sends a single message without a reply yet the messages is echoed on another normal connection' do
- run_reactor do
+ run_reactor(10) do
channel.attach do |echo_channel|
no_echo_channel.attach do
no_echo_channel.publish 'test_event', payload
no_echo_channel.subscribe('test_event') do |message|
fail "Message should not have been echoed back"
end
echo_channel.subscribe('test_event') do |message|
expect(message.data).to eql(payload)
- EventMachine.add_timer(1.5) { stop_reactor }
+ EventMachine.add_timer(1) do
+ stop_reactor
+ end
end
end
end
end
end
end
context 'with multiple messages' do
- let(:send_count) { 15 }
+ let(:send_count) { 15 }
let(:expected_echos) { send_count * 2 }
- let(:channel_name) { SecureRandom.hex }
+ let(:channel_name) { SecureRandom.hex }
let(:echos) do
{ client: 0, other: 0 }
end
let(:callbacks) do
{ client: 0, other: 0 }
end
- def expect_messages_to_be_echoed_on_both_connections
- {
- channel => :client,
- other_client_channel => :other
- }.each do |target_channel, echo_key|
- EventMachine.defer do
- target_channel.subscribe('test_event') do |message|
- echos[echo_key] += 1
+ it 'sends and receives the messages on both opened connections and calls the callbacks (expects twice number of messages due to local echos)' do
+ run_reactor(8) do
+ check_message_and_callback_counts = Proc.new do
+ if echos[:client] == expected_echos && echos[:other] == expected_echos
+ # Wait for message backlog to clear
+ EventMachine.add_timer(0.5) do
+ expect(echos[:client]).to eql(expected_echos)
+ expect(echos[:other]).to eql(expected_echos)
- if echos[:client] == expected_echos && echos[:other] == expected_echos
- # Wait briefly before doing the final check in case additional messages received
- EventMachine.add_timer(0.5) do
- expect(echos[:client]).to eql(expected_echos)
- expect(echos[:other]).to eql(expected_echos)
- expect(callbacks[:client]).to eql(send_count)
- expect(callbacks[:other]).to eql(send_count)
- stop_reactor
- end
+ expect(callbacks[:client]).to eql(send_count)
+ expect(callbacks[:other]).to eql(send_count)
+
+ EventMachine.stop
end
end
end
- end
- end
- it 'sends and receives the messages on both opened connections (4 x send count due to local echos) and calls the callbacks' do
- run_reactor(10) do
- channel.attach
- other_client_channel.attach
+ published = false
+ attach_callback = Proc.new do
+ next if published
- channel.on(:attached) do
- other_client_channel.on(:attached) do
+ if channel.attached? && other_client_channel.attached?
send_count.times do |index|
channel.publish('test_event', "#{index}: #{payload}") do
callbacks[:client] += 1
end
other_client_channel.publish('test_event', "#{index}: #{payload}") do
callbacks[:other] += 1
end
end
- expect_messages_to_be_echoed_on_both_connections
+
+ published = true
end
end
+
+ channel.subscribe('test_event') do |message|
+ echos[:client] += 1
+ check_message_and_callback_counts.call
+ end
+ other_client_channel.subscribe('test_event') do |message|
+ echos[:other] += 1
+ check_message_and_callback_counts.call
+ end
+
+ channel.attach &attach_callback
+ other_client_channel.attach &attach_callback
end
end
end
context 'without suitable publishing permissions' do
let(:restricted_client) do
Ably::Realtime::Client.new(options.merge(api_key: restricted_api_key, environment: environment, protocol: protocol))
end
let(:restricted_channel) { restricted_client.channel("cansubscribe:example") }
- let(:payload) { 'Test message without permission to publish' }
+ let(:payload) { 'Test message without permission to publish' }
it 'calls the error callback' do
run_reactor do
restricted_channel.attach
restricted_channel.on(:attached) do
@@ -146,9 +154,286 @@
stop_reactor
end
deferrable.callback do |message|
fail 'Success callback should not have been called'
stop_reactor
+ end
+ end
+ end
+ end
+ end
+
+ context 'encoding and decoding encrypted messages' do
+ shared_examples 'an Ably encrypter and decrypter' do |item, data|
+ let(:algorithm) { data['algorithm'].upcase }
+ let(:mode) { data['mode'].upcase }
+ let(:key_length) { data['keylength'] }
+ let(:secret_key) { Base64.decode64(data['key']) }
+ let(:iv) { Base64.decode64(data['iv']) }
+
+ let(:cipher_options) { { key: secret_key, iv: iv, algorithm: algorithm, mode: mode, key_length: key_length } }
+
+ context 'publish & subscribe' do
+ let(:encoded) { item['encoded'] }
+ let(:encoded_data) { encoded['data'] }
+ let(:encoded_encoding) { encoded['encoding'] }
+ let(:encoded_data_decoded) do
+ if encoded_encoding == 'json'
+ JSON.parse(encoded_data)
+ elsif encoded_encoding == 'base64'
+ Base64.decode64(encoded_data)
+ else
+ encoded_data
+ end
+ end
+
+ let(:encrypted) { item['encrypted'] }
+ let(:encrypted_data) { encrypted['data'] }
+ let(:encrypted_encoding) { encrypted['encoding'] }
+ let(:encrypted_data_decoded) do
+ if encrypted_encoding.match(%r{/base64$})
+ Base64.decode64(encrypted_data)
+ else
+ encrypted_data
+ end
+ end
+
+ let(:encrypted_channel) { client.channel(channel_name, encrypted: true, cipher_params: cipher_options) }
+
+ it 'encrypts message automatically when published' do
+ run_reactor do
+ encrypted_channel.__incoming_msgbus__.unsubscribe # remove all subscribe callbacks that could decrypt the message
+
+ encrypted_channel.__incoming_msgbus__.subscribe(:message) do |message|
+ if protocol == :json
+ expect(message['encoding']).to eql(encrypted_encoding)
+ expect(message['data']).to eql(encrypted_data)
+ else
+ # Messages received over binary protocol will not have Base64 encoded data
+ expect(message['encoding']).to eql(encrypted_encoding.gsub(%r{/base64$}, ''))
+ expect(message['data']).to eql(encrypted_data_decoded)
+ end
+ stop_reactor
+ end
+
+ encrypted_channel.publish 'example', encoded_data_decoded
+ end
+ end
+
+ it 'sends and receives messages that are encrypted & decrypted by the Ably library' do
+ run_reactor do
+ encrypted_channel.publish 'example', encoded_data_decoded
+ encrypted_channel.subscribe do |message|
+ expect(message.data).to eql(encoded_data_decoded)
+ expect(message.encoding).to be_nil
+ stop_reactor
+ end
+ end
+ end
+ end
+ end
+
+ resources_root = File.expand_path('../../../resources', __FILE__)
+
+ def self.add_tests_for_data(data)
+ data['items'].each_with_index do |item, index|
+ context "item #{index} with encrypted encoding #{item['encrypted']['encoding']}" do
+ it_behaves_like 'an Ably encrypter and decrypter', item, data
+ end
+ end
+ end
+
+ context 'with AES-128-CBC' do
+ data = JSON.parse(File.read(File.join(resources_root, 'crypto-data-128.json')))
+ add_tests_for_data data
+ end
+
+ context 'with AES-256-CBC' do
+ data = JSON.parse(File.read(File.join(resources_root, 'crypto-data-256.json')))
+ add_tests_for_data data
+ end
+
+ context 'multiple sends from one client to another' do
+ let(:cipher_options) { { key: SecureRandom.hex(32) } }
+ let(:encrypted_channel_client1) { client.channel(channel_name, encrypted: true, cipher_params: cipher_options) }
+ let(:encrypted_channel_client2) { other_client.channel(channel_name, encrypted: true, cipher_params: cipher_options) }
+
+ let(:data) { MessagePack.pack({ 'key' => SecureRandom.hex }) }
+ let(:message_count) { 50 }
+
+ it 'encrypt and decrypt messages' do
+ messages_received = {
+ decrypted: 0,
+ encrypted: 0
+ }
+
+ run_reactor do
+ encrypted_channel_client2.attach do
+ encrypted_channel_client2.subscribe do |message|
+ expect(message.data).to eql("#{message.name}-#{data}")
+ expect(message.encoding).to be_nil
+ messages_received[:decrypted] += 1
+ stop_reactor if messages_received[:decrypted] == message_count
+ end
+
+ encrypted_channel_client1.__incoming_msgbus__.subscribe(:message) do |message|
+ expect(message['encoding']).to match(/cipher\+/)
+ messages_received[:encrypted] += 1
+ end
+ end
+
+ message_count.times do |index|
+ encrypted_channel_client2.publish index.to_s, "#{index}-#{data}"
+ end
+ end
+ end
+ end
+
+ context "sending using protocol #{protocol} and subscribing with a different protocol" do
+ let(:other_protocol) { protocol == :msgpack ? :json : :msgpack }
+ let(:other_client) do
+ Ably::Realtime::Client.new(default_options.merge(protocol: other_protocol))
+ end
+
+ let(:cipher_options) { { key: SecureRandom.hex(32), algorithm: 'aes', mode: 'cbc', key_length: 256 } }
+ let(:encrypted_channel_client1) { client.channel(channel_name, encrypted: true, cipher_params: cipher_options) }
+ let(:encrypted_channel_client2) { other_client.channel(channel_name, encrypted: true, cipher_params: cipher_options) }
+
+ before do
+ expect(other_client.protocol_binary?).to_not eql(client.protocol_binary?)
+ end
+
+ [MessagePack.pack({ 'key' => SecureRandom.hex }), '€ unicode', { 'key' => SecureRandom.hex }].each do |payload|
+ payload_description = "#{payload.class}#{" #{payload.encoding}" if payload.kind_of?(String)}"
+
+ specify "delivers a #{payload_description} payload to the receiver" do
+ run_reactor do
+ encrypted_channel_client1.publish 'example', payload
+ encrypted_channel_client2.subscribe do |message|
+ expect(message.data).to eql(payload)
+ expect(message.encoding).to be_nil
+ stop_reactor
+ end
+ end
+ end
+ end
+ end
+
+ context 'publishing on an unencrypted channel and subscribing on an encrypted channel with another client' do
+ let(:cipher_options) { { key: SecureRandom.hex(32), algorithm: 'aes', mode: 'cbc', key_length: 256 } }
+ let(:unencrypted_channel_client1) { client.channel(channel_name) }
+ let(:encrypted_channel_client2) { other_client.channel(channel_name, encrypted: true, cipher_params: cipher_options) }
+
+ let(:payload) { MessagePack.pack({ 'key' => SecureRandom.hex }) }
+
+ it 'does not attempt to decrypt the message' do
+ run_reactor do
+ unencrypted_channel_client1.publish 'example', payload
+ encrypted_channel_client2.subscribe do |message|
+ expect(message.data).to eql(payload)
+ expect(message.encoding).to be_nil
+ stop_reactor
+ end
+ end
+ end
+ end
+
+ context 'publishing on an encrypted channel and subscribing on an unencrypted channel with another client' do
+ let(:cipher_options) { { key: SecureRandom.hex(32), algorithm: 'aes', mode: 'cbc', key_length: 256 } }
+ let(:encrypted_channel_client1) { client.channel(channel_name, encrypted: true, cipher_params: cipher_options) }
+ let(:unencrypted_channel_client2) { other_client.channel(channel_name) }
+
+ let(:payload) { MessagePack.pack({ 'key' => SecureRandom.hex }) }
+
+ it 'delivers the message but still encrypted' do
+ run_reactor do
+ encrypted_channel_client1.publish 'example', payload
+ unencrypted_channel_client2.subscribe do |message|
+ expect(message.data).to_not eql(payload)
+ expect(message.encoding).to match(/^cipher\+aes-256-cbc/)
+ stop_reactor
+ end
+ end
+ end
+
+ it 'triggers a Cipher error on the channel' do
+ run_reactor do
+ unencrypted_channel_client2.attach do
+ encrypted_channel_client1.publish 'example', payload
+ unencrypted_channel_client2.on(:error) do |error|
+ expect(error).to be_a(Ably::Exceptions::CipherError)
+ expect(error.code).to eql(92001)
+ expect(error.message).to match(/Message cannot be decrypted/)
+ stop_reactor
+ end
+ end
+ end
+ end
+ end
+
+ context 'publishing on an encrypted channel and subscribing with a different algorithm on another client' do
+ let(:cipher_options_client1) { { key: SecureRandom.hex(32), algorithm: 'aes', mode: 'cbc', key_length: 256 } }
+ let(:encrypted_channel_client1) { client.channel(channel_name, encrypted: true, cipher_params: cipher_options_client1) }
+ let(:cipher_options_client2) { { key: SecureRandom.hex(32), algorithm: 'aes', mode: 'cbc', key_length: 128 } }
+ let(:encrypted_channel_client2) { other_client.channel(channel_name, encrypted: true, cipher_params: cipher_options_client2) }
+
+ let(:payload) { MessagePack.pack({ 'key' => SecureRandom.hex }) }
+
+ it 'delivers the message but still encrypted' do
+ run_reactor do
+ encrypted_channel_client1.publish 'example', payload
+ encrypted_channel_client2.subscribe do |message|
+ expect(message.data).to_not eql(payload)
+ expect(message.encoding).to match(/^cipher\+aes-256-cbc/)
+ stop_reactor
+ end
+ end
+ end
+
+ it 'triggers a Cipher error on the channel' do
+ run_reactor do
+ encrypted_channel_client2.attach do
+ encrypted_channel_client1.publish 'example', payload
+ encrypted_channel_client2.on(:error) do |error|
+ expect(error).to be_a(Ably::Exceptions::CipherError)
+ expect(error.code).to eql(92002)
+ expect(error.message).to match(/Cipher algorithm [\w\d-]+ does not match/)
+ stop_reactor
+ end
+ end
+ end
+ end
+ end
+
+ context 'publishing on an encrypted channel and subscribing with a different key on another client' do
+ let(:cipher_options_client1) { { key: SecureRandom.hex(32), algorithm: 'aes', mode: 'cbc', key_length: 256 } }
+ let(:encrypted_channel_client1) { client.channel(channel_name, encrypted: true, cipher_params: cipher_options_client1) }
+ let(:cipher_options_client2) { { key: SecureRandom.hex(32), algorithm: 'aes', mode: 'cbc', key_length: 256 } }
+ let(:encrypted_channel_client2) { other_client.channel(channel_name, encrypted: true, cipher_params: cipher_options_client2) }
+
+ let(:payload) { MessagePack.pack({ 'key' => SecureRandom.hex }) }
+
+ it 'delivers the message but still encrypted' do
+ run_reactor do
+ encrypted_channel_client1.publish 'example', payload
+ encrypted_channel_client2.subscribe do |message|
+ expect(message.data).to_not eql(payload)
+ expect(message.encoding).to match(/^cipher\+aes-256-cbc/)
+ stop_reactor
+ end
+ end
+ end
+
+ it 'triggers a Cipher error on the channel' do
+ run_reactor do
+ encrypted_channel_client2.attach do
+ encrypted_channel_client1.publish 'example', payload
+ encrypted_channel_client2.on(:error) do |error|
+ expect(error).to be_a(Ably::Exceptions::CipherError)
+ expect(error.code).to eql(92003)
+ expect(error.message).to match(/CipherError decrypting data/)
+ stop_reactor
+ end
end
end
end
end
end