arborist 0.1.0 → 0.2.0.pre20170519125456

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.
@@ -14,6 +14,7 @@ describe Arborist::Client do
14
14
  before( :each ) do
15
15
  @manager_thread = Thread.new do
16
16
  @manager = make_testing_manager()
17
+ Loggability[ Arborist ].info "Starting a testing manager: %p" % [ @manager ]
17
18
  Thread.current.abort_on_exception = true
18
19
  @manager.run
19
20
  Loggability[ Arborist ].info "Stopped the test manager"
@@ -24,27 +25,29 @@ describe Arborist::Client do
24
25
  sleep 0.1
25
26
  count += 1
26
27
  end
27
- raise "Manager didn't start up" unless @manager.running?
28
+ raise "Manager didn't start up" unless @manager && @manager.running?
28
29
  end
29
30
 
30
31
  after( :each ) do
31
- @manager.simulate_signal( :TERM )
32
- @manager_thread.join
33
-
34
- count = 0
35
- while @manager.zmq_loop.running? || count > 30
36
- sleep 0.1
37
- Loggability[ Arborist ].info "ZMQ loop still running"
38
- count += 1
32
+ if @manager
33
+ @manager.simulate_signal( :TERM )
34
+ @manager_thread.join
35
+
36
+ count = 0
37
+ while @manager.running? || count > 30
38
+ sleep 0.1
39
+ Loggability[ Arborist ].info "Manager still running"
40
+ count += 1
41
+ end
42
+ raise "Manager didn't stop" if @manager.running?
39
43
  end
40
- raise "ZMQ Loop didn't stop" if @manager.zmq_loop.running?
41
44
  end
42
45
 
43
46
 
44
47
  let( :manager ) { @manager }
45
48
 
46
49
 
47
- describe "high-level API" do
50
+ describe "high-level methods" do
48
51
 
49
52
  it "provides a convenience method for acknowledging" do
50
53
  manager.nodes['sidonie'].update( error: "Clown apocalypse" )
@@ -260,7 +263,8 @@ describe Arborist::Client do
260
263
  it "can prune nodes from the tree" do
261
264
  res = client.prune( 'sidonie-ssh' )
262
265
 
263
- expect( res ).to eq( true )
266
+ expect( res ).to be_a( Hash )
267
+ expect( res ).to include( 'identifier' => 'sidonie-ssh' )
264
268
  expect( manager.nodes ).to_not include( 'sidonie-ssh' )
265
269
  end
266
270
 
@@ -273,7 +277,7 @@ describe Arborist::Client do
273
277
 
274
278
  it "can graft new nodes onto the tree" do
275
279
  res = client.graft( 'breakfast-burrito', type: 'host' )
276
- expect( res ).to eq( 'breakfast-burrito' )
280
+ expect( res ).to eq({ 'identifier' => 'breakfast-burrito' })
277
281
  expect( manager.nodes ).to include( 'breakfast-burrito' )
278
282
  expect( manager.nodes['breakfast-burrito'] ).to be_a( Arborist::Node::Host )
279
283
  expect( manager.nodes['breakfast-burrito'].parent ).to eq( '_' )
@@ -287,7 +291,7 @@ describe Arborist::Client do
287
291
  port: 9999,
288
292
  tags: ['yusss']
289
293
  )
290
- expect( res ).to eq( 'duir-breakfast-burrito' )
294
+ expect( res ).to eq({ 'identifier' => 'duir-breakfast-burrito' })
291
295
  expect( manager.nodes ).to include( 'duir-breakfast-burrito' )
292
296
  expect( manager.nodes['duir-breakfast-burrito'] ).to be_a( Arborist::Node::Service )
293
297
  expect( manager.nodes['duir-breakfast-burrito'].parent ).to eq( 'duir' )
@@ -311,62 +315,58 @@ describe Arborist::Client do
311
315
 
312
316
  it "can make a raw status request" do
313
317
  req = client.make_status_request
314
- expect( req ).to be_a( String )
315
- expect( req.encoding ).to eq( Encoding::ASCII_8BIT )
316
-
317
- msg = unpack_message( req )
318
- expect( msg ).to be_an( Array )
319
- expect( msg.first ).to be_a( Hash )
320
- expect( msg.first ).to include( 'version', 'action' )
321
- expect( msg.first['version'] ).to eq( Arborist::Client::API_VERSION )
322
- expect( msg.first['action'] ).to eq( 'status' )
318
+ expect( req ).to be_a( CZTop::Message )
319
+
320
+ header, body = Arborist::TreeAPI.decode( req )
321
+
322
+ expect( header ).to be_a( Hash )
323
+ expect( header ).to include( 'version', 'action' )
324
+ expect( header['version'] ).to eq( Arborist::Client::API_VERSION )
325
+ expect( header['action'] ).to eq( 'status' )
323
326
  end
324
327
 
325
328
 
326
329
  it "can make a raw list request" do
327
330
  req = client.make_list_request
328
- expect( req ).to be_a( String )
329
- expect( req.encoding ).to eq( Encoding::ASCII_8BIT )
330
-
331
- msg = unpack_message( req )
332
- expect( msg ).to be_an( Array )
333
- expect( msg.first ).to be_a( Hash )
334
- expect( msg.first ).to include( 'version', 'action' )
335
- expect( msg.first ).to_not include( 'from' )
336
- expect( msg.first['version'] ).to eq( Arborist::Client::API_VERSION )
337
- expect( msg.first['action'] ).to eq( 'list' )
331
+ expect( req ).to be_a( CZTop::Message )
332
+
333
+ header, body = Arborist::TreeAPI.decode( req )
334
+
335
+ expect( header ).to be_a( Hash )
336
+ expect( header ).to include( 'version', 'action' )
337
+ expect( header ).to_not include( 'from' )
338
+ expect( header['version'] ).to eq( Arborist::Client::API_VERSION )
339
+ expect( header['action'] ).to eq( 'list' )
338
340
  end
339
341
 
340
342
 
341
343
  it "can make a raw fetch request" do
342
344
  req = client.make_fetch_request( {} )
343
- expect( req ).to be_a( String )
344
- expect( req.encoding ).to eq( Encoding::ASCII_8BIT )
345
+ expect( req ).to be_a( CZTop::Message )
345
346
 
346
- msg = unpack_message( req )
347
- expect( msg ).to be_an( Array )
348
- expect( msg.first ).to be_a( Hash )
349
- expect( msg.first ).to include( 'version', 'action' )
350
- expect( msg.first['version'] ).to eq( Arborist::Client::API_VERSION )
351
- expect( msg.first['action'] ).to eq( 'fetch' )
347
+ header, body = Arborist::TreeAPI.decode( req )
352
348
 
353
- expect( msg.last ).to eq([ {}, {} ])
349
+ expect( header ).to be_a( Hash )
350
+ expect( header ).to include( 'version', 'action' )
351
+ expect( header['version'] ).to eq( Arborist::Client::API_VERSION )
352
+ expect( header['action'] ).to eq( 'fetch' )
353
+
354
+ expect( body ).to eq([ {}, {} ])
354
355
  end
355
356
 
356
357
 
357
358
  it "can make a raw fetch request with criteria" do
358
359
  req = client.make_fetch_request( {type: 'host'} )
359
- expect( req ).to be_a( String )
360
- expect( req.encoding ).to eq( Encoding::ASCII_8BIT )
360
+ expect( req ).to be_a( CZTop::Message )
361
+
362
+ header, body = Arborist::TreeAPI.decode( req )
361
363
 
362
- msg = unpack_message( req )
363
- expect( msg ).to be_an( Array )
364
- expect( msg.first ).to be_a( Hash )
365
- expect( msg.first ).to include( 'version', 'action' )
366
- expect( msg.first['version'] ).to eq( Arborist::Client::API_VERSION )
367
- expect( msg.first['action'] ).to eq( 'fetch' )
364
+ expect( header ).to be_a( Hash )
365
+ expect( header ).to include( 'version', 'action' )
366
+ expect( header['version'] ).to eq( Arborist::Client::API_VERSION )
367
+ expect( header['action'] ).to eq( 'fetch' )
368
368
 
369
- body = msg.last
369
+ body = body
370
370
  expect( body.first ).to be_a( Hash )
371
371
  expect( body.first ).to include( 'type' )
372
372
  expect( body.first['type'] ).to eq( 'host' )
@@ -375,19 +375,18 @@ describe Arborist::Client do
375
375
 
376
376
  it "can make a raw update request" do
377
377
  req = client.make_update_request( duir: {error: "Something happened."} )
378
- expect( req ).to be_a( String )
379
- expect( req.encoding ).to eq( Encoding::ASCII_8BIT )
380
-
381
- msg = unpack_message( req )
382
- expect( msg ).to be_an( Array )
383
- expect( msg.first ).to be_a( Hash )
384
- expect( msg.first ).to include( 'version', 'action' )
385
- expect( msg.first['version'] ).to eq( Arborist::Client::API_VERSION )
386
- expect( msg.first['action'] ).to eq( 'update' )
387
-
388
- expect( msg.last ).to be_a( Hash )
389
- expect( msg.last ).to include( 'duir' )
390
- expect( msg.last['duir'] ).to eq( 'error' => 'Something happened.' )
378
+ expect( req ).to be_a( CZTop::Message )
379
+
380
+ header, body = Arborist::TreeAPI.decode( req )
381
+
382
+ expect( header ).to be_a( Hash )
383
+ expect( header ).to include( 'version', 'action' )
384
+ expect( header['version'] ).to eq( Arborist::Client::API_VERSION )
385
+ expect( header['action'] ).to eq( 'update' )
386
+
387
+ expect( body ).to be_a( Hash )
388
+ expect( body ).to include( 'duir' )
389
+ expect( body['duir'] ).to eq( 'error' => 'Something happened.' )
391
390
  end
392
391
 
393
392
  end
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env rspec -cfd
2
+
3
+ require_relative '../spec_helper'
4
+
5
+ require 'arborist/event_api'
6
+
7
+
8
+ describe Arborist::EventAPI do
9
+
10
+ let( :uuid ) { '9E630B46-B0D2-4658-AFE6-ED4A1E838C69' }
11
+
12
+ it "encodes events published by the Manager" do
13
+ encoded = described_class.encode( uuid, {a: 1, b: 2} )
14
+ expect( encoded ).to be_a( CZTop::Message )
15
+ identifier, payload = described_class.decode( encoded )
16
+ expect( identifier ).to eq( uuid )
17
+ expect( payload ).to eq({ 'a' => 1, 'b' => 2 })
18
+ end
19
+
20
+
21
+ end
22
+
23
+
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative '../spec_helper'
4
4
 
5
+ require 'tmpdir'
5
6
  require 'timecop'
6
7
  require 'arborist/manager'
7
8
  require 'arborist/node/host'
@@ -125,8 +126,8 @@ describe Arborist::Manager do
125
126
  it "restores the state of loaded nodes if the state file is configured" do
126
127
  _ = manager
127
128
 
128
- statefile = Pathname( './arborist.tree' )
129
- Arborist::Manager.state_file = statefile
129
+ Arborist::Manager.state_file = './arborist.tree'
130
+ statefile = Arborist::Manager.state_file
130
131
  state_file_io = instance_double( File )
131
132
 
132
133
  saved_router_node = Marshal.load( Marshal.dump(router_node) )
@@ -153,27 +154,30 @@ describe Arborist::Manager do
153
154
  it "doesn't error if the configured state file isn't readable" do
154
155
  _ = manager
155
156
 
156
- statefile = Pathname( './arborist.tree' )
157
- Arborist::Manager.state_file = statefile
157
+ Arborist::Manager.state_file = './arborist.tree'
158
158
 
159
- expect( statefile ).to receive( :readable? ).and_return( false )
160
- expect( statefile ).to_not receive( :open )
159
+ expect( Arborist::Manager.state_file ).to receive( :readable? ).and_return( false )
160
+ expect( Arborist::Manager.state_file ).to_not receive( :open )
161
161
 
162
162
  expect( manager.restore_node_states ).to be_falsey
163
163
  end
164
164
 
165
165
 
166
166
  it "checkpoints the state file periodically if an interval is configured" do
167
- described_class.configure( checkpoint_frequency: 20_000, state_file: 'arb.tree' )
168
-
169
- zloop = instance_double( ZMQ::Loop, register: nil, :verbose= => nil )
170
- timer = instance_double( ZMQ::Timer, "checkpoint timer" )
171
- expect( ZMQ::Loop ).to receive( :new ).and_return( zloop )
172
- allow( ZMQ::Timer ).to receive( :new ).and_call_original
173
- expect( ZMQ::Timer ).to receive( :new ).with( 20.0, 0 ).and_return( timer )
167
+ statefile = Pathname( Dir.tmpdir ) + Dir::Tmpname.make_tmpname( 'arb', 'tree' )
168
+ described_class.configure( checkpoint_frequency: 20_000, state_file: statefile )
174
169
 
175
170
  manager = described_class.new
176
- expect( manager.checkpoint_timer ).to eq( timer )
171
+ manager.register_checkpoint_timer
172
+ expect( manager.checkpoint_timer ).to be_a( Timers::Timer )
173
+ expect( statefile ).to_not exist
174
+
175
+ manager.checkpoint_timer.fire
176
+ expect( statefile ).to exist
177
+ states = Marshal.load( statefile.open('r:binary') )
178
+
179
+ expect( states ).to be_a( Hash )
180
+ expect( states.keys ).to eq( manager.nodes.keys )
177
181
  end
178
182
 
179
183
 
@@ -204,7 +208,7 @@ describe Arborist::Manager do
204
208
  it "errors if configured with a heartbeat of 0" do
205
209
  expect {
206
210
  described_class.configure( heartbeat_frequency: 0 )
207
- }.to raise_error( Arborist::ConfigError, /positive non-zero/i )
211
+ }.to raise_error( Arborist::ConfigError, /positive and non-zero/i )
208
212
  end
209
213
 
210
214
 
@@ -361,6 +365,651 @@ describe Arborist::Manager do
361
365
  end
362
366
 
363
367
 
368
+ xdescribe "tree API", :testing_manager do
369
+
370
+ before( :each ) do
371
+ @manager = nil
372
+ @manager_thread = Thread.new do
373
+ @manager = make_testing_manager()
374
+ Thread.current.abort_on_exception = true
375
+ @manager.run
376
+ Loggability[ Arborist ].info "Stopped the test manager"
377
+ end
378
+
379
+ count = 0
380
+ until (@manager && @manager.running?) || count > 30
381
+ sleep 0.1
382
+ count += 1
383
+ end
384
+ raise "Manager didn't start up" unless @manager.running?
385
+ end
386
+
387
+ after( :each ) do
388
+ @manager.simulate_signal( :TERM )
389
+ unless @manager_thread.join( 5 )
390
+ $stderr.puts "Manager thread didn't exit on its own; killing it."
391
+ @manager_thread.kill
392
+ end
393
+
394
+ count = 0
395
+ while @manager.running? || count > 30
396
+ sleep 0.1
397
+ Loggability[ Arborist ].info "Manager still running"
398
+ count += 1
399
+ end
400
+ raise "Manager didn't stop" if @manager.running?
401
+ end
402
+
403
+
404
+ describe "status" do
405
+
406
+
407
+ it "returns a Map describing the manager and its state" do
408
+ msg = Arborist::TreeAPI.encode( :status )
409
+
410
+ sock.send( msg )
411
+ resmsg = sock.recv
412
+
413
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
414
+ expect( hdr ).to include( 'success' => true )
415
+ expect( body.length ).to eq( 4 )
416
+ expect( body ).to include( 'server_version', 'state', 'uptime', 'nodecount' )
417
+ end
418
+
419
+ end
420
+
421
+
422
+ describe "fetch" do
423
+
424
+ it "returns an array of full state maps for nodes matching specified criteria" do
425
+ msg = Arborist::TreeAPI.encode( :fetch, type: 'service', port: 22 )
426
+
427
+ sock.send( msg )
428
+ resmsg = sock.recv
429
+
430
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
431
+ expect( hdr ).to include( 'success' => true )
432
+
433
+ expect( body ).to be_a( Hash )
434
+ expect( body.length ).to eq( 3 )
435
+
436
+ expect( body.values ).to all( be_a(Hash) )
437
+ expect( body.values ).to all( include('status', 'type') )
438
+ end
439
+
440
+
441
+ it "returns an array of full state maps for nodes not matching specified negative criteria" do
442
+ msg = Arborist::TreeAPI.encode( :fetch, [ {}, {type: 'service', port: 22} ] )
443
+
444
+ sock.send( msg )
445
+ resmsg = sock.recv
446
+
447
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
448
+ expect( hdr ).to include( 'success' => true )
449
+
450
+ expect( body ).to be_a( Hash )
451
+ expect( body.length ).to eq( manager.nodes.length - 3 )
452
+
453
+ expect( body.values ).to all( be_a(Hash) )
454
+ expect( body.values ).to all( include('status', 'type') )
455
+ end
456
+
457
+
458
+ it "returns an array of full state maps for nodes combining positive and negative criteria" do
459
+ msg = Arborist::TreeAPI.encode( :fetch, [ {type: 'service'}, {port: 22} ] )
460
+
461
+ sock.send( msg )
462
+ resmsg = sock.recv
463
+
464
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
465
+ expect( hdr ).to include( 'success' => true )
466
+
467
+ expect( body ).to be_a( Hash )
468
+ expect( body.length ).to eq( 16 )
469
+
470
+ expect( body.values ).to all( be_a(Hash) )
471
+ expect( body.values ).to all( include('status', 'type') )
472
+ end
473
+
474
+
475
+ it "doesn't return nodes beneath downed nodes by default" do
476
+ manager.nodes['sidonie'].update( error: 'sunspots' )
477
+ msg = Arborist::TreeAPI.encode( :fetch, type: 'service', port: 22 )
478
+
479
+ sock.send( msg )
480
+ resmsg = sock.recv
481
+
482
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
483
+ expect( hdr ).to include( 'success' => true )
484
+ expect( body ).to be_a( Hash )
485
+ expect( body.length ).to eq( 2 )
486
+ expect( body ).to include( 'duir-ssh', 'yevaud-ssh' )
487
+ end
488
+
489
+
490
+ it "does return nodes beneath downed nodes if asked to" do
491
+ manager.nodes['sidonie'].update( error: 'plague of locusts' )
492
+ msg = Arborist::TreeAPI.encode( :fetch, {include_down: true}, type: 'service', port: 22 )
493
+
494
+ sock.send( msg )
495
+ resmsg = sock.recv
496
+
497
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
498
+ expect( hdr ).to include( 'success' => true )
499
+ expect( body ).to be_a( Hash )
500
+ expect( body.length ).to eq( 3 )
501
+ expect( body ).to include( 'duir-ssh', 'yevaud-ssh', 'sidonie-ssh' )
502
+ end
503
+
504
+
505
+ it "returns only identifiers if the `return` header is set to `nil`" do
506
+ msg = Arborist::TreeAPI.encode( :fetch, {return: nil}, type: 'service', port: 22 )
507
+
508
+ sock.send( msg )
509
+ resmsg = sock.recv
510
+
511
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
512
+ expect( hdr ).to include( 'success' => true )
513
+ expect( body ).to be_a( Hash )
514
+ expect( body.length ).to eq( 3 )
515
+ expect( body ).to include( 'duir-ssh', 'yevaud-ssh', 'sidonie-ssh' )
516
+ expect( body.values ).to all( be_empty )
517
+ end
518
+
519
+
520
+ it "returns only specified state if the `return` header is set to an Array of keys" do
521
+ msg = Arborist::TreeAPI.encode( :fetch, {return: %w[status tags addresses]},
522
+ type: 'service', port: 22 )
523
+
524
+ sock.send( msg )
525
+ resmsg = sock.recv
526
+
527
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
528
+ expect( hdr ).to include( 'success' => true )
529
+ expect( body.length ).to eq( 3 )
530
+ expect( body ).to include( 'duir-ssh', 'yevaud-ssh', 'sidonie-ssh' )
531
+ expect( body.values.map(&:keys) ).to all( contain_exactly('status', 'tags', 'addresses') )
532
+ end
533
+
534
+
535
+ end
536
+
537
+
538
+ describe "list" do
539
+
540
+ it "returns an array of node state" do
541
+ msg = Arborist::TreeAPI.encode( :list )
542
+ sock.send( msg )
543
+ resmsg = sock.recv
544
+
545
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
546
+ expect( hdr ).to include( 'success' => true )
547
+ expect( body.length ).to eq( manager.nodes.length )
548
+ expect( body ).to all( be_a(Hash) )
549
+ expect( body ).to include( hash_including('identifier' => '_') )
550
+ expect( body ).to include( hash_including('identifier' => 'duir') )
551
+ expect( body ).to include( hash_including('identifier' => 'sidonie') )
552
+ expect( body ).to include( hash_including('identifier' => 'sidonie-ssh') )
553
+ expect( body ).to include( hash_including('identifier' => 'sidonie-demon-http') )
554
+ expect( body ).to include( hash_including('identifier' => 'yevaud') )
555
+ end
556
+
557
+ it "can be limited by depth" do
558
+ msg = Arborist::TreeAPI.encode( :list, {depth: 1}, nil )
559
+ sock.send( msg )
560
+ resmsg = sock.recv
561
+
562
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
563
+ expect( hdr ).to include( 'success' => true )
564
+ expect( body.length ).to eq( 3 )
565
+ expect( body ).to all( be_a(Hash) )
566
+ expect( body ).to include( hash_including('identifier' => '_') )
567
+ expect( body ).to include( hash_including('identifier' => 'duir') )
568
+ expect( body ).to_not include( hash_including('identifier' => 'duir-ssh') )
569
+ end
570
+ end
571
+
572
+
573
+ describe "update" do
574
+
575
+ it "merges the properties sent with those of the targeted nodes" do
576
+ update_data = {
577
+ duir: {
578
+ ping: {
579
+ rtt: 254
580
+ }
581
+ },
582
+ sidonie: {
583
+ ping: {
584
+ rtt: 1208
585
+ }
586
+ },
587
+ yevaud: {
588
+ ping: {
589
+ rtt: 843
590
+ }
591
+ }
592
+ }
593
+ msg = Arborist::TreeAPI.encode( :update, update_data )
594
+ sock.send( msg )
595
+ resmsg = sock.recv
596
+
597
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
598
+ expect( hdr ).to include( 'success' => true )
599
+ expect( body ).to be_nil
600
+
601
+ expect( manager.nodes['duir'].properties['ping'] ).to include( 'rtt' => 254 )
602
+ expect( manager.nodes['sidonie'].properties['ping'] ).to include( 'rtt' => 1208 )
603
+ expect( manager.nodes['yevaud'].properties['ping'] ).to include( 'rtt' => 843 )
604
+ end
605
+
606
+
607
+ it "ignores unknown identifiers" do
608
+ msg = Arborist::TreeAPI.encode( :update, charlie_humperton: {ping: { rtt: 8 }} )
609
+ sock.send( msg )
610
+ resmsg = sock.recv
611
+
612
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
613
+ expect( hdr ).to include( 'success' => true )
614
+ end
615
+
616
+ it "fails with a client error if the body is invalid" do
617
+ msg = Arborist::TreeAPI.encode( :update, nil )
618
+ sock.send( msg )
619
+ resmsg = sock.recv
620
+
621
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
622
+ expect( hdr ).to include( 'success' => false )
623
+ expect( hdr['reason'] ).to match( /respond to #each/ )
624
+ end
625
+ end
626
+
627
+
628
+ describe "subscribe" do
629
+
630
+ it "adds a subscription for all event types to the root node by default" do
631
+ msg = Arborist::TreeAPI.encode( :subscribe, [{}, {}] )
632
+
633
+ resmsg = nil
634
+ expect {
635
+ sock.send( msg )
636
+ resmsg = sock.recv
637
+ }.to change { manager.subscriptions.length }.by( 1 ).and(
638
+ change { manager.root.subscriptions.length }.by( 1 )
639
+ )
640
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
641
+
642
+ sub_id = manager.subscriptions.keys.first
643
+
644
+ expect( hdr ).to include( 'success' => true )
645
+ expect( body ).to eq([ sub_id ])
646
+ end
647
+
648
+
649
+ it "adds a subscription to the specified node if an identifier is specified" do
650
+ msg = Arborist::TreeAPI.encode( :subscribe, {identifier: 'sidonie'}, [{}, {}] )
651
+
652
+ resmsg = nil
653
+ expect {
654
+ sock.send( msg )
655
+ resmsg = sock.recv
656
+ }.to change { manager.subscriptions.length }.by( 1 ).and(
657
+ change { manager.nodes['sidonie'].subscriptions.length }.by( 1 )
658
+ )
659
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
660
+
661
+ sub_id = manager.subscriptions.keys.first
662
+
663
+ expect( hdr ).to include( 'success' => true )
664
+ expect( body ).to eq([ sub_id ])
665
+ end
666
+
667
+
668
+ it "adds a subscription for particular event types if one is specified" do
669
+ msg = Arborist::TreeAPI.encode( :subscribe, {event_type: 'node.acked'}, [{}, {}] )
670
+
671
+ resmsg = nil
672
+ expect {
673
+ sock.send( msg )
674
+ resmsg = sock.recv
675
+ }.to change { manager.subscriptions.length }.by( 1 ).and(
676
+ change { manager.root.subscriptions.length }.by( 1 )
677
+ )
678
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
679
+ node = manager.subscriptions[ body.first ]
680
+ sub = node.subscriptions[ body.first ]
681
+
682
+ expect( sub.event_type ).to eq( 'node.acked' )
683
+ end
684
+
685
+
686
+ it "adds a subscription for events which match a pattern if one is specified" do
687
+ criteria = { type: 'host' }
688
+
689
+ msg = Arborist::TreeAPI.encode( :subscribe, [criteria, {}] )
690
+
691
+ resmsg = nil
692
+ expect {
693
+ sock.send( msg )
694
+ resmsg = sock.recv
695
+ }.to change { manager.subscriptions.length }.by( 1 ).and(
696
+ change { manager.root.subscriptions.length }.by( 1 )
697
+ )
698
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
699
+ node = manager.subscriptions[ body.first ]
700
+ sub = node.subscriptions[ body.first ]
701
+
702
+ expect( sub.event_type ).to be_nil
703
+ expect( sub.criteria ).to eq({ 'type' => 'host' })
704
+ end
705
+
706
+
707
+ it "adds a subscription for events which don't match a pattern if an exclusion pattern is given" do
708
+ criteria = { type: 'host' }
709
+
710
+ msg = Arborist::TreeAPI.encode( :subscribe, [{}, criteria] )
711
+
712
+ resmsg = nil
713
+ expect {
714
+ sock.send( msg )
715
+ resmsg = sock.recv
716
+ }.to change { manager.subscriptions.length }.by( 1 ).and(
717
+ change { manager.root.subscriptions.length }.by( 1 )
718
+ )
719
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
720
+ node = manager.subscriptions[ body.first ]
721
+ sub = node.subscriptions[ body.first ]
722
+
723
+ expect( sub.event_type ).to be_nil
724
+ expect( sub.negative_criteria ).to eq({ 'type' => 'host' })
725
+ end
726
+
727
+ end
728
+
729
+
730
+ describe "unsubscribe" do
731
+
732
+ let( :subscription ) do
733
+ manager.create_subscription( nil, 'node.delta', {type: 'host'} )
734
+ end
735
+
736
+
737
+ it "removes the subscription with the specified ID" do
738
+ msg = Arborist::TreeAPI.encode( :unsubscribe, {subscription_id: subscription.id}, nil )
739
+
740
+ resmsg = nil
741
+ expect {
742
+ sock.send( msg )
743
+ resmsg = sock.recv
744
+ }.to change { manager.subscriptions.length }.by( -1 ).and(
745
+ change { manager.root.subscriptions.length }.by( -1 )
746
+ )
747
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
748
+
749
+ expect( body ).to include( 'event_type' => 'node.delta', 'criteria' => {'type' => 'host'} )
750
+ end
751
+
752
+
753
+ it "ignores unsubscription of a non-existant ID" do
754
+ msg = Arborist::TreeAPI.encode( :unsubscribe, {subscription_id: 'the bears!'}, nil )
755
+
756
+ resmsg = nil
757
+ expect {
758
+ sock.send( msg )
759
+ resmsg = sock.recv
760
+ }.to_not change { manager.subscriptions.length }
761
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
762
+
763
+ expect( body ).to be_nil
764
+ end
765
+
766
+ end
767
+
768
+
769
+ describe "prune" do
770
+
771
+ it "removes a single node" do
772
+ msg = Arborist::TreeAPI.encode( :prune, {identifier: 'duir-ssh'}, nil )
773
+ sock.send( msg )
774
+ resmsg = sock.recv
775
+
776
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
777
+ expect( hdr ).to include( 'success' => true )
778
+ expect( body ).to eq( true )
779
+ expect( manager.nodes ).to_not include( 'duir-ssh' )
780
+ end
781
+
782
+
783
+ it "returns Nil without error if the node to prune didn't exist" do
784
+ msg = Arborist::TreeAPI.encode( :prune, {identifier: 'shemp-ssh'}, nil )
785
+ sock.send( msg )
786
+ resmsg = sock.recv
787
+
788
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
789
+ expect( hdr ).to include( 'success' => true )
790
+ expect( body ).to be_nil
791
+ end
792
+
793
+
794
+ it "removes children nodes along with the parent" do
795
+ msg = Arborist::TreeAPI.encode( :prune, {identifier: 'duir'}, nil )
796
+ sock.send( msg )
797
+ resmsg = sock.recv
798
+
799
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
800
+ expect( hdr ).to include( 'success' => true )
801
+ expect( body ).to eq( true )
802
+ expect( manager.nodes ).to_not include( 'duir' )
803
+ expect( manager.nodes ).to_not include( 'duir-ssh' )
804
+ end
805
+
806
+
807
+ it "returns an error to the client when missing required attributes" do
808
+ msg = Arborist::TreeAPI.encode( :prune )
809
+ sock.send( msg )
810
+ resmsg = sock.recv
811
+
812
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
813
+ expect( hdr ).to include( 'success' => false )
814
+ expect( hdr['reason'] ).to match( /no identifier/i )
815
+ end
816
+ end
817
+
818
+
819
+ describe "graft" do
820
+
821
+ it "can add a node with no explicit parent" do
822
+ header = {
823
+ identifier: 'guenter',
824
+ type: 'host',
825
+ }
826
+ attributes = {
827
+ description: 'The evil penguin node of doom.',
828
+ addresses: ['10.2.66.8'],
829
+ tags: ['internal', 'football']
830
+ }
831
+ msg = Arborist::TreeAPI.encode( :graft, header, attributes )
832
+
833
+ sock.send( msg )
834
+ resmsg = sock.recv
835
+
836
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
837
+ expect( hdr ).to include( 'success' => true )
838
+ expect( body ).to eq( 'guenter' )
839
+
840
+ new_node = manager.nodes[ 'guenter' ]
841
+ expect( new_node ).to be_a( Arborist::Node::Host )
842
+ expect( new_node.identifier ).to eq( header[:identifier] )
843
+ expect( new_node.description ).to eq( attributes[:description] )
844
+ expect( new_node.addresses ).to eq([ IPAddr.new(attributes[:addresses].first) ])
845
+ expect( new_node.tags ).to include( *attributes[:tags] )
846
+ end
847
+
848
+
849
+ it "can add a node with a parent specified" do
850
+ header = {
851
+ identifier: 'orgalorg',
852
+ type: 'host',
853
+ parent: 'duir'
854
+ }
855
+ attributes = {
856
+ description: 'The true form of the evil penguin node of doom.',
857
+ addresses: ['192.168.22.8'],
858
+ tags: ['evil', 'space', 'entity']
859
+ }
860
+ msg = Arborist::TreeAPI.encode( :graft, header, attributes )
861
+
862
+ sock.send( msg )
863
+ resmsg = sock.recv
864
+
865
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
866
+ expect( hdr ).to include( 'success' => true )
867
+ expect( body ).to eq( 'orgalorg' )
868
+
869
+ new_node = manager.nodes[ 'orgalorg' ]
870
+ expect( new_node ).to be_a( Arborist::Node::Host )
871
+ expect( new_node.identifier ).to eq( header[:identifier] )
872
+ expect( new_node.parent ).to eq( header[:parent] )
873
+ expect( new_node.description ).to eq( attributes[:description] )
874
+ expect( new_node.addresses ).to eq([ IPAddr.new(attributes[:addresses].first) ])
875
+ expect( new_node.tags ).to include( *attributes[:tags] )
876
+ end
877
+
878
+
879
+ it "can add a subordinate node" do
880
+ header = {
881
+ identifier: 'echo',
882
+ type: 'service',
883
+ parent: 'duir'
884
+ }
885
+ attributes = {
886
+ description: 'Mmmmm AppleTalk.'
887
+ }
888
+ msg = Arborist::TreeAPI.encode( :graft, header, attributes )
889
+
890
+ sock.send( msg )
891
+ resmsg = sock.recv
892
+
893
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
894
+ expect( hdr ).to include( 'success' => true )
895
+ expect( body ).to eq( 'duir-echo' )
896
+
897
+ new_node = manager.nodes[ 'duir-echo' ]
898
+ expect( new_node ).to be_a( Arborist::Node::Service )
899
+ expect( new_node.identifier ).to eq( 'duir-echo' )
900
+ expect( new_node.parent ).to eq( header[:parent] )
901
+ expect( new_node.description ).to eq( attributes[:description] )
902
+ expect( new_node.port ).to eq( 7 )
903
+ expect( new_node.protocol ).to eq( 'tcp' )
904
+ expect( new_node.app_protocol ).to eq( 'echo' )
905
+ end
906
+
907
+
908
+ it "errors if adding a subordinate node with no parent" do
909
+ header = {
910
+ identifier: 'echo',
911
+ type: 'service'
912
+ }
913
+ attributes = {
914
+ description: 'Mmmmm AppleTalk.'
915
+ }
916
+ msg = Arborist::TreeAPI.encode( :graft, header, attributes )
917
+
918
+ sock.send( msg )
919
+ resmsg = sock.recv
920
+
921
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
922
+ expect( hdr ).to include( 'success' => false )
923
+ expect( hdr['reason'] ).to match( /no host given/i )
924
+ end
925
+
926
+ end
927
+
928
+
929
+ describe "modify" do
930
+
931
+ it "can change operational attributes of a node" do
932
+ header = {
933
+ identifier: 'sidonie',
934
+ }
935
+ attributes = {
936
+ parent: '_',
937
+ addresses: ['192.168.32.32', '10.2.2.28']
938
+ }
939
+ msg = Arborist::TreeAPI.encode( :modify, header, attributes )
940
+
941
+ sock.send( msg )
942
+ resmsg = sock.recv
943
+
944
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
945
+ expect( hdr ).to include( 'success' => true )
946
+
947
+ node = manager.nodes[ 'sidonie' ]
948
+ expect(
949
+ node.addresses
950
+ ).to eq( [IPAddr.new('192.168.32.32'), IPAddr.new('10.2.2.28')] )
951
+ expect( node.parent ).to eq( '_' )
952
+ end
953
+
954
+
955
+ it "ignores modifications to unsupported attributes" do
956
+ header = {
957
+ identifier: 'sidonie',
958
+ }
959
+ attributes = {
960
+ identifier: 'somethingelse'
961
+ }
962
+ msg = Arborist::TreeAPI.encode( :modify, header, attributes )
963
+
964
+ sock.send( msg )
965
+ resmsg = sock.recv
966
+
967
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
968
+ expect( hdr ).to include( 'success' => true )
969
+
970
+ expect( manager.nodes['sidonie'] ).to be_an( Arborist::Node )
971
+ expect( manager.nodes['sidonie'].identifier ).to eq( 'sidonie' )
972
+ end
973
+
974
+
975
+ it "errors on modifications to the root node" do
976
+ header = {
977
+ identifier: '_',
978
+ }
979
+ attributes = {
980
+ identifier: 'somethingelse'
981
+ }
982
+ msg = Arborist::TreeAPI.encode( :modify, header, attributes )
983
+
984
+ sock.send( msg )
985
+ resmsg = sock.recv
986
+
987
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
988
+ expect( hdr ).to include( 'success' => false )
989
+ expect( manager.nodes['_'].identifier ).to eq( '_' )
990
+ end
991
+
992
+
993
+ it "errors on modifications to nonexistent nodes" do
994
+ header = {
995
+ identifier: 'nopenopenope',
996
+ }
997
+ attributes = {
998
+ identifier: 'somethingelse'
999
+ }
1000
+ msg = Arborist::TreeAPI.encode( :modify, header, attributes )
1001
+
1002
+ sock.send( msg )
1003
+ resmsg = sock.recv
1004
+
1005
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
1006
+ expect( hdr ).to include( 'success' => false )
1007
+ end
1008
+ end
1009
+
1010
+ end
1011
+
1012
+
364
1013
  describe "tree traversal" do
365
1014
 
366
1015
  let( :tree ) do
@@ -589,77 +1238,204 @@ describe Arborist::Manager do
589
1238
  end
590
1239
 
591
1240
 
592
- describe "sockets" do
1241
+ end
1242
+
593
1243
 
594
- let( :zmq_context ) { Arborist.zmq_context }
595
- let( :zmq_loop ) { instance_double(ZMQ::Loop) }
596
- let( :tree_sock ) { instance_double(ZMQ::Socket::Rep, "tree API socket") }
597
- let( :event_sock ) { instance_double(ZMQ::Socket::Pub, "event socket") }
598
- let( :tree_pollitem ) { instance_double(ZMQ::Pollitem, "tree API pollitem") }
599
- let( :event_pollitem ) { instance_double(ZMQ::Pollitem, "event API pollitem") }
600
- let( :signal_timer ) { instance_double(ZMQ::Timer, "signal timer") }
1244
+ __END__
601
1245
 
1246
+ let( :socket ) { instance_double( ZMQ::Socket::Pub ) }
1247
+ let( :pollitem ) { instance_double( ZMQ::Pollitem, pollable: socket ) }
1248
+ let( :zloop ) { instance_double( ZMQ::Loop ) }
602
1249
 
603
- before( :each ) do
604
- allow( ZMQ::Loop ).to receive( :new ).and_return( zmq_loop )
1250
+ let( :manager ) { Arborist::Manager.new }
1251
+ let( :event ) { Arborist::Event.create(TestEvent, 'stuff') }
1252
+
1253
+ let( :publisher ) { described_class.new(pollitem, manager, zloop) }
1254
+
1255
+
1256
+ it "starts out registered for writing" do
1257
+ expect( publisher ).to be_registered
1258
+ end
1259
+
1260
+
1261
+ it "unregisters itself if told to write with an empty event queue" do
1262
+ expect( zloop ).to receive( :remove ).with( pollitem )
1263
+ expect {
1264
+ publisher.on_writable
1265
+ }.to change { publisher.registered? }.to( false )
1266
+ end
1267
+
1268
+
1269
+ it "registers itself if it's not already when an event is appended" do
1270
+ # Cause the socket to become unregistered
1271
+ allow( zloop ).to receive( :remove )
1272
+ publisher.on_writable
1273
+
1274
+ expect( zloop ).to receive( :register ).with( pollitem )
1275
+
1276
+ expect {
1277
+ publisher.publish( 'identifier-00aa', event )
1278
+ }.to change { publisher.registered? }.to( true )
1279
+ end
1280
+
1281
+
1282
+ it "publishes events with their identifier" do
1283
+ identifier = '65b2430b-6855-4961-ab46-d742cf4456a1'
1284
+
1285
+ expect( socket ).to receive( :sendm ).with( identifier )
1286
+ expect( socket ).to receive( :send ) do |raw_data|
1287
+ ev = MessagePack.unpack( raw_data )
1288
+ expect( ev ).to include( 'type', 'data' )
1289
+
1290
+ expect( ev['type'] ).to eq( 'test.event' )
1291
+ expect( ev['data'] ).to eq( 'stuff' )
1292
+ end
1293
+ expect( zloop ).to receive( :remove ).with( pollitem )
1294
+
1295
+ publisher.publish( identifier, event )
1296
+ publisher.on_writable
1297
+ end
1298
+
1299
+
1300
+
1301
+ let( :manager ) { @manager }
605
1302
 
606
- allow( zmq_context ).to receive( :socket ).with( :REP ).and_return( tree_sock )
607
- allow( zmq_context ).to receive( :socket ).with( :PUB ).and_return( event_sock )
1303
+ let!( :sock ) do
1304
+ sock = Arborist.zmq_context.socket( :REQ )
1305
+ sock.linger = 0
1306
+ sock.connect( TESTING_API_SOCK )
1307
+ sock
1308
+ end
1309
+
1310
+ let( :api_handler ) { described_class.new( rep_sock, manager ) }
1311
+
1312
+
1313
+ describe "malformed requests" do
1314
+
1315
+ it "send an error response if the request can't be deserialized" do
1316
+ sock.send( "whatevs, dude!" )
1317
+ resmsg = sock.recv
1318
+
1319
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
1320
+ expect( hdr ).to include(
1321
+ 'success' => false,
1322
+ 'reason' => /invalid request/i,
1323
+ 'category' => 'client'
1324
+ )
1325
+ expect( body ).to be_nil
1326
+ end
1327
+
1328
+
1329
+ it "send an error response if the request isn't a tuple" do
1330
+ sock.send( MessagePack.pack({ version: 1, action: 'list' }) )
1331
+ resmsg = sock.recv
1332
+
1333
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
1334
+ expect( hdr ).to include(
1335
+ 'success' => false,
1336
+ 'reason' => /invalid request.*not a tuple/i,
1337
+ 'category' => 'client'
1338
+ )
1339
+ expect( body ).to be_nil
1340
+ end
1341
+
1342
+
1343
+ it "send an error response if the request is empty" do
1344
+ sock.send( MessagePack.pack([]) )
1345
+ resmsg = sock.recv
1346
+
1347
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
1348
+ expect( hdr ).to include(
1349
+ 'success' => false,
1350
+ 'reason' => /invalid request.*incorrect length/i,
1351
+ 'category' => 'client'
1352
+ )
1353
+ expect( body ).to be_nil
1354
+ end
608
1355
 
609
- allow( zmq_loop ).to receive( :verbose= )
610
- allow( zmq_loop ).to receive( :remove ).with( tree_pollitem )
611
- allow( zmq_loop ).to receive( :remove ).with( event_pollitem )
612
1356
 
613
- allow( tree_pollitem ).to receive( :pollable ).and_return( tree_sock )
614
- allow( tree_sock ).to receive( :close )
615
- allow( event_pollitem ).to receive( :pollable ).and_return( event_sock )
616
- allow( event_sock ).to receive( :close )
1357
+ it "send an error response if the request is an incorrect length" do
1358
+ sock.send( MessagePack.pack([{}, {}, {}]) )
1359
+ resmsg = sock.recv
617
1360
 
618
- allow( tree_sock ).to receive( :bind ).with( Arborist.tree_api_url )
619
- allow( tree_sock ).to receive( :linger= )
1361
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
1362
+ expect( hdr ).to include(
1363
+ 'success' => false,
1364
+ 'reason' => /invalid request.*incorrect length/i,
1365
+ 'category' => 'client'
1366
+ )
1367
+ expect( body ).to be_nil
1368
+ end
620
1369
 
621
- allow( event_sock ).to receive( :bind ).with( Arborist.event_api_url )
622
- allow( event_sock ).to receive( :linger= )
623
1370
 
624
- allow( ZMQ::Pollitem ).to receive( :new ).with( tree_sock, ZMQ::POLLIN|ZMQ::POLLOUT ).
625
- and_return( tree_pollitem )
626
- allow( ZMQ::Pollitem ).to receive( :new ).with( event_sock, ZMQ::POLLOUT ).
627
- and_return( event_pollitem )
1371
+ it "send an error response if the request's header is not a Map" do
1372
+ sock.send( MessagePack.pack([nil, {}]) )
1373
+ resmsg = sock.recv
628
1374
 
629
- allow( tree_pollitem ).to receive( :handler= ).
630
- with( an_instance_of(Arborist::Manager::TreeAPI) )
631
- allow( zmq_loop ).to receive( :register ).with( tree_pollitem )
632
- allow( event_pollitem ).to receive( :handler= ).
633
- with( an_instance_of(Arborist::Manager::EventPublisher) )
634
- allow( zmq_loop ).to receive( :register ).with( event_pollitem )
1375
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
1376
+ expect( hdr ).to include(
1377
+ 'success' => false,
1378
+ 'reason' => /invalid request.*header is not a map/i,
1379
+ 'category' => 'client'
1380
+ )
1381
+ expect( body ).to be_nil
635
1382
  end
636
1383
 
637
1384
 
638
- it "starts handling signals and events when started" do
639
- expect( ZMQ::Timer ).to receive( :new ).
640
- with( described_class::SIGNAL_INTERVAL, 0, manager.method(:process_signal_queue) ).
641
- and_return( signal_timer )
642
- expect( zmq_loop ).to receive( :register_timer ).with( signal_timer )
643
- expect( zmq_loop ).to receive( :register_timer ).with( manager.heartbeat_timer )
644
- expect( zmq_loop ).to receive( :start )
1385
+ it "send an error response if the request's body is not Nil, a Map, or an Array of Maps" do
1386
+ sock.send( MessagePack.pack([{version: 1, action: 'list'}, 18]) )
1387
+ resmsg = sock.recv
1388
+
1389
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
1390
+ expect( hdr ).to include(
1391
+ 'success' => false,
1392
+ 'reason' => /invalid request.*body must be nil, a map, or an array of maps/i,
1393
+ 'category' => 'client'
1394
+ )
1395
+ expect( body ).to be_nil
1396
+ end
1397
+
645
1398
 
646
- expect( zmq_loop ).to receive( :remove ).with( tree_pollitem )
647
- expect( zmq_loop ).to receive( :remove ).with( event_pollitem )
1399
+ it "send an error response if missing a version" do
1400
+ sock.send( MessagePack.pack([{action: 'list'}]) )
1401
+ resmsg = sock.recv
648
1402
 
649
- manager.run
1403
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
1404
+ expect( hdr ).to include(
1405
+ 'success' => false,
1406
+ 'reason' => /invalid request.*missing required header 'version'/i,
1407
+ 'category' => 'client'
1408
+ )
1409
+ expect( body ).to be_nil
1410
+ end
650
1411
 
651
- expect( manager.event_publisher.event_queue.length ).to eq( 1 )
652
1412
 
653
- event = manager.event_publisher.event_queue.first
654
- expect( event.first ).to eq( 'sys.startup' )
1413
+ it "send an error response if missing an action" do
1414
+ sock.send( MessagePack.pack([{version: 1}]) )
1415
+ resmsg = sock.recv
655
1416
 
656
- payload = unpack_message( event.last )
657
- expect( payload ).to include(
658
- 'start_time' => an_instance_of(String),
659
- 'version' => an_instance_of(String)
1417
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
1418
+ expect( hdr ).to include(
1419
+ 'success' => false,
1420
+ 'reason' => /invalid request.*missing required header 'action'/i,
1421
+ 'category' => 'client'
660
1422
  )
1423
+ expect( body ).to be_nil
661
1424
  end
662
1425
 
1426
+
1427
+ it "send an error response for unknown actions" do
1428
+ badmsg = Arborist::TreeAPI.encode( :slap )
1429
+ sock.send( badmsg )
1430
+ resmsg = sock.recv
1431
+
1432
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
1433
+ expect( hdr ).to include(
1434
+ 'success' => false,
1435
+ 'reason' => /invalid request.*no such action 'slap'/i,
1436
+ 'category' => 'client'
1437
+ )
1438
+ expect( body ).to be_nil
1439
+ end
663
1440
  end
664
- end
665
1441