qubitro-mqtt 0.0.1

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