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.
@@ -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