qubitro-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,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