ably 0.1.6 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (97) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -0
  3. data/.travis.yml +9 -0
  4. data/LICENSE.txt +1 -1
  5. data/README.md +8 -1
  6. data/Rakefile +10 -0
  7. data/ably.gemspec +18 -18
  8. data/lib/ably.rb +6 -5
  9. data/lib/ably/auth.rb +11 -14
  10. data/lib/ably/exceptions.rb +18 -15
  11. data/lib/ably/logger.rb +102 -0
  12. data/lib/ably/models/error_info.rb +1 -1
  13. data/lib/ably/models/message.rb +19 -5
  14. data/lib/ably/models/message_encoders/base.rb +107 -0
  15. data/lib/ably/models/message_encoders/base64.rb +39 -0
  16. data/lib/ably/models/message_encoders/cipher.rb +80 -0
  17. data/lib/ably/models/message_encoders/json.rb +33 -0
  18. data/lib/ably/models/message_encoders/utf8.rb +33 -0
  19. data/lib/ably/models/paginated_resource.rb +23 -6
  20. data/lib/ably/models/presence_message.rb +19 -7
  21. data/lib/ably/models/protocol_message.rb +5 -4
  22. data/lib/ably/models/token.rb +2 -2
  23. data/lib/ably/modules/channels_collection.rb +0 -3
  24. data/lib/ably/modules/conversions.rb +3 -3
  25. data/lib/ably/modules/encodeable.rb +68 -0
  26. data/lib/ably/modules/event_emitter.rb +10 -4
  27. data/lib/ably/modules/event_machine_helpers.rb +6 -4
  28. data/lib/ably/modules/http_helpers.rb +7 -2
  29. data/lib/ably/modules/model_common.rb +2 -0
  30. data/lib/ably/modules/state_emitter.rb +10 -1
  31. data/lib/ably/realtime.rb +19 -12
  32. data/lib/ably/realtime/channel.rb +26 -13
  33. data/lib/ably/realtime/client.rb +31 -7
  34. data/lib/ably/realtime/client/incoming_message_dispatcher.rb +14 -3
  35. data/lib/ably/realtime/client/outgoing_message_dispatcher.rb +13 -4
  36. data/lib/ably/realtime/connection.rb +152 -46
  37. data/lib/ably/realtime/connection/connection_manager.rb +168 -0
  38. data/lib/ably/realtime/connection/connection_state_machine.rb +56 -33
  39. data/lib/ably/realtime/connection/websocket_transport.rb +56 -29
  40. data/lib/ably/{models → realtime/models}/nil_channel.rb +1 -1
  41. data/lib/ably/realtime/presence.rb +38 -13
  42. data/lib/ably/rest.rb +7 -5
  43. data/lib/ably/rest/channel.rb +24 -3
  44. data/lib/ably/rest/client.rb +56 -17
  45. data/lib/ably/rest/middleware/encoder.rb +49 -0
  46. data/lib/ably/rest/middleware/exceptions.rb +3 -2
  47. data/lib/ably/rest/middleware/logger.rb +37 -0
  48. data/lib/ably/rest/presence.rb +10 -2
  49. data/lib/ably/util/crypto.rb +57 -29
  50. data/lib/ably/util/pub_sub.rb +11 -0
  51. data/lib/ably/version.rb +1 -1
  52. data/spec/acceptance/realtime/channel_spec.rb +65 -7
  53. data/spec/acceptance/realtime/connection_spec.rb +123 -27
  54. data/spec/acceptance/realtime/message_spec.rb +319 -34
  55. data/spec/acceptance/realtime/presence_history_spec.rb +58 -0
  56. data/spec/acceptance/realtime/presence_spec.rb +160 -18
  57. data/spec/acceptance/rest/auth_spec.rb +93 -49
  58. data/spec/acceptance/rest/base_spec.rb +10 -10
  59. data/spec/acceptance/rest/channel_spec.rb +35 -19
  60. data/spec/acceptance/rest/channels_spec.rb +8 -8
  61. data/spec/acceptance/rest/message_spec.rb +224 -0
  62. data/spec/acceptance/rest/presence_spec.rb +159 -23
  63. data/spec/acceptance/rest/stats_spec.rb +5 -5
  64. data/spec/acceptance/rest/time_spec.rb +4 -4
  65. data/spec/integration/rest/auth.rb +1 -1
  66. data/spec/resources/crypto-data-128.json +56 -0
  67. data/spec/resources/crypto-data-256.json +56 -0
  68. data/spec/rspec_config.rb +39 -0
  69. data/spec/spec_helper.rb +4 -42
  70. data/spec/support/api_helper.rb +1 -1
  71. data/spec/support/event_machine_helper.rb +0 -5
  72. data/spec/support/protocol_msgbus_helper.rb +3 -3
  73. data/spec/support/test_app.rb +3 -3
  74. data/spec/unit/logger_spec.rb +135 -0
  75. data/spec/unit/models/message_encoders/base64_spec.rb +181 -0
  76. data/spec/unit/models/message_encoders/cipher_spec.rb +260 -0
  77. data/spec/unit/models/message_encoders/json_spec.rb +135 -0
  78. data/spec/unit/models/message_encoders/utf8_spec.rb +100 -0
  79. data/spec/unit/models/message_spec.rb +16 -1
  80. data/spec/unit/models/paginated_resource_spec.rb +46 -0
  81. data/spec/unit/models/presence_message_spec.rb +18 -5
  82. data/spec/unit/models/token_spec.rb +1 -1
  83. data/spec/unit/modules/event_emitter_spec.rb +24 -10
  84. data/spec/unit/realtime/channel_spec.rb +3 -3
  85. data/spec/unit/realtime/channels_spec.rb +1 -1
  86. data/spec/unit/realtime/client_spec.rb +44 -2
  87. data/spec/unit/realtime/connection_spec.rb +2 -2
  88. data/spec/unit/realtime/incoming_message_dispatcher_spec.rb +4 -4
  89. data/spec/unit/realtime/presence_spec.rb +1 -1
  90. data/spec/unit/realtime/realtime_spec.rb +3 -3
  91. data/spec/unit/realtime/websocket_transport_spec.rb +24 -0
  92. data/spec/unit/rest/channels_spec.rb +1 -1
  93. data/spec/unit/rest/client_spec.rb +45 -10
  94. data/spec/unit/util/crypto_spec.rb +82 -0
  95. data/spec/unit/{modules → util}/pub_sub_spec.rb +13 -1
  96. metadata +43 -12
  97. data/spec/acceptance/crypto.rb +0 -63
@@ -0,0 +1,100 @@
1
+ require 'spec_helper'
2
+
3
+ require 'ably/models/message_encoders/utf8'
4
+
5
+ describe Ably::Models::MessageEncoders::Utf8 do
6
+ let(:string_ascii) { 'string'.force_encoding(Encoding::ASCII_8BIT) }
7
+ let(:string_utf8) { 'string'.force_encoding(Encoding::UTF_8) }
8
+
9
+ let(:client) { instance_double('Ably::Realtime::Client') }
10
+
11
+ subject { Ably::Models::MessageEncoders::Utf8.new(client) }
12
+
13
+ context '#decode' do
14
+ before do
15
+ subject.decode message, {}
16
+ end
17
+
18
+ context 'message with utf8 payload' do
19
+ let(:message) { { data: string_ascii, encoding: 'utf-8' } }
20
+
21
+ it 'sets the encoding' do
22
+ expect(message[:data]).to eq(string_utf8)
23
+ expect(message[:data].encoding).to eql(Encoding::UTF_8)
24
+ end
25
+
26
+ it 'strips the encoding' do
27
+ expect(message[:encoding]).to be_nil
28
+ end
29
+ end
30
+
31
+ context 'message with utf8 payload before other payloads' do
32
+ let(:message) { { data: string_utf8, encoding: 'json/utf-8' } }
33
+
34
+ it 'sets the encoding' do
35
+ expect(message[:data]).to eql(string_utf8)
36
+ expect(message[:data].encoding).to eql(Encoding::UTF_8)
37
+ end
38
+
39
+ it 'strips the encoding' do
40
+ expect(message[:encoding]).to eql('json')
41
+ end
42
+ end
43
+
44
+ context 'message with another payload' do
45
+ let(:message) { { data: string_ascii, encoding: 'json' } }
46
+
47
+ it 'leaves the message data intact' do
48
+ expect(message[:data]).to eql(string_ascii)
49
+ end
50
+
51
+ it 'leaves the encoding intact' do
52
+ expect(message[:encoding]).to eql('json')
53
+ end
54
+ end
55
+ end
56
+
57
+ context '#encode' do
58
+ before do
59
+ subject.encode message, {}
60
+ end
61
+
62
+ context 'message with json payload' do
63
+ let(:message) { { data: string_ascii, encoding: 'json' } }
64
+
65
+ it 'sets the cencoding' do
66
+ expect(message[:data]).to eql(string_utf8)
67
+ expect(message[:data].encoding).to eql(Encoding::UTF_8)
68
+ end
69
+
70
+ it 'adds the encoding' do
71
+ expect(message[:encoding]).to eql('json/utf-8')
72
+ end
73
+ end
74
+
75
+
76
+ context 'message with string payload and no encoding' do
77
+ let(:message) { { data: string_ascii, encoding: nil } }
78
+
79
+ it 'leaves the message data intact' do
80
+ expect(message[:data]).to eql(string_ascii)
81
+ end
82
+
83
+ it 'leaves the encoding intact' do
84
+ expect(message[:encoding]).to eql(nil)
85
+ end
86
+ end
87
+
88
+ context 'message with string payload and UTF-8 encoding' do
89
+ let(:message) { { data: string_ascii, encoding: 'utf-8' } }
90
+
91
+ it 'leaves the message data intact' do
92
+ expect(message[:data]).to eql(string_ascii)
93
+ end
94
+
95
+ it 'leaves the encoding intact' do
96
+ expect(message[:encoding]).to eql('utf-8')
97
+ end
98
+ end
99
+ end
100
+ end
@@ -1,5 +1,7 @@
1
1
  require 'spec_helper'
2
2
  require 'support/model_helper'
3
+ require 'base64'
4
+ require 'msgpack'
3
5
 
4
6
  describe Ably::Models::Message do
5
7
  include Ably::Modules::Conversions
@@ -8,7 +10,7 @@ describe Ably::Models::Message do
8
10
  let(:protocol_message_timestamp) { as_since_epoch(Time.now) }
9
11
  let(:protocol_message) { Ably::Models::ProtocolMessage.new(action: 1, timestamp: protocol_message_timestamp) }
10
12
 
11
- it_behaves_like 'a model', with_simple_attributes: %w(name client_id data) do
13
+ it_behaves_like 'a model', with_simple_attributes: %w(name client_id data encoding) do
12
14
  let(:model_args) { [protocol_message] }
13
15
  end
14
16
 
@@ -46,6 +48,19 @@ describe Ably::Models::Message do
46
48
  expect { model.to_json }.to raise_error RuntimeError, /cannot generate a valid Hash/
47
49
  end
48
50
  end
51
+
52
+ context 'with binary data' do
53
+ let(:data) { MessagePack.pack(SecureRandom.hex(32)) }
54
+ let(:model) { subject.new({ name: 'test', data: data }, protocol_message) }
55
+
56
+ it 'encodes as Base64 so that it can be converted to UTF-8 automatically by JSON#dump' do
57
+ expect(json_object["data"]).to eql(::Base64.encode64(data))
58
+ end
59
+
60
+ it 'adds Base64 encoding' do
61
+ expect(json_object["encoding"]).to eql('base64')
62
+ end
63
+ end
49
64
  end
50
65
 
51
66
  context 'from REST request with embedded fields' do
@@ -67,6 +67,52 @@ describe Ably::Models::PaginatedResource do
67
67
  end
68
68
  end
69
69
 
70
+ context 'with each block' do
71
+ let(:headers) do
72
+ {
73
+ 'link' => [
74
+ '<./history?index=1>; rel="next"'
75
+ ].join(', ')
76
+ }
77
+ end
78
+ let(:paged_client) do
79
+ instance_double('Ably::Rest::Client').tap do |client|
80
+ allow(client).to receive(:get).and_return(http_response_page2)
81
+ end
82
+ end
83
+ let(:body_page2) do
84
+ [
85
+ { id: 2 },
86
+ { id: 3 }
87
+ ]
88
+ end
89
+ let(:http_response_page2) do
90
+ instance_double('Faraday::Response', {
91
+ body: body_page2,
92
+ headers: headers
93
+ })
94
+ end
95
+
96
+ subject do
97
+ paginated_resource_class.new(http_response, full_url, paged_client, paginated_resource_options) do |resource|
98
+ resource[:added_attribute_from_block] = "id:#{resource[:id]}"
99
+ resource
100
+ end
101
+ end
102
+
103
+ it 'calls the block for each resource after retrieving the resources' do
104
+ expect(subject[0][:added_attribute_from_block]).to eql("id:#{body[0][:id]}")
105
+ end
106
+
107
+ it 'calls the block for each resource on second page after retrieving the resources' do
108
+ page_1_first_id = subject[0][:id]
109
+ next_page = subject.next_page
110
+
111
+ expect(next_page[0][:added_attribute_from_block]).to eql("id:#{body_page2[0][:id]}")
112
+ expect(next_page[0][:id]).to_not eql(page_1_first_id)
113
+ end
114
+ end
115
+
70
116
  context 'with non paged http response' do
71
117
  it 'is the first page' do
72
118
  expect(subject).to be_first_page
@@ -8,7 +8,7 @@ describe Ably::Models::PresenceMessage do
8
8
  let(:protocol_message_timestamp) { as_since_epoch(Time.now) }
9
9
  let(:protocol_message) { Ably::Models::ProtocolMessage.new(action: 1, timestamp: protocol_message_timestamp) }
10
10
 
11
- it_behaves_like 'a model', with_simple_attributes: %w(client_id member_id client_data) do
11
+ it_behaves_like 'a model', with_simple_attributes: %w(client_id member_id data encoding) do
12
12
  let(:model_args) { [protocol_message] }
13
13
  end
14
14
 
@@ -62,6 +62,19 @@ describe Ably::Models::PresenceMessage do
62
62
  expect { model.to_json }.to raise_error KeyError, /cannot generate a valid Hash/
63
63
  end
64
64
  end
65
+
66
+ context 'with binary data' do
67
+ let(:data) { MessagePack.pack(SecureRandom.hex(32)) }
68
+ let(:model) { subject.new({ action: 'enter', data: data }, protocol_message) }
69
+
70
+ it 'encodes as Base64 so that it can be converted to UTF-8 automatically by JSON#dump' do
71
+ expect(json_object["data"]).to eql(::Base64.encode64(data))
72
+ end
73
+
74
+ it 'adds Base64 encoding' do
75
+ expect(json_object["encoding"]).to eql('base64')
76
+ end
77
+ end
65
78
  end
66
79
 
67
80
  context 'from REST request with embedded fields' do
@@ -100,14 +113,14 @@ describe Ably::Models::PresenceMessage do
100
113
  let(:presence_0_json) do
101
114
  {
102
115
  client_id: 'zero',
103
- client_data: presence_0_payload
116
+ data: presence_0_payload
104
117
  }
105
118
  end
106
119
  let(:presence_1_payload) { SecureRandom.hex(8) }
107
120
  let(:presence_1_json) do
108
121
  {
109
122
  client_id: 'one',
110
- client_data: presence_1_payload
123
+ data: presence_1_payload
111
124
  }
112
125
  end
113
126
 
@@ -133,8 +146,8 @@ describe Ably::Models::PresenceMessage do
133
146
  end
134
147
 
135
148
  it 'should not modify the data payload' do
136
- expect(presence_0.client_data).to eql(presence_0_payload)
137
- expect(presence_1.client_data).to eql(presence_1_payload)
149
+ expect(presence_0.data).to eql(presence_0_payload)
150
+ expect(presence_1.data).to eql(presence_1_payload)
138
151
  end
139
152
  end
140
153
 
@@ -1,4 +1,4 @@
1
- require "spec_helper"
1
+ require 'spec_helper'
2
2
 
3
3
  describe Ably::Models::Token do
4
4
  subject { Ably::Models::Token }
@@ -95,19 +95,33 @@ describe Ably::Modules::EventEmitter do
95
95
  subject.trigger :message, msg
96
96
  end
97
97
 
98
- it 'deletes matching callbacks' do
99
- expect(obj).to_not receive(:received_message).with(msg)
100
- subject.off(:message, &callback)
101
- end
98
+ context 'with event names as arguments' do
99
+ it 'deletes matching callbacks' do
100
+ expect(obj).to_not receive(:received_message).with(msg)
101
+ subject.off(:message, &callback)
102
+ end
103
+
104
+ it 'deletes all callbacks if not block given' do
105
+ expect(obj).to_not receive(:received_message).with(msg)
106
+ subject.off(:message)
107
+ end
102
108
 
103
- it 'deletes all callbacks if not block given' do
104
- expect(obj).to_not receive(:received_message).with(msg)
105
- subject.off(:message)
109
+ it 'continues if the block does not exist' do
110
+ expect(obj).to receive(:received_message).with(msg)
111
+ subject.off(:message) { true }
112
+ end
106
113
  end
107
114
 
108
- it 'continues if the block does not exist' do
109
- expect(obj).to receive(:received_message).with(msg)
110
- subject.off(:message) { true }
115
+ context 'without any event names' do
116
+ it 'deletes all matching callbacks' do
117
+ expect(obj).to_not receive(:received_message).with(msg)
118
+ subject.off(&callback)
119
+ end
120
+
121
+ it 'deletes all callbacks if not block given' do
122
+ expect(obj).to_not receive(:received_message).with(msg)
123
+ subject.off
124
+ end
111
125
  end
112
126
  end
113
127
  end
@@ -1,8 +1,8 @@
1
1
  require 'spec_helper'
2
- require "support/protocol_msgbus_helper"
2
+ require 'support/protocol_msgbus_helper'
3
3
 
4
4
  describe Ably::Realtime::Channel do
5
- let(:client) { double('client').as_null_object }
5
+ let(:client) { double('client').as_null_object }
6
6
  let(:channel_name) { 'test' }
7
7
 
8
8
  subject do
@@ -48,7 +48,7 @@ describe Ably::Realtime::Channel do
48
48
  context 'subscriptions' do
49
49
  let(:message_history) { Hash.new { |hash, key| hash[key] = 0 } }
50
50
  let(:event_name) { 'click' }
51
- let(:message) { instance_double('Ably::Models::Message', name: event_name) }
51
+ let(:message) { instance_double('Ably::Models::Message', name: event_name, encode: nil, decode: nil) }
52
52
 
53
53
  context '#subscribe' do
54
54
  specify 'to all events' do
@@ -1,4 +1,4 @@
1
- require "spec_helper"
1
+ require 'spec_helper'
2
2
 
3
3
  describe Ably::Realtime::Channels do
4
4
  let(:connection) { instance_double('Ably::Realtime::Connection', on: true) }
@@ -1,5 +1,5 @@
1
1
  require 'spec_helper'
2
- require "support/protocol_msgbus_helper"
2
+ require 'support/protocol_msgbus_helper'
3
3
 
4
4
  describe Ably::Realtime::Client do
5
5
  let(:client_options) { 'appid.keyuid:keysecret' }
@@ -27,12 +27,54 @@ describe Ably::Realtime::Client do
27
27
  end
28
28
 
29
29
  context 'for attribute' do
30
- [:environment, :use_tls?, :logger, :log_level].each do |attribute|
30
+ [:environment, :use_tls?, :log_level].each do |attribute|
31
31
  specify "##{attribute}" do
32
32
  expect(subject.rest_client).to receive(attribute)
33
33
  subject.public_send attribute
34
34
  end
35
35
  end
36
36
  end
37
+
38
+ context 'logger' do
39
+ context 'defaults' do
40
+ let(:logger) { subject.logger }
41
+
42
+ subject { Ably::Realtime::Client.new(client_options) }
43
+
44
+ it 'uses default Ruby Logger by default' do
45
+ expect(subject.logger.logger).to be_a(::Logger)
46
+ end
47
+
48
+ it 'defaults to Logger::ERROR log level' do
49
+ expect(subject.logger.log_level).to eql(::Logger::ERROR)
50
+ end
51
+
52
+ it 'returns the connection ID' do
53
+ allow(subject).to receive_message_chain(:connection, :id).and_return('AAA')
54
+ expect(logger.logger.formatter.call(0, Time.now, '', 'unique_message')).to match(/AAA/)
55
+ end
56
+ end
57
+
58
+ context 'with custom logger and log_level' do
59
+ let(:custom_logger) do
60
+ Class.new do
61
+ extend Forwardable
62
+ def initialize
63
+ @logger = Logger.new(STDOUT)
64
+ end
65
+ def_delegators :@logger, :fatal, :error, :warn, :info, :debug, :level, :level=
66
+ end
67
+ end
68
+ subject { Ably::Realtime::Client.new(api_key: 'appid.keyuid:keysecret', logger: custom_logger.new, log_level: :debug) }
69
+
70
+ it 'uses the custom logger' do
71
+ expect(subject.logger.logger.class).to eql(custom_logger)
72
+ end
73
+
74
+ it 'sets the custom log level' do
75
+ expect(subject.logger.log_level).to eql(Logger::DEBUG)
76
+ end
77
+ end
78
+ end
37
79
  end
38
80
  end
@@ -10,8 +10,8 @@ describe Ably::Realtime::Connection do
10
10
  end
11
11
 
12
12
  before do
13
- expect(EventMachine::Timer).to receive(:new)
14
- expect(EventMachine).to receive(:next_tick)
13
+ expect(EventMachine::Timer).to receive(:new) # Connection Manager #initializer
14
+ expect(EventMachine).to receive(:next_tick) # non_blocking_loop_while for delivery of messages async
15
15
  end
16
16
 
17
17
  describe 'callbacks' do
@@ -5,7 +5,7 @@ describe Ably::Realtime::Client::IncomingMessageDispatcher do
5
5
  Ably::Util::PubSub.new
6
6
  end
7
7
  let(:connection) do
8
- instance_double('Ably::Realtime::Connection', __incoming_protocol_msgbus__: msgbus, update_connection_id: true)
8
+ instance_double('Ably::Realtime::Connection', __incoming_protocol_msgbus__: msgbus, update_connection_id: true, id: nil)
9
9
  end
10
10
  let(:client) do
11
11
  instance_double('Ably::Realtime::Client', channels: {})
@@ -15,7 +15,7 @@ describe Ably::Realtime::Client::IncomingMessageDispatcher do
15
15
 
16
16
  context '#initialize' do
17
17
  it 'should subscribe to protocol messages from the connection' do
18
- expect(msgbus).to receive(:subscribe).with(:message).and_call_original
18
+ expect(msgbus).to receive(:subscribe).with(:protocol_message).and_call_original
19
19
  subject
20
20
  end
21
21
  end
@@ -24,13 +24,13 @@ describe Ably::Realtime::Client::IncomingMessageDispatcher do
24
24
  before { subject }
25
25
 
26
26
  it 'should raise an exception if a message is sent that is not a ProtocolMessage' do
27
- expect { msgbus.publish :message, nil }.to raise_error ArgumentError
27
+ expect { msgbus.publish :protocol_message, nil }.to raise_error ArgumentError
28
28
  end
29
29
 
30
30
  it 'should warn if a message is received for a non-existent channel' do
31
31
  allow(subject).to receive_message_chain(:logger, :debug)
32
32
  expect(subject).to receive_message_chain(:logger, :warn)
33
- msgbus.publish :message, Ably::Models::ProtocolMessage.new(:action => :attached, channel: 'unknown')
33
+ msgbus.publish :protocol_message, Ably::Models::ProtocolMessage.new(:action => :attached, channel: 'unknown')
34
34
  end
35
35
  end
36
36
  end
@@ -1,5 +1,5 @@
1
1
  require 'spec_helper'
2
- require "support/protocol_msgbus_helper"
2
+ require 'support/protocol_msgbus_helper'
3
3
 
4
4
  describe Ably::Realtime::Presence do
5
5
  let(:channel) { double('Ably::Realtime::Channel').as_null_object }
@@ -1,4 +1,4 @@
1
- require "spec_helper"
1
+ require 'spec_helper'
2
2
 
3
3
  describe Ably::Realtime do
4
4
  let(:options) { { api_key: 'app.key:secret' } }
@@ -8,8 +8,8 @@ describe Ably::Realtime do
8
8
  end
9
9
 
10
10
  describe Ably::Realtime::Client do
11
- describe "initializing the client" do
12
- it "should disallow an invalid key" do
11
+ describe 'initializing the client' do
12
+ it 'should disallow an invalid key' do
13
13
  expect { Ably::Realtime::Client.new({}) }.to raise_error(ArgumentError, /api_key is missing/)
14
14
  expect { Ably::Realtime::Client.new(api_key: 'invalid') }.to raise_error(ArgumentError, /api_key is invalid/)
15
15
  expect { Ably::Realtime::Client.new(api_key: 'invalid:asdad') }.to raise_error(ArgumentError, /api_key is invalid/)