go-mqtt 0.0.1
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.
- checksums.yaml +7 -0
- data/LICENSE.md +21 -0
- data/NEWS.md +150 -0
- data/README.md +237 -0
- data/lib/mqtt/client.rb +755 -0
- data/lib/mqtt/openssl_fix.rb +29 -0
- data/lib/mqtt/packet.rb +1049 -0
- data/lib/mqtt/proxy.rb +115 -0
- data/lib/mqtt/sn/packet.rb +720 -0
- data/lib/mqtt/version.rb +4 -0
- data/lib/mqtt.rb +48 -0
- data/spec/mqtt_client_spec.rb +1124 -0
- data/spec/mqtt_packet_spec.rb +2012 -0
- data/spec/mqtt_proxy_spec.rb +8 -0
- data/spec/mqtt_sn_packet_spec.rb +1721 -0
- data/spec/mqtt_version_spec.rb +23 -0
- data/spec/zz_client_integration_spec.rb +177 -0
- metadata +152 -0
@@ -0,0 +1,1124 @@
|
|
1
|
+
# encoding: BINARY
|
2
|
+
# Encoding is set to binary, so that the binary packets aren't validated as UTF-8
|
3
|
+
|
4
|
+
$:.unshift(File.dirname(__FILE__))
|
5
|
+
|
6
|
+
require 'spec_helper'
|
7
|
+
require 'mqtt'
|
8
|
+
|
9
|
+
describe MQTT::Client do
|
10
|
+
|
11
|
+
before(:each) do
|
12
|
+
# Reset environment variable
|
13
|
+
ENV.delete('MQTT_SERVER')
|
14
|
+
end
|
15
|
+
|
16
|
+
let(:client) { MQTT::Client.new(:host => 'localhost') }
|
17
|
+
let(:socket) do
|
18
|
+
socket = StringIO.new
|
19
|
+
if socket.respond_to?(:set_encoding)
|
20
|
+
socket.set_encoding("binary")
|
21
|
+
else
|
22
|
+
socket
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
if Process.const_defined? :CLOCK_MONOTONIC
|
27
|
+
def now
|
28
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
29
|
+
end
|
30
|
+
else
|
31
|
+
# Support older Ruby
|
32
|
+
def now
|
33
|
+
Time.now.to_f
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
describe "initializing a client" do
|
38
|
+
it "with no arguments, it should use the defaults" do
|
39
|
+
client = MQTT::Client.new
|
40
|
+
expect(client.host).to eq(nil)
|
41
|
+
expect(client.port).to eq(1883)
|
42
|
+
expect(client.version).to eq('3.1.1')
|
43
|
+
expect(client.keep_alive).to eq(15)
|
44
|
+
end
|
45
|
+
|
46
|
+
it "with a single string argument, it should use it has the host" do
|
47
|
+
client = MQTT::Client.new('otherhost.mqtt.org')
|
48
|
+
expect(client.host).to eq('otherhost.mqtt.org')
|
49
|
+
expect(client.port).to eq(1883)
|
50
|
+
expect(client.keep_alive).to eq(15)
|
51
|
+
end
|
52
|
+
|
53
|
+
it "with two arguments, it should use it as the host and port" do
|
54
|
+
client = MQTT::Client.new('otherhost.mqtt.org', 1000)
|
55
|
+
expect(client.host).to eq('otherhost.mqtt.org')
|
56
|
+
expect(client.port).to eq(1000)
|
57
|
+
expect(client.keep_alive).to eq(15)
|
58
|
+
end
|
59
|
+
|
60
|
+
it "with names arguments, it should use those as arguments" do
|
61
|
+
client = MQTT::Client.new(:host => 'otherhost.mqtt.org', :port => 1000)
|
62
|
+
expect(client.host).to eq('otherhost.mqtt.org')
|
63
|
+
expect(client.port).to eq(1000)
|
64
|
+
expect(client.keep_alive).to eq(15)
|
65
|
+
end
|
66
|
+
|
67
|
+
it "with a hash, it should use those as arguments" do
|
68
|
+
client = MQTT::Client.new({:host => 'otherhost.mqtt.org', :port => 1000})
|
69
|
+
expect(client.host).to eq('otherhost.mqtt.org')
|
70
|
+
expect(client.port).to eq(1000)
|
71
|
+
expect(client.keep_alive).to eq(15)
|
72
|
+
end
|
73
|
+
|
74
|
+
it "with a hash containing just a keep alive setting" do
|
75
|
+
client = MQTT::Client.new(:host => 'localhost', :keep_alive => 60)
|
76
|
+
expect(client.host).to eq('localhost')
|
77
|
+
expect(client.port).to eq(1883)
|
78
|
+
expect(client.keep_alive).to eq(60)
|
79
|
+
end
|
80
|
+
|
81
|
+
it "with a combination of a host name and a hash of settings" do
|
82
|
+
client = MQTT::Client.new('localhost', :keep_alive => 65)
|
83
|
+
expect(client.host).to eq('localhost')
|
84
|
+
expect(client.port).to eq(1883)
|
85
|
+
expect(client.keep_alive).to eq(65)
|
86
|
+
end
|
87
|
+
|
88
|
+
it "with a combination of a host name, port and a hash of settings" do
|
89
|
+
client = MQTT::Client.new('localhost', 1888, :keep_alive => 65)
|
90
|
+
expect(client.host).to eq('localhost')
|
91
|
+
expect(client.port).to eq(1888)
|
92
|
+
expect(client.keep_alive).to eq(65)
|
93
|
+
end
|
94
|
+
|
95
|
+
it "with a mqtt:// URI containing just a hostname" do
|
96
|
+
client = MQTT::Client.new(URI.parse('mqtt://mqtt.example.com'))
|
97
|
+
expect(client.host).to eq('mqtt.example.com')
|
98
|
+
expect(client.port).to eq(1883)
|
99
|
+
expect(client.ssl).to be_falsey
|
100
|
+
end
|
101
|
+
|
102
|
+
it "with a mqtts:// URI containing just a hostname" do
|
103
|
+
client = MQTT::Client.new(URI.parse('mqtts://mqtt.example.com'))
|
104
|
+
expect(client.host).to eq('mqtt.example.com')
|
105
|
+
expect(client.port).to eq(8883)
|
106
|
+
expect(client.ssl).to be_truthy
|
107
|
+
end
|
108
|
+
|
109
|
+
it "with a mqtt:// URI containing a custom port number" do
|
110
|
+
client = MQTT::Client.new(URI.parse('mqtt://mqtt.example.com:1234/'))
|
111
|
+
expect(client.host).to eq('mqtt.example.com')
|
112
|
+
expect(client.port).to eq(1234)
|
113
|
+
expect(client.ssl).to be_falsey
|
114
|
+
end
|
115
|
+
|
116
|
+
it "with a mqtts:// URI containing a custom port number" do
|
117
|
+
client = MQTT::Client.new(URI.parse('mqtts://mqtt.example.com:1234/'))
|
118
|
+
expect(client.host).to eq('mqtt.example.com')
|
119
|
+
expect(client.port).to eq(1234)
|
120
|
+
expect(client.ssl).to be_truthy
|
121
|
+
end
|
122
|
+
|
123
|
+
it "with a URI containing a username and password" do
|
124
|
+
client = MQTT::Client.new(URI.parse('mqtt://auser:bpass@mqtt.example.com'))
|
125
|
+
expect(client.host).to eq('mqtt.example.com')
|
126
|
+
expect(client.port).to eq(1883)
|
127
|
+
expect(client.username).to eq('auser')
|
128
|
+
expect(client.password).to eq('bpass')
|
129
|
+
end
|
130
|
+
|
131
|
+
it "with a URI containing an escaped username and password" do
|
132
|
+
client = MQTT::Client.new(URI.parse('mqtt://foo%20bar:%40123%2B%25@mqtt.example.com'))
|
133
|
+
expect(client.host).to eq('mqtt.example.com')
|
134
|
+
expect(client.port).to eq(1883)
|
135
|
+
expect(client.username).to eq('foo bar')
|
136
|
+
expect(client.password).to eq('@123+%')
|
137
|
+
end
|
138
|
+
|
139
|
+
it "with a URI containing a double escaped username and password" do
|
140
|
+
client = MQTT::Client.new(URI.parse('mqtt://foo%2520bar:123%2525@mqtt.example.com'))
|
141
|
+
expect(client.host).to eq('mqtt.example.com')
|
142
|
+
expect(client.port).to eq(1883)
|
143
|
+
expect(client.username).to eq('foo%20bar')
|
144
|
+
expect(client.password).to eq('123%25')
|
145
|
+
end
|
146
|
+
|
147
|
+
it "with a URI as a string" do
|
148
|
+
client = MQTT::Client.new('mqtt://mqtt.example.com')
|
149
|
+
expect(client.host).to eq('mqtt.example.com')
|
150
|
+
expect(client.port).to eq(1883)
|
151
|
+
end
|
152
|
+
|
153
|
+
it "with a URI as a string including port" do
|
154
|
+
client = MQTT::Client.new('mqtt://user:pass@m10.cloudmqtt.com:13858', nil)
|
155
|
+
expect(client.host).to eq('m10.cloudmqtt.com')
|
156
|
+
expect(client.port).to eq(13858)
|
157
|
+
end
|
158
|
+
|
159
|
+
it "with a URI and a hash of settings" do
|
160
|
+
client = MQTT::Client.new('mqtt://mqtt.example.com', :keep_alive => 65)
|
161
|
+
expect(client.host).to eq('mqtt.example.com')
|
162
|
+
expect(client.port).to eq(1883)
|
163
|
+
expect(client.keep_alive).to eq(65)
|
164
|
+
end
|
165
|
+
|
166
|
+
it "with no arguments uses the MQTT_SERVER environment variable as connect URI" do
|
167
|
+
ENV['MQTT_SERVER'] = 'mqtt://mqtt.example.com:1234'
|
168
|
+
client = MQTT::Client.new
|
169
|
+
expect(client.host).to eq('mqtt.example.com')
|
170
|
+
expect(client.port).to eq(1234)
|
171
|
+
end
|
172
|
+
|
173
|
+
it "with an unsupported URI scheme" do
|
174
|
+
expect {
|
175
|
+
client = MQTT::Client.new(URI.parse('http://mqtt.example.com/'))
|
176
|
+
}.to raise_error(
|
177
|
+
'Only the mqtt:// and mqtts:// schemes are supported'
|
178
|
+
)
|
179
|
+
end
|
180
|
+
|
181
|
+
it "with three arguments" do
|
182
|
+
expect {
|
183
|
+
client = MQTT::Client.new(1, 2, 3)
|
184
|
+
}.to raise_error(
|
185
|
+
'Unsupported number of arguments'
|
186
|
+
)
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
describe "setting a client certificate file path" do
|
191
|
+
it "should add a certificate to the SSL context" do
|
192
|
+
expect(client.ssl_context.cert).to be_nil
|
193
|
+
client.cert_file = fixture_path('client.pem')
|
194
|
+
expect(client.ssl_context.cert).to be_a(OpenSSL::X509::Certificate)
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
describe "setting a client certificate directly" do
|
199
|
+
it "should add a certificate to the SSL context" do
|
200
|
+
expect(client.ssl_context.cert).to be_nil
|
201
|
+
client.cert = File.read(fixture_path('client.pem'))
|
202
|
+
expect(client.ssl_context.cert).to be_a(OpenSSL::X509::Certificate)
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
describe "setting a client private key file path" do
|
207
|
+
it "should add a certificate to the SSL context" do
|
208
|
+
expect(client.ssl_context.key).to be_nil
|
209
|
+
client.key_file = fixture_path('client.key')
|
210
|
+
expect(client.ssl_context.key).to be_a(OpenSSL::PKey::RSA)
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
describe "setting a client private key directly" do
|
215
|
+
it "should add a certificate to the SSL context" do
|
216
|
+
expect(client.ssl_context.key).to be_nil
|
217
|
+
client.key = File.read(fixture_path('client.key'))
|
218
|
+
expect(client.ssl_context.key).to be_a(OpenSSL::PKey::RSA)
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
describe "setting an encrypted client private key, w/the correct passphrase" do
|
223
|
+
let(:key_pass) { 'mqtt' }
|
224
|
+
|
225
|
+
it "should add the decrypted certificate to the SSL context" do
|
226
|
+
expect(client.ssl_context.key).to be_nil
|
227
|
+
client.key_file = [fixture_path('client.pass.key'), key_pass]
|
228
|
+
expect(client.ssl_context.key).to be_a(OpenSSL::PKey::RSA)
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
describe "setting an encrypted client private key, w/an incorrect passphrase" do
|
233
|
+
let(:key_pass) { 'ttqm' }
|
234
|
+
|
235
|
+
it "should raise an exception" do
|
236
|
+
expect(client.ssl_context.key).to be_nil
|
237
|
+
expect { client.key_file = [fixture_path('client.pass.key'), key_pass] }.to(
|
238
|
+
raise_error(/Could not parse PKey/))
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
describe "setting a client private EC key file path" do
|
243
|
+
it "should add a certificate to the SSL context" do
|
244
|
+
expect(client.ssl_context.key).to be_nil
|
245
|
+
client.key_file = fixture_path('ec.key')
|
246
|
+
expect(client.ssl_context.key).to be_a(OpenSSL::PKey::EC)
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
describe "setting a client private EC key directly" do
|
251
|
+
it "should add a certificate to the SSL context" do
|
252
|
+
expect(client.ssl_context.key).to be_nil
|
253
|
+
client.key = File.read(fixture_path('ec.key'))
|
254
|
+
expect(client.ssl_context.key).to be_a(OpenSSL::PKey::EC)
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
describe "setting an encrypted client private EC key, w/the correct passphrase" do
|
259
|
+
let(:key_pass) { 'mqtt' }
|
260
|
+
|
261
|
+
it "should add the decrypted certificate to the SSL context" do
|
262
|
+
expect(client.ssl_context.key).to be_nil
|
263
|
+
client.key_file = [fixture_path('ec.pass.key'), key_pass]
|
264
|
+
expect(client.ssl_context.key).to be_a(OpenSSL::PKey::EC)
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
describe "setting an encrypted client private EC key, w/an incorrect passphrase" do
|
269
|
+
let(:key_pass) { 'ttqm' }
|
270
|
+
|
271
|
+
it "should raise an exception" do
|
272
|
+
expect(client.ssl_context.key).to be_nil
|
273
|
+
expect { client.key_file = [fixture_path('ec.pass.key'), key_pass] }.to(
|
274
|
+
raise_error(/Could not parse PKey/))
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
describe "setting a Certificate Authority file path" do
|
279
|
+
it "should add a CA file path to the SSL context" do
|
280
|
+
expect(client.ssl_context.ca_file).to be_nil
|
281
|
+
client.ca_file = fixture_path('root-ca.pem')
|
282
|
+
expect(client.ssl_context.ca_file).to eq(fixture_path('root-ca.pem'))
|
283
|
+
end
|
284
|
+
|
285
|
+
it "should enable peer verification" do
|
286
|
+
client.ca_file = fixture_path('root-ca.pem')
|
287
|
+
expect(client.ssl_context.verify_mode).to eq(OpenSSL::SSL::VERIFY_PEER)
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
describe "deprecated attributes" do
|
292
|
+
it "should allow getting and setting the host name using the remote_host method" do
|
293
|
+
client.remote_host = 'remote-host.example.com'
|
294
|
+
expect(client.host).to eq('remote-host.example.com')
|
295
|
+
expect(client.remote_host).to eq('remote-host.example.com')
|
296
|
+
client.host = 'foo.example.org'
|
297
|
+
expect(client.host).to eq('foo.example.org')
|
298
|
+
expect(client.remote_host).to eq('foo.example.org')
|
299
|
+
end
|
300
|
+
|
301
|
+
it "should allow getting and setting the port using the remote_port method" do
|
302
|
+
client.remote_port = 9999
|
303
|
+
expect(client.port).to eq(9999)
|
304
|
+
expect(client.remote_port).to eq(9999)
|
305
|
+
client.port = 1234
|
306
|
+
expect(client.port).to eq(1234)
|
307
|
+
expect(client.remote_port).to eq(1234)
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
311
|
+
describe "when calling the 'connect' method on a client" do
|
312
|
+
before(:each) do
|
313
|
+
allow(TCPSocket).to receive(:new).and_return(socket)
|
314
|
+
allow(Thread).to receive(:new)
|
315
|
+
allow(client).to receive(:receive_connack)
|
316
|
+
end
|
317
|
+
|
318
|
+
it "should create a TCP Socket if not connected" do
|
319
|
+
expect(TCPSocket).to receive(:new).once.and_return(socket)
|
320
|
+
client.connect('myclient')
|
321
|
+
end
|
322
|
+
|
323
|
+
it "should not create a new TCP Socket if connected" do
|
324
|
+
allow(client).to receive(:connected?).and_return(true)
|
325
|
+
expect(TCPSocket).to receive(:new).never
|
326
|
+
client.connect('myclient')
|
327
|
+
end
|
328
|
+
|
329
|
+
it "should start the reader thread if not connected" do
|
330
|
+
expect(Thread).to receive(:new).once
|
331
|
+
client.connect('myclient')
|
332
|
+
end
|
333
|
+
|
334
|
+
context "protocol version 3.1.0" do
|
335
|
+
it "should write a valid CONNECT packet to the socket if not connected" do
|
336
|
+
client.version = '3.1.0'
|
337
|
+
client.connect('myclient')
|
338
|
+
expect(socket.string).to eq("\020\026\x00\x06MQIsdp\x03\x02\x00\x0f\x00\x08myclient")
|
339
|
+
end
|
340
|
+
end
|
341
|
+
|
342
|
+
context "protocol version 3.1.1" do
|
343
|
+
it "should write a valid CONNECT packet to the socket if not connected" do
|
344
|
+
client.version = '3.1.1'
|
345
|
+
client.connect('myclient')
|
346
|
+
expect(socket.string).to eq("\020\024\x00\x04MQTT\x04\x02\x00\x0f\x00\x08myclient")
|
347
|
+
end
|
348
|
+
end
|
349
|
+
|
350
|
+
it "should try and read an acknowledgement packet to the socket if not connected" do
|
351
|
+
expect(client).to receive(:receive_connack).once
|
352
|
+
client.connect('myclient')
|
353
|
+
end
|
354
|
+
|
355
|
+
it "should raise an exception if no host is configured" do
|
356
|
+
expect {
|
357
|
+
client = MQTT::Client.new
|
358
|
+
client.connect
|
359
|
+
}.to raise_error(
|
360
|
+
'No MQTT server host set when attempting to connect'
|
361
|
+
)
|
362
|
+
end
|
363
|
+
|
364
|
+
context "if a block is given" do
|
365
|
+
it "should disconnect after connecting" do
|
366
|
+
expect(client).to receive(:disconnect).once
|
367
|
+
client.connect('myclient') { nil }
|
368
|
+
end
|
369
|
+
|
370
|
+
it "should disconnect even if the block raises an exception" do
|
371
|
+
expect(client).to receive(:disconnect).once
|
372
|
+
begin
|
373
|
+
client.connect('myclient') { raise StandardError }
|
374
|
+
rescue StandardError
|
375
|
+
end
|
376
|
+
end
|
377
|
+
end
|
378
|
+
|
379
|
+
it "should not disconnect after connecting, if no block is given" do
|
380
|
+
expect(client).to receive(:disconnect).never
|
381
|
+
client.connect('myclient')
|
382
|
+
end
|
383
|
+
|
384
|
+
it "should include the username and password for an authenticated connection" do
|
385
|
+
client.username = 'username'
|
386
|
+
client.password = 'password'
|
387
|
+
client.connect('myclient')
|
388
|
+
expect(socket.string).to eq(
|
389
|
+
"\x10\x28"+
|
390
|
+
"\x00\x04MQTT"+
|
391
|
+
"\x04\xC2\x00\x0f"+
|
392
|
+
"\x00\x08myclient"+
|
393
|
+
"\x00\x08username"+
|
394
|
+
"\x00\x08password"
|
395
|
+
)
|
396
|
+
end
|
397
|
+
|
398
|
+
context "no client id is given" do
|
399
|
+
it "should raise an exception if the clean session flag is false" do
|
400
|
+
expect {
|
401
|
+
client.client_id = nil
|
402
|
+
client.clean_session = false
|
403
|
+
client.connect
|
404
|
+
}.to raise_error(
|
405
|
+
'Must provide a client_id if clean_session is set to false'
|
406
|
+
)
|
407
|
+
end
|
408
|
+
|
409
|
+
context "protocol version 3.1.0" do
|
410
|
+
it "should generate a client if the clean session flag is true" do
|
411
|
+
client.version = '3.1.0'
|
412
|
+
client.client_id = nil
|
413
|
+
client.clean_session = true
|
414
|
+
client.connect
|
415
|
+
expect(client.client_id).to match(/^\w+$/)
|
416
|
+
end
|
417
|
+
end
|
418
|
+
|
419
|
+
context "protocol version 3.1.1" do
|
420
|
+
it "should send empty client if the clean session flag is true" do
|
421
|
+
client.version = '3.1.1'
|
422
|
+
client.client_id = nil
|
423
|
+
client.clean_session = true
|
424
|
+
client.connect
|
425
|
+
expect(client.client_id).to be_nil
|
426
|
+
expect(socket.string).to eq("\020\014\x00\x04MQTT\x04\x02\x00\x0f\x00\x00")
|
427
|
+
end
|
428
|
+
end
|
429
|
+
end
|
430
|
+
|
431
|
+
context "and using ssl" do
|
432
|
+
let(:ssl_socket) {
|
433
|
+
double(
|
434
|
+
"SSLSocket",
|
435
|
+
:sync_close= => true,
|
436
|
+
:write => true,
|
437
|
+
:connect => true,
|
438
|
+
:closed? => false
|
439
|
+
)
|
440
|
+
}
|
441
|
+
|
442
|
+
it "should use ssl if it enabled using the :ssl => true parameter" do
|
443
|
+
expect(OpenSSL::SSL::SSLSocket).to receive(:new).and_return(ssl_socket)
|
444
|
+
expect(ssl_socket).to receive(:connect)
|
445
|
+
expect(ssl_socket).to receive(:post_connection_check).with('mqtt.example.com')
|
446
|
+
|
447
|
+
client = MQTT::Client.new('mqtt.example.com', :ssl => true)
|
448
|
+
allow(client).to receive(:receive_connack)
|
449
|
+
client.connect
|
450
|
+
end
|
451
|
+
|
452
|
+
it "should use ssl if it enabled using the mqtts:// scheme" do
|
453
|
+
expect(OpenSSL::SSL::SSLSocket).to receive(:new).and_return(ssl_socket)
|
454
|
+
expect(ssl_socket).to receive(:connect)
|
455
|
+
expect(ssl_socket).to receive(:post_connection_check).with('mqtt.example.com')
|
456
|
+
|
457
|
+
client = MQTT::Client.new('mqtts://mqtt.example.com')
|
458
|
+
allow(client).to receive(:receive_connack)
|
459
|
+
client.connect
|
460
|
+
end
|
461
|
+
|
462
|
+
it "should use set the SSL version, if the :ssl parameter is a symbol" do
|
463
|
+
expect(OpenSSL::SSL::SSLSocket).to receive(:new).and_return(ssl_socket)
|
464
|
+
expect(ssl_socket).to receive(:connect)
|
465
|
+
expect(ssl_socket).to receive(:post_connection_check).with('mqtt.example.com')
|
466
|
+
|
467
|
+
client = MQTT::Client.new('mqtt.example.com', :ssl => :TLSv1)
|
468
|
+
expect(client.ssl_context).to receive('ssl_version=').with(:TLSv1)
|
469
|
+
allow(client).to receive(:receive_connack)
|
470
|
+
client.connect
|
471
|
+
end
|
472
|
+
|
473
|
+
it "should use set hostname on the SSL socket for SNI" do
|
474
|
+
expect(OpenSSL::SSL::SSLSocket).to receive(:new).and_return(ssl_socket)
|
475
|
+
expect(ssl_socket).to receive(:hostname=).with('mqtt.example.com')
|
476
|
+
expect(ssl_socket).to receive(:post_connection_check).with('mqtt.example.com')
|
477
|
+
|
478
|
+
client = MQTT::Client.new('mqtts://mqtt.example.com')
|
479
|
+
allow(client).to receive(:receive_connack)
|
480
|
+
client.connect
|
481
|
+
end
|
482
|
+
|
483
|
+
it "should skip host verification" do
|
484
|
+
expect(OpenSSL::SSL::SSLSocket).to receive(:new).and_return(ssl_socket)
|
485
|
+
expect(ssl_socket).to receive(:connect)
|
486
|
+
|
487
|
+
client = MQTT::Client.new('mqtt.example.com', :ssl => true, :verify_host => false)
|
488
|
+
allow(client).to receive(:receive_connack)
|
489
|
+
client.connect
|
490
|
+
end
|
491
|
+
end
|
492
|
+
|
493
|
+
context "with a last will and testament set" do
|
494
|
+
before(:each) do
|
495
|
+
client.set_will('topic', 'hello', retain=false, qos=1)
|
496
|
+
end
|
497
|
+
|
498
|
+
it "should have set the Will's topic" do
|
499
|
+
expect(client.will_topic).to eq('topic')
|
500
|
+
end
|
501
|
+
|
502
|
+
it "should have set the Will's payload" do
|
503
|
+
expect(client.will_payload).to eq('hello')
|
504
|
+
end
|
505
|
+
|
506
|
+
it "should have set the Will's retain flag to true" do
|
507
|
+
expect(client.will_retain).to be_falsey
|
508
|
+
end
|
509
|
+
|
510
|
+
it "should have set the Will's retain QoS value to 1" do
|
511
|
+
expect(client.will_qos).to eq(1)
|
512
|
+
end
|
513
|
+
|
514
|
+
it "should include the will in the CONNECT message" do
|
515
|
+
client.connect('myclient')
|
516
|
+
expect(socket.string).to eq(
|
517
|
+
"\x10\x22"+
|
518
|
+
"\x00\x04MQTT"+
|
519
|
+
"\x04\x0e\x00\x0f"+
|
520
|
+
"\x00\x08myclient"+
|
521
|
+
"\x00\x05topic\x00\x05hello"
|
522
|
+
)
|
523
|
+
end
|
524
|
+
end
|
525
|
+
|
526
|
+
end
|
527
|
+
|
528
|
+
describe "calling 'connect' on the class" do
|
529
|
+
it "should create a new client object" do
|
530
|
+
client = double("MQTT::Client")
|
531
|
+
allow(client).to receive(:connect)
|
532
|
+
expect(MQTT::Client).to receive(:new).once.and_return(client)
|
533
|
+
MQTT::Client.connect
|
534
|
+
end
|
535
|
+
|
536
|
+
it "should call connect new client object" do
|
537
|
+
client = double("MQTT::Client")
|
538
|
+
expect(client).to receive(:connect)
|
539
|
+
allow(MQTT::Client).to receive(:new).once.and_return(client)
|
540
|
+
MQTT::Client.connect
|
541
|
+
end
|
542
|
+
|
543
|
+
it "should return the new client object" do
|
544
|
+
client = double("MQTT::Client")
|
545
|
+
allow(client).to receive(:connect)
|
546
|
+
allow(MQTT::Client).to receive(:new).once.and_return(client)
|
547
|
+
expect(MQTT::Client.connect).to eq(client)
|
548
|
+
end
|
549
|
+
end
|
550
|
+
|
551
|
+
describe "when calling the 'receive_connack' method" do
|
552
|
+
before(:each) do
|
553
|
+
client.instance_variable_set('@socket', socket)
|
554
|
+
allow(IO).to receive(:select).and_return([[socket], [], []])
|
555
|
+
end
|
556
|
+
|
557
|
+
it "should not raise an exception for a successful CONNACK packet" do
|
558
|
+
socket.write("\x20\x02\x00\x00")
|
559
|
+
socket.rewind
|
560
|
+
expect { client.send(:receive_connack) }.not_to raise_error
|
561
|
+
expect(socket).not_to be_closed
|
562
|
+
end
|
563
|
+
|
564
|
+
it "should raise an exception if the packet type isn't CONNACK" do
|
565
|
+
socket.write("\xD0\x00")
|
566
|
+
socket.rewind
|
567
|
+
expect { client.send(:receive_connack) }.to raise_error(MQTT::ProtocolException)
|
568
|
+
end
|
569
|
+
|
570
|
+
it "should raise an exception if the CONNACK packet return code is 'unacceptable protocol version'" do
|
571
|
+
socket.write("\x20\x02\x00\x01")
|
572
|
+
socket.rewind
|
573
|
+
expect { client.send(:receive_connack) }.to raise_error(MQTT::ProtocolException, /unacceptable protocol version/i)
|
574
|
+
end
|
575
|
+
|
576
|
+
it "should raise an exception if the CONNACK packet return code is 'client identifier rejected'" do
|
577
|
+
socket.write("\x20\x02\x00\x02")
|
578
|
+
socket.rewind
|
579
|
+
expect { client.send(:receive_connack) }.to raise_error(MQTT::ProtocolException, /client identifier rejected/i)
|
580
|
+
end
|
581
|
+
|
582
|
+
it "should raise an exception if the CONNACK packet return code is 'server unavailable'" do
|
583
|
+
socket.write("\x20\x02\x00\x03")
|
584
|
+
socket.rewind
|
585
|
+
expect { client.send(:receive_connack) }.to raise_error(MQTT::ProtocolException, /server unavailable/i)
|
586
|
+
end
|
587
|
+
|
588
|
+
it "should raise an exception if the CONNACK packet return code is an unknown" do
|
589
|
+
socket.write("\x20\x02\x00\xAA")
|
590
|
+
socket.rewind
|
591
|
+
expect { client.send(:receive_connack) }.to raise_error(MQTT::ProtocolException, /connection refused/i)
|
592
|
+
end
|
593
|
+
|
594
|
+
it "should close the socket for an unsuccessful CONNACK packet" do
|
595
|
+
socket.write("\x20\x02\x00\x05")
|
596
|
+
socket.rewind
|
597
|
+
expect { client.send(:receive_connack) }.to raise_error(MQTT::ProtocolException, /not authorised/i)
|
598
|
+
expect(socket).to be_closed
|
599
|
+
end
|
600
|
+
end
|
601
|
+
|
602
|
+
describe "when calling the 'disconnect' method" do
|
603
|
+
before(:each) do
|
604
|
+
thread = double('Read Thread', :alive? => true, :kill => true)
|
605
|
+
client.instance_variable_set('@socket', socket)
|
606
|
+
client.instance_variable_set('@read_thread', thread)
|
607
|
+
end
|
608
|
+
|
609
|
+
it "should not do anything if the socket is already disconnected" do
|
610
|
+
allow(client).to receive(:connected?).and_return(false)
|
611
|
+
client.disconnect(true)
|
612
|
+
expect(socket.string).to eq("")
|
613
|
+
end
|
614
|
+
|
615
|
+
it "should write a valid DISCONNECT packet to the socket if connected and the send_msg=true an" do
|
616
|
+
allow(client).to receive(:connected?).and_return(true)
|
617
|
+
client.disconnect(true)
|
618
|
+
expect(socket.string).to eq("\xE0\x00")
|
619
|
+
end
|
620
|
+
|
621
|
+
it "should not write anything to the socket if the send_msg=false" do
|
622
|
+
allow(client).to receive(:connected?).and_return(true)
|
623
|
+
client.disconnect(false)
|
624
|
+
expect(socket.string).to be_empty
|
625
|
+
end
|
626
|
+
|
627
|
+
it "should call the close method on the socket" do
|
628
|
+
expect(socket).to receive(:close)
|
629
|
+
client.disconnect
|
630
|
+
end
|
631
|
+
end
|
632
|
+
|
633
|
+
describe "when calling the 'publish' method" do
|
634
|
+
class ClientWithPubackInjection < MQTT::Client
|
635
|
+
def initialize
|
636
|
+
super(:host => 'localhost')
|
637
|
+
@injected_pubacks = {}
|
638
|
+
end
|
639
|
+
|
640
|
+
def inject_puback(packet)
|
641
|
+
@injected_pubacks[packet.id] = packet
|
642
|
+
end
|
643
|
+
|
644
|
+
def wait_for_puback(id)
|
645
|
+
packet = @injected_pubacks.fetch(id) {
|
646
|
+
return super
|
647
|
+
}
|
648
|
+
Queue.new << packet
|
649
|
+
end
|
650
|
+
end
|
651
|
+
|
652
|
+
let(:client) { ClientWithPubackInjection.new }
|
653
|
+
|
654
|
+
before(:each) do
|
655
|
+
client.instance_variable_set('@socket', socket)
|
656
|
+
end
|
657
|
+
|
658
|
+
it "should respect timeouts" do
|
659
|
+
require "socket"
|
660
|
+
server = TCPServer.new('127.0.0.1', 0)
|
661
|
+
port = server.addr[1]
|
662
|
+
client = MQTT::Client.new(:host => '127.0.0.1', :port => port, :ack_timeout => 1.0)
|
663
|
+
|
664
|
+
# Accept the connection in a separate thread
|
665
|
+
server_thread = Thread.new do
|
666
|
+
socket = server.accept
|
667
|
+
# Send a valid CONNACK packet
|
668
|
+
socket.write("\x20\x02\x00\x00")
|
669
|
+
# Keep socket open until signaled
|
670
|
+
Thread.stop
|
671
|
+
socket.close
|
672
|
+
end
|
673
|
+
|
674
|
+
client.connect
|
675
|
+
|
676
|
+
t = Thread.new {
|
677
|
+
Thread.current[:parent] = Thread.main
|
678
|
+
loop do
|
679
|
+
client.send :receive_packet
|
680
|
+
end
|
681
|
+
}
|
682
|
+
|
683
|
+
start = now
|
684
|
+
expect(client.publish('topic','payload', false, 1)).to be false
|
685
|
+
elapsed = now - start
|
686
|
+
t.kill
|
687
|
+
client.disconnect
|
688
|
+
server_thread.run # Signal server thread to close socket
|
689
|
+
server_thread.join
|
690
|
+
server.close
|
691
|
+
expect(elapsed).to be_within(0.1).of(1.0)
|
692
|
+
end
|
693
|
+
|
694
|
+
it "should write a valid PUBLISH packet to the socket without the retain flag" do
|
695
|
+
client.publish('topic','payload', false, 0)
|
696
|
+
expect(socket.string).to eq("\x30\x0e\x00\x05topicpayload")
|
697
|
+
end
|
698
|
+
|
699
|
+
it "should write a valid PUBLISH packet to the socket with the retain flag set" do
|
700
|
+
client.publish('topic','payload', true, 0)
|
701
|
+
expect(socket.string).to eq("\x31\x0e\x00\x05topicpayload")
|
702
|
+
end
|
703
|
+
|
704
|
+
it "should write a valid PUBLISH packet to the socket with the QoS set to 1" do
|
705
|
+
inject_puback(1)
|
706
|
+
client.publish('topic','payload', false, 1)
|
707
|
+
expect(socket.string).to eq("\x32\x10\x00\x05topic\x00\x01payload")
|
708
|
+
end
|
709
|
+
|
710
|
+
it "should wrap the packet id after 65535" do
|
711
|
+
allow(client).to receive(:wait_for_ack).and_return(true)
|
712
|
+
0xffff.times do |n|
|
713
|
+
socket.string = "" # Clear the socket buffer
|
714
|
+
client.publish('topic','payload', false, 1)
|
715
|
+
end
|
716
|
+
expect(client.instance_variable_get(:@last_packet_id)).to eq(0xffff)
|
717
|
+
|
718
|
+
socket.string = ""
|
719
|
+
client.publish('topic','payload', false, 1)
|
720
|
+
expect(socket.string).to eq("\x32\x10\x00\x05topic\x00\x01payload")
|
721
|
+
end
|
722
|
+
|
723
|
+
it "should write a valid PUBLISH packet to the socket with the QoS set to 2" do
|
724
|
+
allow(client).to receive(:wait_for_ack).and_return(true)
|
725
|
+
client.publish('topic','payload', false, 2)
|
726
|
+
# The PUBLISH packet should be the first part of the socket string
|
727
|
+
expect(socket.string).to eq("4\x10\x00\x05topic\x00\x01payloadb\x02\x00\x01")
|
728
|
+
end
|
729
|
+
|
730
|
+
it "should write a valid PUBLISH packet with no payload" do
|
731
|
+
client.publish('test')
|
732
|
+
expect(socket.string).to eq("\x30\x06\x00\x04test")
|
733
|
+
end
|
734
|
+
|
735
|
+
it "should write a valid PUBLISH packet with frozen payload" do
|
736
|
+
client.publish('topic', 'payload'.freeze, false, 0)
|
737
|
+
expect(socket.string).to eq("\x30\x0e\x00\x05topicpayload")
|
738
|
+
end
|
739
|
+
|
740
|
+
it "should raise an ArgumentError exception, if the topic is nil" do
|
741
|
+
expect {
|
742
|
+
client.publish(nil)
|
743
|
+
}.to raise_error(
|
744
|
+
ArgumentError,
|
745
|
+
'Topic name cannot be nil'
|
746
|
+
)
|
747
|
+
end
|
748
|
+
|
749
|
+
it "should raise an ArgumentError exception, if the topic is empty" do
|
750
|
+
expect {
|
751
|
+
client.publish("")
|
752
|
+
}.to raise_error(
|
753
|
+
ArgumentError,
|
754
|
+
'Topic name cannot be empty'
|
755
|
+
)
|
756
|
+
end
|
757
|
+
|
758
|
+
it "correctly assigns consecutive ids to packets with QoS 1" do
|
759
|
+
inject_puback(1)
|
760
|
+
inject_puback(2)
|
761
|
+
|
762
|
+
expect(client).to receive(:send_packet) { |packet| expect(packet.id).to eq(1) }
|
763
|
+
client.publish "topic", "message", false, 1
|
764
|
+
expect(client).to receive(:send_packet) { |packet| expect(packet.id).to eq(2) }
|
765
|
+
client.publish "topic", "message", false, 1
|
766
|
+
end
|
767
|
+
|
768
|
+
it "does not crash when receiving a PUBACK for a packet it never sent" do
|
769
|
+
expect { client.send(:handle_packet, MQTT::Packet::Puback.new(:id => 666)) }.to_not raise_error
|
770
|
+
end
|
771
|
+
|
772
|
+
it "does not crash with QoS 1 when the broker sends the PUBACK instantly" do
|
773
|
+
allow(client).to receive(:send_packet).and_wrap_original do |send_packet, packet, *args, **kwargs, &block|
|
774
|
+
send_packet.call(packet, *args, **kwargs, &block).tap do
|
775
|
+
client.send(:handle_packet, MQTT::Packet::Puback.new(:id => packet.id))
|
776
|
+
end
|
777
|
+
end
|
778
|
+
|
779
|
+
expect { client.publish("topic", "message", false, 1) }.to_not raise_error
|
780
|
+
end
|
781
|
+
end
|
782
|
+
|
783
|
+
describe "when calling the 'subscribe' method" do
|
784
|
+
before(:each) do
|
785
|
+
client.instance_variable_set('@socket', socket)
|
786
|
+
end
|
787
|
+
|
788
|
+
it "should write a valid SUBSCRIBE packet to the socket if given a single topic String" do
|
789
|
+
client.subscribe('a/b')
|
790
|
+
expect(socket.string).to eq("\x82\x08\x00\x01\x00\x03a/b\x00")
|
791
|
+
end
|
792
|
+
|
793
|
+
it "should write a valid SUBSCRIBE packet to the socket if given a two topic Strings in an Array" do
|
794
|
+
client.subscribe('a/b','c/d')
|
795
|
+
expect(socket.string).to eq("\x82\x0e\x00\x01\x00\x03a/b\x00\x00\x03c/d\x00")
|
796
|
+
end
|
797
|
+
|
798
|
+
it "should write a valid SUBSCRIBE packet to the socket if given a two topic Strings with QoS in an Array" do
|
799
|
+
client.subscribe(['a/b',0],['c/d',1])
|
800
|
+
expect(socket.string).to eq("\x82\x0e\x00\x01\x00\x03a/b\x00\x00\x03c/d\x01")
|
801
|
+
end
|
802
|
+
|
803
|
+
it "should write a valid SUBSCRIBE packet to the socket if given a two topic Strings with QoS in a Hash" do
|
804
|
+
client.subscribe('a/b' => 0,'c/d' => 1)
|
805
|
+
expect(socket.string).to eq("\x82\x0e\x00\x01\x00\x03a/b\x00\x00\x03c/d\x01")
|
806
|
+
end
|
807
|
+
end
|
808
|
+
|
809
|
+
describe "when calling the 'queue_length' method" do
|
810
|
+
it "should return 0 if there are no incoming messages waiting" do
|
811
|
+
expect(client.queue_length).to eq(0)
|
812
|
+
end
|
813
|
+
|
814
|
+
it "should return 1 if there is one incoming message waiting" do
|
815
|
+
inject_packet(:topic => 'topic0', :payload => 'payload0', :qos => 0)
|
816
|
+
expect(client.queue_length).to eq(1)
|
817
|
+
end
|
818
|
+
|
819
|
+
it "should return 2 if there are two incoming message waiting" do
|
820
|
+
inject_packet(:topic => 'topic0', :payload => 'payload0', :qos => 0)
|
821
|
+
inject_packet(:topic => 'topic0', :payload => 'payload1', :qos => 0)
|
822
|
+
expect(client.queue_length).to eq(2)
|
823
|
+
end
|
824
|
+
end
|
825
|
+
|
826
|
+
describe "when calling the 'queue_emtpy?' method" do
|
827
|
+
it "should return return true if there no incoming messages waiting" do
|
828
|
+
expect(client.queue_empty?).to be_truthy
|
829
|
+
end
|
830
|
+
|
831
|
+
it "should return return false if there is an incoming messages waiting" do
|
832
|
+
inject_packet(:topic => 'topic0', :payload => 'payload0', :qos => 0)
|
833
|
+
expect(client.queue_empty?).to be_falsey
|
834
|
+
end
|
835
|
+
end
|
836
|
+
|
837
|
+
describe "when calling the 'clear_queue' method" do
|
838
|
+
it "should clear the waiting incoming messages" do
|
839
|
+
inject_packet(:topic => 'topic0', :payload => 'payload0', :qos => 0)
|
840
|
+
expect(client.queue_length).to eq(1)
|
841
|
+
client.clear_queue
|
842
|
+
expect(client.queue_length).to eq(0)
|
843
|
+
end
|
844
|
+
end
|
845
|
+
|
846
|
+
describe "when calling the 'get' method" do
|
847
|
+
before(:each) do
|
848
|
+
client.instance_variable_set('@socket', socket)
|
849
|
+
end
|
850
|
+
|
851
|
+
it "should successfully receive a valid PUBLISH packet with a QoS 0" do
|
852
|
+
inject_packet(:topic => 'topic0', :payload => 'payload0', :qos => 0)
|
853
|
+
topic,payload = client.get
|
854
|
+
expect(topic).to eq('topic0')
|
855
|
+
expect(payload).to eq('payload0')
|
856
|
+
end
|
857
|
+
|
858
|
+
it "should successfully receive a valid PUBLISH packet with a QoS 1" do
|
859
|
+
inject_packet(:topic => 'topic1', :payload => 'payload1', :qos => 1)
|
860
|
+
topic,payload = client.get
|
861
|
+
expect(topic).to eq('topic1')
|
862
|
+
expect(payload).to eq('payload1')
|
863
|
+
expect(client.queue_empty?).to be_truthy
|
864
|
+
end
|
865
|
+
|
866
|
+
it "should successfully receive a valid PUBLISH packet, but not return it, if omit_retained is set" do
|
867
|
+
inject_packet(:topic => 'topic1', :payload => 'payload1', :qos => 1, :retain => 1)
|
868
|
+
inject_packet(:topic => 'topic1', :payload => 'payload2', :qos => 1)
|
869
|
+
topic,payload = client.get(nil, :omit_retained => true)
|
870
|
+
expect(topic).to eq('topic1')
|
871
|
+
expect(payload).to eq('payload2')
|
872
|
+
expect(client.queue_empty?).to be_truthy
|
873
|
+
end
|
874
|
+
|
875
|
+
it "acks calling #get_packet and qos=1" do
|
876
|
+
inject_packet(:topic => 'topic1', :payload => 'payload1', :qos => 1)
|
877
|
+
expect(client).to receive(:send_packet).with(an_instance_of(MQTT::Packet::Puback))
|
878
|
+
client.get_packet
|
879
|
+
end
|
880
|
+
|
881
|
+
it "acks calling #get and qos=1" do
|
882
|
+
inject_packet(:topic => 'topic1', :payload => 'payload1', :qos => 1)
|
883
|
+
expect(client).to receive(:send_packet).with(an_instance_of(MQTT::Packet::Puback))
|
884
|
+
client.get
|
885
|
+
end
|
886
|
+
|
887
|
+
context "with a block" do
|
888
|
+
it "should successfully receive more than 1 message" do
|
889
|
+
inject_packet(:topic => 'topic0', :payload => 'payload0')
|
890
|
+
inject_packet(:topic => 'topic1', :payload => 'payload1')
|
891
|
+
payloads = []
|
892
|
+
client.get do |topic,payload|
|
893
|
+
payloads << payload
|
894
|
+
break if payloads.size > 1
|
895
|
+
end
|
896
|
+
expect(payloads.size).to eq(2)
|
897
|
+
expect(payloads).to eq(['payload0', 'payload1'])
|
898
|
+
end
|
899
|
+
|
900
|
+
it "acks when qos > 1 after running the block" do
|
901
|
+
inject_packet(:topic => 'topic1', :payload => 'payload1', :qos => 1)
|
902
|
+
inject_packet(:topic => 'topic2', :payload => 'payload1')
|
903
|
+
expect(client).to receive(:send_packet).with(an_instance_of(MQTT::Packet::Puback))
|
904
|
+
payloads = []
|
905
|
+
client.get do |topic,payload|
|
906
|
+
payloads << payload
|
907
|
+
break if payloads.size > 1
|
908
|
+
end
|
909
|
+
end
|
910
|
+
|
911
|
+
it "should ignore a PUBLISH message when it is marked as retained and omit_retained is set" do
|
912
|
+
inject_packet(:topic => 'topic0', :payload => 'payload0', :retain => 1)
|
913
|
+
inject_packet(:topic => 'topic1', :payload => 'payload1')
|
914
|
+
payloads = []
|
915
|
+
client.get(nil, :omit_retained => true) do |topic,payload|
|
916
|
+
payloads << payload
|
917
|
+
break if payloads.size > 0
|
918
|
+
end
|
919
|
+
expect(payloads.size).to eq(1)
|
920
|
+
expect(payloads).to eq(['payload1'])
|
921
|
+
end
|
922
|
+
end
|
923
|
+
end
|
924
|
+
|
925
|
+
describe "when calling the 'get_packet' method" do
|
926
|
+
before(:each) do
|
927
|
+
client.instance_variable_set('@socket', socket)
|
928
|
+
end
|
929
|
+
|
930
|
+
it "should successfully receive a valid PUBLISH packet with a QoS 0" do
|
931
|
+
inject_packet(:topic => 'topic0', :payload => 'payload0', :qos => 0)
|
932
|
+
packet = client.get_packet
|
933
|
+
expect(packet.class).to eq(MQTT::Packet::Publish)
|
934
|
+
expect(packet.qos).to eq(0)
|
935
|
+
expect(packet.topic).to eq('topic0')
|
936
|
+
expect(packet.payload).to eq('payload0')
|
937
|
+
end
|
938
|
+
|
939
|
+
it "should successfully receive a valid PUBLISH packet with a QoS 1" do
|
940
|
+
inject_packet(:topic => 'topic1', :payload => 'payload1', :qos => 1)
|
941
|
+
packet = client.get_packet
|
942
|
+
expect(packet.class).to eq(MQTT::Packet::Publish)
|
943
|
+
expect(packet.qos).to eq(1)
|
944
|
+
expect(packet.topic).to eq('topic1')
|
945
|
+
expect(packet.payload).to eq('payload1')
|
946
|
+
expect(client.queue_empty?).to be_truthy
|
947
|
+
end
|
948
|
+
|
949
|
+
context "with a block" do
|
950
|
+
it "should successfully receive more than 1 packet" do
|
951
|
+
inject_packet(:topic => 'topic0', :payload => 'payload0')
|
952
|
+
inject_packet(:topic => 'topic1', :payload => 'payload1')
|
953
|
+
packets = []
|
954
|
+
client.get_packet do |packet|
|
955
|
+
packets << packet
|
956
|
+
break if packets.size > 1
|
957
|
+
end
|
958
|
+
expect(packets.size).to eq(2)
|
959
|
+
expect(packets.map{|p| p.payload}).to eq(['payload0', 'payload1'])
|
960
|
+
end
|
961
|
+
end
|
962
|
+
end
|
963
|
+
|
964
|
+
describe "when calling the 'unsubscribe' method" do
|
965
|
+
before(:each) do
|
966
|
+
client.instance_variable_set('@socket', socket)
|
967
|
+
end
|
968
|
+
|
969
|
+
it "should write a valid UNSUBSCRIBE packet to the socket if given a single topic String" do
|
970
|
+
client.unsubscribe('a/b')
|
971
|
+
expect(socket.string).to eq("\xa2\x07\x00\x01\x00\x03a/b")
|
972
|
+
end
|
973
|
+
|
974
|
+
it "should write a valid UNSUBSCRIBE packet to the socket if given a two topic Strings" do
|
975
|
+
client.unsubscribe('a/b','c/d')
|
976
|
+
expect(socket.string).to eq("\xa2\x0c\x00\x01\x00\x03a/b\x00\x03c/d")
|
977
|
+
end
|
978
|
+
|
979
|
+
it "should write a valid UNSUBSCRIBE packet to the socket if given an array of Strings" do
|
980
|
+
client.unsubscribe(['a/b','c/d'])
|
981
|
+
expect(socket.string).to eq("\xa2\x0c\x00\x01\x00\x03a/b\x00\x03c/d")
|
982
|
+
end
|
983
|
+
end
|
984
|
+
|
985
|
+
describe "when calling the 'receive_packet' method" do
|
986
|
+
before(:each) do
|
987
|
+
client.instance_variable_set('@socket', socket)
|
988
|
+
allow(IO).to receive(:select).and_return([[socket], [], []])
|
989
|
+
@read_queue = client.instance_variable_get('@read_queue')
|
990
|
+
@parent_thread = Thread.current[:parent] = double('Parent Thread')
|
991
|
+
allow(@parent_thread).to receive(:raise)
|
992
|
+
end
|
993
|
+
|
994
|
+
it "should put PUBLISH messages on to the read queue" do
|
995
|
+
socket.write("\x30\x0e\x00\x05topicpayload")
|
996
|
+
socket.rewind
|
997
|
+
client.send(:receive_packet)
|
998
|
+
expect(@read_queue.size).to eq(1)
|
999
|
+
end
|
1000
|
+
|
1001
|
+
it "should not put other messages on to the read queue" do
|
1002
|
+
socket.write("\x20\x02\x00\x00")
|
1003
|
+
socket.rewind
|
1004
|
+
client.send(:receive_packet)
|
1005
|
+
expect(@read_queue.size).to eq(0)
|
1006
|
+
end
|
1007
|
+
|
1008
|
+
it "should close the socket if there is an MQTT exception" do
|
1009
|
+
expect(socket).to receive(:close).once
|
1010
|
+
allow(MQTT::Packet).to receive(:read).and_raise(MQTT::Exception)
|
1011
|
+
client.send(:receive_packet)
|
1012
|
+
end
|
1013
|
+
|
1014
|
+
it "should close the socket if there is a system call error" do
|
1015
|
+
expect(socket).to receive(:close).once
|
1016
|
+
allow(MQTT::Packet).to receive(:read).and_raise(Errno::ECONNRESET)
|
1017
|
+
client.send(:receive_packet)
|
1018
|
+
end
|
1019
|
+
|
1020
|
+
it "should pass exceptions up to parent thread" do
|
1021
|
+
e = MQTT::Exception.new
|
1022
|
+
expect(@parent_thread).to receive(:raise).with(e).once
|
1023
|
+
allow(MQTT::Packet).to receive(:read).and_raise(e)
|
1024
|
+
client.send(:receive_packet)
|
1025
|
+
end
|
1026
|
+
|
1027
|
+
it "should pass a system call error up to parent thread" do
|
1028
|
+
e = Errno::ECONNRESET.new
|
1029
|
+
expect(@parent_thread).to receive(:raise).with(e).once
|
1030
|
+
allow(MQTT::Packet).to receive(:read).and_raise(e)
|
1031
|
+
client.send(:receive_packet)
|
1032
|
+
end
|
1033
|
+
|
1034
|
+
it "should update last_ping_response when receiving a Pingresp" do
|
1035
|
+
allow(MQTT::Packet).to receive(:read).and_return MQTT::Packet::Pingresp.new
|
1036
|
+
client.instance_variable_set '@last_ping_response', Time.at(0)
|
1037
|
+
client.send :receive_packet
|
1038
|
+
expect(client.last_ping_response).to be_within(1).of now
|
1039
|
+
end
|
1040
|
+
end
|
1041
|
+
|
1042
|
+
describe "when calling the 'keep_alive!' method" do
|
1043
|
+
before(:each) do
|
1044
|
+
client.instance_variable_set('@socket', socket)
|
1045
|
+
end
|
1046
|
+
|
1047
|
+
it "should send a ping packet if one is due" do
|
1048
|
+
client.instance_variable_set('@last_ping_request', 0.0)
|
1049
|
+
client.send('keep_alive!')
|
1050
|
+
expect(socket.string).to eq("\xC0\x00")
|
1051
|
+
end
|
1052
|
+
|
1053
|
+
it "should update the time a ping was last sent" do
|
1054
|
+
client.instance_variable_set('@last_ping_request', 0.0)
|
1055
|
+
client.send('keep_alive!')
|
1056
|
+
expect(client.instance_variable_get('@last_ping_request')).to be_within(0.01).of(now)
|
1057
|
+
end
|
1058
|
+
|
1059
|
+
it "should raise an exception if no ping response has been received" do
|
1060
|
+
client.instance_variable_set('@last_ping_request', now)
|
1061
|
+
client.instance_variable_set('@last_ping_response', 0.0)
|
1062
|
+
expect {
|
1063
|
+
client.send('keep_alive!')
|
1064
|
+
}.to raise_error(
|
1065
|
+
MQTT::ProtocolException,
|
1066
|
+
/No Ping Response received for \d+ seconds/
|
1067
|
+
)
|
1068
|
+
end
|
1069
|
+
|
1070
|
+
it "should not raise an exception if no ping response received and client is disconnected" do
|
1071
|
+
client.instance_variable_set('@last_ping_request', Time.now)
|
1072
|
+
client.instance_variable_set('@last_ping_response', Time.at(0))
|
1073
|
+
client.disconnect(false)
|
1074
|
+
client.send('keep_alive!')
|
1075
|
+
end
|
1076
|
+
end
|
1077
|
+
|
1078
|
+
describe "generating a client identifier" do
|
1079
|
+
context "with default parameters" do
|
1080
|
+
let(:client_id) { MQTT::Client.generate_client_id }
|
1081
|
+
|
1082
|
+
it "should be less or equal to 23 characters long" do
|
1083
|
+
expect(client_id.length).to be <= 23
|
1084
|
+
end
|
1085
|
+
|
1086
|
+
it "should have a prefix of ruby" do
|
1087
|
+
expect(client_id).to match(/^ruby/)
|
1088
|
+
end
|
1089
|
+
|
1090
|
+
it "should end in 16 characters of lowercase letters and numbers" do
|
1091
|
+
expect(client_id).to match(/^ruby[a-z0-9]{16}$/)
|
1092
|
+
end
|
1093
|
+
end
|
1094
|
+
|
1095
|
+
context "with an alternative prefix" do
|
1096
|
+
let(:client_id) { MQTT::Client.generate_client_id('test') }
|
1097
|
+
|
1098
|
+
it "should be less or equal to 23 characters long" do
|
1099
|
+
expect(client_id.length).to be <= 23
|
1100
|
+
end
|
1101
|
+
|
1102
|
+
it "should have a prefix of test" do
|
1103
|
+
expect(client_id).to match(/^test/)
|
1104
|
+
end
|
1105
|
+
|
1106
|
+
it "should end in 16 characters of lowercase letters and numbers" do
|
1107
|
+
expect(client_id).to match(/^test[a-z0-9]{16}$/)
|
1108
|
+
end
|
1109
|
+
end
|
1110
|
+
end
|
1111
|
+
|
1112
|
+
private
|
1113
|
+
|
1114
|
+
def inject_packet(opts={})
|
1115
|
+
packet = MQTT::Packet::Publish.new(opts)
|
1116
|
+
client.instance_variable_get('@read_queue').push(packet)
|
1117
|
+
end
|
1118
|
+
|
1119
|
+
def inject_puback(packet_id)
|
1120
|
+
packet = MQTT::Packet::Puback.new(:id => packet_id)
|
1121
|
+
client.inject_puback packet
|
1122
|
+
end
|
1123
|
+
|
1124
|
+
end
|