arborist 0.1.0 → 0.2.0.pre20170519125456

Sign up to get free protection for your applications and to get access to all the features.
@@ -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