spec/arborist/manager_spec.rb in arborist-0.1.0 vs spec/arborist/manager_spec.rb in arborist-0.2.0.pre20170519125456
- old
+ new
@@ -1,9 +1,10 @@
#!/usr/bin/env rspec -cfd
require_relative '../spec_helper'
+require 'tmpdir'
require 'timecop'
require 'arborist/manager'
require 'arborist/node/host'
describe Arborist::Manager do
@@ -123,12 +124,12 @@
it "restores the state of loaded nodes if the state file is configured" do
_ = manager
- statefile = Pathname( './arborist.tree' )
- Arborist::Manager.state_file = statefile
+ Arborist::Manager.state_file = './arborist.tree'
+ statefile = Arborist::Manager.state_file
state_file_io = instance_double( File )
saved_router_node = Marshal.load( Marshal.dump(router_node) )
saved_router_node.instance_variable_set( :@status, 'up' )
saved_host_node = Marshal.load( Marshal.dump(host_node) )
@@ -151,31 +152,34 @@
it "doesn't error if the configured state file isn't readable" do
_ = manager
- statefile = Pathname( './arborist.tree' )
- Arborist::Manager.state_file = statefile
+ Arborist::Manager.state_file = './arborist.tree'
- expect( statefile ).to receive( :readable? ).and_return( false )
- expect( statefile ).to_not receive( :open )
+ expect( Arborist::Manager.state_file ).to receive( :readable? ).and_return( false )
+ expect( Arborist::Manager.state_file ).to_not receive( :open )
expect( manager.restore_node_states ).to be_falsey
end
it "checkpoints the state file periodically if an interval is configured" do
- described_class.configure( checkpoint_frequency: 20_000, state_file: 'arb.tree' )
+ statefile = Pathname( Dir.tmpdir ) + Dir::Tmpname.make_tmpname( 'arb', 'tree' )
+ described_class.configure( checkpoint_frequency: 20_000, state_file: statefile )
- zloop = instance_double( ZMQ::Loop, register: nil, :verbose= => nil )
- timer = instance_double( ZMQ::Timer, "checkpoint timer" )
- expect( ZMQ::Loop ).to receive( :new ).and_return( zloop )
- allow( ZMQ::Timer ).to receive( :new ).and_call_original
- expect( ZMQ::Timer ).to receive( :new ).with( 20.0, 0 ).and_return( timer )
-
manager = described_class.new
- expect( manager.checkpoint_timer ).to eq( timer )
+ manager.register_checkpoint_timer
+ expect( manager.checkpoint_timer ).to be_a( Timers::Timer )
+ expect( statefile ).to_not exist
+
+ manager.checkpoint_timer.fire
+ expect( statefile ).to exist
+ states = Marshal.load( statefile.open('r:binary') )
+
+ expect( states ).to be_a( Hash )
+ expect( states.keys ).to eq( manager.nodes.keys )
end
it "doesn't checkpoint if no interval is configured" do
described_class.configure( manager: {checkpoint_frequency: nil, state_file: 'arb.tree'} )
@@ -202,11 +206,11 @@
context "heartbeat event" do
it "errors if configured with a heartbeat of 0" do
expect {
described_class.configure( heartbeat_frequency: 0 )
- }.to raise_error( Arborist::ConfigError, /positive non-zero/i )
+ }.to raise_error( Arborist::ConfigError, /positive and non-zero/i )
end
it "is sent at the configured interval"
@@ -359,10 +363,655 @@
end
end
+ xdescribe "tree API", :testing_manager do
+
+ before( :each ) do
+ @manager = nil
+ @manager_thread = Thread.new do
+ @manager = make_testing_manager()
+ Thread.current.abort_on_exception = true
+ @manager.run
+ Loggability[ Arborist ].info "Stopped the test manager"
+ end
+
+ count = 0
+ until (@manager && @manager.running?) || count > 30
+ sleep 0.1
+ count += 1
+ end
+ raise "Manager didn't start up" unless @manager.running?
+ end
+
+ after( :each ) do
+ @manager.simulate_signal( :TERM )
+ unless @manager_thread.join( 5 )
+ $stderr.puts "Manager thread didn't exit on its own; killing it."
+ @manager_thread.kill
+ end
+
+ count = 0
+ while @manager.running? || count > 30
+ sleep 0.1
+ Loggability[ Arborist ].info "Manager still running"
+ count += 1
+ end
+ raise "Manager didn't stop" if @manager.running?
+ end
+
+
+ describe "status" do
+
+
+ it "returns a Map describing the manager and its state" do
+ msg = Arborist::TreeAPI.encode( :status )
+
+ sock.send( msg )
+ resmsg = sock.recv
+
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
+ expect( hdr ).to include( 'success' => true )
+ expect( body.length ).to eq( 4 )
+ expect( body ).to include( 'server_version', 'state', 'uptime', 'nodecount' )
+ end
+
+ end
+
+
+ describe "fetch" do
+
+ it "returns an array of full state maps for nodes matching specified criteria" do
+ msg = Arborist::TreeAPI.encode( :fetch, type: 'service', port: 22 )
+
+ sock.send( msg )
+ resmsg = sock.recv
+
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
+ expect( hdr ).to include( 'success' => true )
+
+ expect( body ).to be_a( Hash )
+ expect( body.length ).to eq( 3 )
+
+ expect( body.values ).to all( be_a(Hash) )
+ expect( body.values ).to all( include('status', 'type') )
+ end
+
+
+ it "returns an array of full state maps for nodes not matching specified negative criteria" do
+ msg = Arborist::TreeAPI.encode( :fetch, [ {}, {type: 'service', port: 22} ] )
+
+ sock.send( msg )
+ resmsg = sock.recv
+
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
+ expect( hdr ).to include( 'success' => true )
+
+ expect( body ).to be_a( Hash )
+ expect( body.length ).to eq( manager.nodes.length - 3 )
+
+ expect( body.values ).to all( be_a(Hash) )
+ expect( body.values ).to all( include('status', 'type') )
+ end
+
+
+ it "returns an array of full state maps for nodes combining positive and negative criteria" do
+ msg = Arborist::TreeAPI.encode( :fetch, [ {type: 'service'}, {port: 22} ] )
+
+ sock.send( msg )
+ resmsg = sock.recv
+
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
+ expect( hdr ).to include( 'success' => true )
+
+ expect( body ).to be_a( Hash )
+ expect( body.length ).to eq( 16 )
+
+ expect( body.values ).to all( be_a(Hash) )
+ expect( body.values ).to all( include('status', 'type') )
+ end
+
+
+ it "doesn't return nodes beneath downed nodes by default" do
+ manager.nodes['sidonie'].update( error: 'sunspots' )
+ msg = Arborist::TreeAPI.encode( :fetch, type: 'service', port: 22 )
+
+ sock.send( msg )
+ resmsg = sock.recv
+
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
+ expect( hdr ).to include( 'success' => true )
+ expect( body ).to be_a( Hash )
+ expect( body.length ).to eq( 2 )
+ expect( body ).to include( 'duir-ssh', 'yevaud-ssh' )
+ end
+
+
+ it "does return nodes beneath downed nodes if asked to" do
+ manager.nodes['sidonie'].update( error: 'plague of locusts' )
+ msg = Arborist::TreeAPI.encode( :fetch, {include_down: true}, type: 'service', port: 22 )
+
+ sock.send( msg )
+ resmsg = sock.recv
+
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
+ expect( hdr ).to include( 'success' => true )
+ expect( body ).to be_a( Hash )
+ expect( body.length ).to eq( 3 )
+ expect( body ).to include( 'duir-ssh', 'yevaud-ssh', 'sidonie-ssh' )
+ end
+
+
+ it "returns only identifiers if the `return` header is set to `nil`" do
+ msg = Arborist::TreeAPI.encode( :fetch, {return: nil}, type: 'service', port: 22 )
+
+ sock.send( msg )
+ resmsg = sock.recv
+
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
+ expect( hdr ).to include( 'success' => true )
+ expect( body ).to be_a( Hash )
+ expect( body.length ).to eq( 3 )
+ expect( body ).to include( 'duir-ssh', 'yevaud-ssh', 'sidonie-ssh' )
+ expect( body.values ).to all( be_empty )
+ end
+
+
+ it "returns only specified state if the `return` header is set to an Array of keys" do
+ msg = Arborist::TreeAPI.encode( :fetch, {return: %w[status tags addresses]},
+ type: 'service', port: 22 )
+
+ sock.send( msg )
+ resmsg = sock.recv
+
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
+ expect( hdr ).to include( 'success' => true )
+ expect( body.length ).to eq( 3 )
+ expect( body ).to include( 'duir-ssh', 'yevaud-ssh', 'sidonie-ssh' )
+ expect( body.values.map(&:keys) ).to all( contain_exactly('status', 'tags', 'addresses') )
+ end
+
+
+ end
+
+
+ describe "list" do
+
+ it "returns an array of node state" do
+ msg = Arborist::TreeAPI.encode( :list )
+ sock.send( msg )
+ resmsg = sock.recv
+
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
+ expect( hdr ).to include( 'success' => true )
+ expect( body.length ).to eq( manager.nodes.length )
+ expect( body ).to all( be_a(Hash) )
+ expect( body ).to include( hash_including('identifier' => '_') )
+ expect( body ).to include( hash_including('identifier' => 'duir') )
+ expect( body ).to include( hash_including('identifier' => 'sidonie') )
+ expect( body ).to include( hash_including('identifier' => 'sidonie-ssh') )
+ expect( body ).to include( hash_including('identifier' => 'sidonie-demon-http') )
+ expect( body ).to include( hash_including('identifier' => 'yevaud') )
+ end
+
+ it "can be limited by depth" do
+ msg = Arborist::TreeAPI.encode( :list, {depth: 1}, nil )
+ sock.send( msg )
+ resmsg = sock.recv
+
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
+ expect( hdr ).to include( 'success' => true )
+ expect( body.length ).to eq( 3 )
+ expect( body ).to all( be_a(Hash) )
+ expect( body ).to include( hash_including('identifier' => '_') )
+ expect( body ).to include( hash_including('identifier' => 'duir') )
+ expect( body ).to_not include( hash_including('identifier' => 'duir-ssh') )
+ end
+ end
+
+
+ describe "update" do
+
+ it "merges the properties sent with those of the targeted nodes" do
+ update_data = {
+ duir: {
+ ping: {
+ rtt: 254
+ }
+ },
+ sidonie: {
+ ping: {
+ rtt: 1208
+ }
+ },
+ yevaud: {
+ ping: {
+ rtt: 843
+ }
+ }
+ }
+ msg = Arborist::TreeAPI.encode( :update, update_data )
+ sock.send( msg )
+ resmsg = sock.recv
+
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
+ expect( hdr ).to include( 'success' => true )
+ expect( body ).to be_nil
+
+ expect( manager.nodes['duir'].properties['ping'] ).to include( 'rtt' => 254 )
+ expect( manager.nodes['sidonie'].properties['ping'] ).to include( 'rtt' => 1208 )
+ expect( manager.nodes['yevaud'].properties['ping'] ).to include( 'rtt' => 843 )
+ end
+
+
+ it "ignores unknown identifiers" do
+ msg = Arborist::TreeAPI.encode( :update, charlie_humperton: {ping: { rtt: 8 }} )
+ sock.send( msg )
+ resmsg = sock.recv
+
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
+ expect( hdr ).to include( 'success' => true )
+ end
+
+ it "fails with a client error if the body is invalid" do
+ msg = Arborist::TreeAPI.encode( :update, nil )
+ sock.send( msg )
+ resmsg = sock.recv
+
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
+ expect( hdr ).to include( 'success' => false )
+ expect( hdr['reason'] ).to match( /respond to #each/ )
+ end
+ end
+
+
+ describe "subscribe" do
+
+ it "adds a subscription for all event types to the root node by default" do
+ msg = Arborist::TreeAPI.encode( :subscribe, [{}, {}] )
+
+ resmsg = nil
+ expect {
+ sock.send( msg )
+ resmsg = sock.recv
+ }.to change { manager.subscriptions.length }.by( 1 ).and(
+ change { manager.root.subscriptions.length }.by( 1 )
+ )
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
+
+ sub_id = manager.subscriptions.keys.first
+
+ expect( hdr ).to include( 'success' => true )
+ expect( body ).to eq([ sub_id ])
+ end
+
+
+ it "adds a subscription to the specified node if an identifier is specified" do
+ msg = Arborist::TreeAPI.encode( :subscribe, {identifier: 'sidonie'}, [{}, {}] )
+
+ resmsg = nil
+ expect {
+ sock.send( msg )
+ resmsg = sock.recv
+ }.to change { manager.subscriptions.length }.by( 1 ).and(
+ change { manager.nodes['sidonie'].subscriptions.length }.by( 1 )
+ )
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
+
+ sub_id = manager.subscriptions.keys.first
+
+ expect( hdr ).to include( 'success' => true )
+ expect( body ).to eq([ sub_id ])
+ end
+
+
+ it "adds a subscription for particular event types if one is specified" do
+ msg = Arborist::TreeAPI.encode( :subscribe, {event_type: 'node.acked'}, [{}, {}] )
+
+ resmsg = nil
+ expect {
+ sock.send( msg )
+ resmsg = sock.recv
+ }.to change { manager.subscriptions.length }.by( 1 ).and(
+ change { manager.root.subscriptions.length }.by( 1 )
+ )
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
+ node = manager.subscriptions[ body.first ]
+ sub = node.subscriptions[ body.first ]
+
+ expect( sub.event_type ).to eq( 'node.acked' )
+ end
+
+
+ it "adds a subscription for events which match a pattern if one is specified" do
+ criteria = { type: 'host' }
+
+ msg = Arborist::TreeAPI.encode( :subscribe, [criteria, {}] )
+
+ resmsg = nil
+ expect {
+ sock.send( msg )
+ resmsg = sock.recv
+ }.to change { manager.subscriptions.length }.by( 1 ).and(
+ change { manager.root.subscriptions.length }.by( 1 )
+ )
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
+ node = manager.subscriptions[ body.first ]
+ sub = node.subscriptions[ body.first ]
+
+ expect( sub.event_type ).to be_nil
+ expect( sub.criteria ).to eq({ 'type' => 'host' })
+ end
+
+
+ it "adds a subscription for events which don't match a pattern if an exclusion pattern is given" do
+ criteria = { type: 'host' }
+
+ msg = Arborist::TreeAPI.encode( :subscribe, [{}, criteria] )
+
+ resmsg = nil
+ expect {
+ sock.send( msg )
+ resmsg = sock.recv
+ }.to change { manager.subscriptions.length }.by( 1 ).and(
+ change { manager.root.subscriptions.length }.by( 1 )
+ )
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
+ node = manager.subscriptions[ body.first ]
+ sub = node.subscriptions[ body.first ]
+
+ expect( sub.event_type ).to be_nil
+ expect( sub.negative_criteria ).to eq({ 'type' => 'host' })
+ end
+
+ end
+
+
+ describe "unsubscribe" do
+
+ let( :subscription ) do
+ manager.create_subscription( nil, 'node.delta', {type: 'host'} )
+ end
+
+
+ it "removes the subscription with the specified ID" do
+ msg = Arborist::TreeAPI.encode( :unsubscribe, {subscription_id: subscription.id}, nil )
+
+ resmsg = nil
+ expect {
+ sock.send( msg )
+ resmsg = sock.recv
+ }.to change { manager.subscriptions.length }.by( -1 ).and(
+ change { manager.root.subscriptions.length }.by( -1 )
+ )
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
+
+ expect( body ).to include( 'event_type' => 'node.delta', 'criteria' => {'type' => 'host'} )
+ end
+
+
+ it "ignores unsubscription of a non-existant ID" do
+ msg = Arborist::TreeAPI.encode( :unsubscribe, {subscription_id: 'the bears!'}, nil )
+
+ resmsg = nil
+ expect {
+ sock.send( msg )
+ resmsg = sock.recv
+ }.to_not change { manager.subscriptions.length }
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
+
+ expect( body ).to be_nil
+ end
+
+ end
+
+
+ describe "prune" do
+
+ it "removes a single node" do
+ msg = Arborist::TreeAPI.encode( :prune, {identifier: 'duir-ssh'}, nil )
+ sock.send( msg )
+ resmsg = sock.recv
+
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
+ expect( hdr ).to include( 'success' => true )
+ expect( body ).to eq( true )
+ expect( manager.nodes ).to_not include( 'duir-ssh' )
+ end
+
+
+ it "returns Nil without error if the node to prune didn't exist" do
+ msg = Arborist::TreeAPI.encode( :prune, {identifier: 'shemp-ssh'}, nil )
+ sock.send( msg )
+ resmsg = sock.recv
+
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
+ expect( hdr ).to include( 'success' => true )
+ expect( body ).to be_nil
+ end
+
+
+ it "removes children nodes along with the parent" do
+ msg = Arborist::TreeAPI.encode( :prune, {identifier: 'duir'}, nil )
+ sock.send( msg )
+ resmsg = sock.recv
+
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
+ expect( hdr ).to include( 'success' => true )
+ expect( body ).to eq( true )
+ expect( manager.nodes ).to_not include( 'duir' )
+ expect( manager.nodes ).to_not include( 'duir-ssh' )
+ end
+
+
+ it "returns an error to the client when missing required attributes" do
+ msg = Arborist::TreeAPI.encode( :prune )
+ sock.send( msg )
+ resmsg = sock.recv
+
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
+ expect( hdr ).to include( 'success' => false )
+ expect( hdr['reason'] ).to match( /no identifier/i )
+ end
+ end
+
+
+ describe "graft" do
+
+ it "can add a node with no explicit parent" do
+ header = {
+ identifier: 'guenter',
+ type: 'host',
+ }
+ attributes = {
+ description: 'The evil penguin node of doom.',
+ addresses: ['10.2.66.8'],
+ tags: ['internal', 'football']
+ }
+ msg = Arborist::TreeAPI.encode( :graft, header, attributes )
+
+ sock.send( msg )
+ resmsg = sock.recv
+
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
+ expect( hdr ).to include( 'success' => true )
+ expect( body ).to eq( 'guenter' )
+
+ new_node = manager.nodes[ 'guenter' ]
+ expect( new_node ).to be_a( Arborist::Node::Host )
+ expect( new_node.identifier ).to eq( header[:identifier] )
+ expect( new_node.description ).to eq( attributes[:description] )
+ expect( new_node.addresses ).to eq([ IPAddr.new(attributes[:addresses].first) ])
+ expect( new_node.tags ).to include( *attributes[:tags] )
+ end
+
+
+ it "can add a node with a parent specified" do
+ header = {
+ identifier: 'orgalorg',
+ type: 'host',
+ parent: 'duir'
+ }
+ attributes = {
+ description: 'The true form of the evil penguin node of doom.',
+ addresses: ['192.168.22.8'],
+ tags: ['evil', 'space', 'entity']
+ }
+ msg = Arborist::TreeAPI.encode( :graft, header, attributes )
+
+ sock.send( msg )
+ resmsg = sock.recv
+
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
+ expect( hdr ).to include( 'success' => true )
+ expect( body ).to eq( 'orgalorg' )
+
+ new_node = manager.nodes[ 'orgalorg' ]
+ expect( new_node ).to be_a( Arborist::Node::Host )
+ expect( new_node.identifier ).to eq( header[:identifier] )
+ expect( new_node.parent ).to eq( header[:parent] )
+ expect( new_node.description ).to eq( attributes[:description] )
+ expect( new_node.addresses ).to eq([ IPAddr.new(attributes[:addresses].first) ])
+ expect( new_node.tags ).to include( *attributes[:tags] )
+ end
+
+
+ it "can add a subordinate node" do
+ header = {
+ identifier: 'echo',
+ type: 'service',
+ parent: 'duir'
+ }
+ attributes = {
+ description: 'Mmmmm AppleTalk.'
+ }
+ msg = Arborist::TreeAPI.encode( :graft, header, attributes )
+
+ sock.send( msg )
+ resmsg = sock.recv
+
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
+ expect( hdr ).to include( 'success' => true )
+ expect( body ).to eq( 'duir-echo' )
+
+ new_node = manager.nodes[ 'duir-echo' ]
+ expect( new_node ).to be_a( Arborist::Node::Service )
+ expect( new_node.identifier ).to eq( 'duir-echo' )
+ expect( new_node.parent ).to eq( header[:parent] )
+ expect( new_node.description ).to eq( attributes[:description] )
+ expect( new_node.port ).to eq( 7 )
+ expect( new_node.protocol ).to eq( 'tcp' )
+ expect( new_node.app_protocol ).to eq( 'echo' )
+ end
+
+
+ it "errors if adding a subordinate node with no parent" do
+ header = {
+ identifier: 'echo',
+ type: 'service'
+ }
+ attributes = {
+ description: 'Mmmmm AppleTalk.'
+ }
+ msg = Arborist::TreeAPI.encode( :graft, header, attributes )
+
+ sock.send( msg )
+ resmsg = sock.recv
+
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
+ expect( hdr ).to include( 'success' => false )
+ expect( hdr['reason'] ).to match( /no host given/i )
+ end
+
+ end
+
+
+ describe "modify" do
+
+ it "can change operational attributes of a node" do
+ header = {
+ identifier: 'sidonie',
+ }
+ attributes = {
+ parent: '_',
+ addresses: ['192.168.32.32', '10.2.2.28']
+ }
+ msg = Arborist::TreeAPI.encode( :modify, header, attributes )
+
+ sock.send( msg )
+ resmsg = sock.recv
+
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
+ expect( hdr ).to include( 'success' => true )
+
+ node = manager.nodes[ 'sidonie' ]
+ expect(
+ node.addresses
+ ).to eq( [IPAddr.new('192.168.32.32'), IPAddr.new('10.2.2.28')] )
+ expect( node.parent ).to eq( '_' )
+ end
+
+
+ it "ignores modifications to unsupported attributes" do
+ header = {
+ identifier: 'sidonie',
+ }
+ attributes = {
+ identifier: 'somethingelse'
+ }
+ msg = Arborist::TreeAPI.encode( :modify, header, attributes )
+
+ sock.send( msg )
+ resmsg = sock.recv
+
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
+ expect( hdr ).to include( 'success' => true )
+
+ expect( manager.nodes['sidonie'] ).to be_an( Arborist::Node )
+ expect( manager.nodes['sidonie'].identifier ).to eq( 'sidonie' )
+ end
+
+
+ it "errors on modifications to the root node" do
+ header = {
+ identifier: '_',
+ }
+ attributes = {
+ identifier: 'somethingelse'
+ }
+ msg = Arborist::TreeAPI.encode( :modify, header, attributes )
+
+ sock.send( msg )
+ resmsg = sock.recv
+
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
+ expect( hdr ).to include( 'success' => false )
+ expect( manager.nodes['_'].identifier ).to eq( '_' )
+ end
+
+
+ it "errors on modifications to nonexistent nodes" do
+ header = {
+ identifier: 'nopenopenope',
+ }
+ attributes = {
+ identifier: 'somethingelse'
+ }
+ msg = Arborist::TreeAPI.encode( :modify, header, attributes )
+
+ sock.send( msg )
+ resmsg = sock.recv
+
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
+ expect( hdr ).to include( 'success' => false )
+ end
+ end
+
+ end
+
+
describe "tree traversal" do
let( :tree ) do
# router
# host_a host_b host_c
@@ -587,79 +1236,206 @@
end
end
- describe "sockets" do
+end
- let( :zmq_context ) { Arborist.zmq_context }
- let( :zmq_loop ) { instance_double(ZMQ::Loop) }
- let( :tree_sock ) { instance_double(ZMQ::Socket::Rep, "tree API socket") }
- let( :event_sock ) { instance_double(ZMQ::Socket::Pub, "event socket") }
- let( :tree_pollitem ) { instance_double(ZMQ::Pollitem, "tree API pollitem") }
- let( :event_pollitem ) { instance_double(ZMQ::Pollitem, "event API pollitem") }
- let( :signal_timer ) { instance_double(ZMQ::Timer, "signal timer") }
+__END__
- before( :each ) do
- allow( ZMQ::Loop ).to receive( :new ).and_return( zmq_loop )
+ let( :socket ) { instance_double( ZMQ::Socket::Pub ) }
+ let( :pollitem ) { instance_double( ZMQ::Pollitem, pollable: socket ) }
+ let( :zloop ) { instance_double( ZMQ::Loop ) }
- allow( zmq_context ).to receive( :socket ).with( :REP ).and_return( tree_sock )
- allow( zmq_context ).to receive( :socket ).with( :PUB ).and_return( event_sock )
+ let( :manager ) { Arborist::Manager.new }
+ let( :event ) { Arborist::Event.create(TestEvent, 'stuff') }
- allow( zmq_loop ).to receive( :verbose= )
- allow( zmq_loop ).to receive( :remove ).with( tree_pollitem )
- allow( zmq_loop ).to receive( :remove ).with( event_pollitem )
+ let( :publisher ) { described_class.new(pollitem, manager, zloop) }
- allow( tree_pollitem ).to receive( :pollable ).and_return( tree_sock )
- allow( tree_sock ).to receive( :close )
- allow( event_pollitem ).to receive( :pollable ).and_return( event_sock )
- allow( event_sock ).to receive( :close )
- allow( tree_sock ).to receive( :bind ).with( Arborist.tree_api_url )
- allow( tree_sock ).to receive( :linger= )
+ it "starts out registered for writing" do
+ expect( publisher ).to be_registered
+ end
- allow( event_sock ).to receive( :bind ).with( Arborist.event_api_url )
- allow( event_sock ).to receive( :linger= )
- allow( ZMQ::Pollitem ).to receive( :new ).with( tree_sock, ZMQ::POLLIN|ZMQ::POLLOUT ).
- and_return( tree_pollitem )
- allow( ZMQ::Pollitem ).to receive( :new ).with( event_sock, ZMQ::POLLOUT ).
- and_return( event_pollitem )
+ it "unregisters itself if told to write with an empty event queue" do
+ expect( zloop ).to receive( :remove ).with( pollitem )
+ expect {
+ publisher.on_writable
+ }.to change { publisher.registered? }.to( false )
+ end
- allow( tree_pollitem ).to receive( :handler= ).
- with( an_instance_of(Arborist::Manager::TreeAPI) )
- allow( zmq_loop ).to receive( :register ).with( tree_pollitem )
- allow( event_pollitem ).to receive( :handler= ).
- with( an_instance_of(Arborist::Manager::EventPublisher) )
- allow( zmq_loop ).to receive( :register ).with( event_pollitem )
+
+ it "registers itself if it's not already when an event is appended" do
+ # Cause the socket to become unregistered
+ allow( zloop ).to receive( :remove )
+ publisher.on_writable
+
+ expect( zloop ).to receive( :register ).with( pollitem )
+
+ expect {
+ publisher.publish( 'identifier-00aa', event )
+ }.to change { publisher.registered? }.to( true )
+ end
+
+
+ it "publishes events with their identifier" do
+ identifier = '65b2430b-6855-4961-ab46-d742cf4456a1'
+
+ expect( socket ).to receive( :sendm ).with( identifier )
+ expect( socket ).to receive( :send ) do |raw_data|
+ ev = MessagePack.unpack( raw_data )
+ expect( ev ).to include( 'type', 'data' )
+
+ expect( ev['type'] ).to eq( 'test.event' )
+ expect( ev['data'] ).to eq( 'stuff' )
end
+ expect( zloop ).to receive( :remove ).with( pollitem )
+ publisher.publish( identifier, event )
+ publisher.on_writable
+ end
- it "starts handling signals and events when started" do
- expect( ZMQ::Timer ).to receive( :new ).
- with( described_class::SIGNAL_INTERVAL, 0, manager.method(:process_signal_queue) ).
- and_return( signal_timer )
- expect( zmq_loop ).to receive( :register_timer ).with( signal_timer )
- expect( zmq_loop ).to receive( :register_timer ).with( manager.heartbeat_timer )
- expect( zmq_loop ).to receive( :start )
- expect( zmq_loop ).to receive( :remove ).with( tree_pollitem )
- expect( zmq_loop ).to receive( :remove ).with( event_pollitem )
- manager.run
+ let( :manager ) { @manager }
- expect( manager.event_publisher.event_queue.length ).to eq( 1 )
+ let!( :sock ) do
+ sock = Arborist.zmq_context.socket( :REQ )
+ sock.linger = 0
+ sock.connect( TESTING_API_SOCK )
+ sock
+ end
- event = manager.event_publisher.event_queue.first
- expect( event.first ).to eq( 'sys.startup' )
+ let( :api_handler ) { described_class.new( rep_sock, manager ) }
- payload = unpack_message( event.last )
- expect( payload ).to include(
- 'start_time' => an_instance_of(String),
- 'version' => an_instance_of(String)
+
+ describe "malformed requests" do
+
+ it "send an error response if the request can't be deserialized" do
+ sock.send( "whatevs, dude!" )
+ resmsg = sock.recv
+
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
+ expect( hdr ).to include(
+ 'success' => false,
+ 'reason' => /invalid request/i,
+ 'category' => 'client'
)
+ expect( body ).to be_nil
end
+
+ it "send an error response if the request isn't a tuple" do
+ sock.send( MessagePack.pack({ version: 1, action: 'list' }) )
+ resmsg = sock.recv
+
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
+ expect( hdr ).to include(
+ 'success' => false,
+ 'reason' => /invalid request.*not a tuple/i,
+ 'category' => 'client'
+ )
+ expect( body ).to be_nil
+ end
+
+
+ it "send an error response if the request is empty" do
+ sock.send( MessagePack.pack([]) )
+ resmsg = sock.recv
+
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
+ expect( hdr ).to include(
+ 'success' => false,
+ 'reason' => /invalid request.*incorrect length/i,
+ 'category' => 'client'
+ )
+ expect( body ).to be_nil
+ end
+
+
+ it "send an error response if the request is an incorrect length" do
+ sock.send( MessagePack.pack([{}, {}, {}]) )
+ resmsg = sock.recv
+
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
+ expect( hdr ).to include(
+ 'success' => false,
+ 'reason' => /invalid request.*incorrect length/i,
+ 'category' => 'client'
+ )
+ expect( body ).to be_nil
+ end
+
+
+ it "send an error response if the request's header is not a Map" do
+ sock.send( MessagePack.pack([nil, {}]) )
+ resmsg = sock.recv
+
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
+ expect( hdr ).to include(
+ 'success' => false,
+ 'reason' => /invalid request.*header is not a map/i,
+ 'category' => 'client'
+ )
+ expect( body ).to be_nil
+ end
+
+
+ it "send an error response if the request's body is not Nil, a Map, or an Array of Maps" do
+ sock.send( MessagePack.pack([{version: 1, action: 'list'}, 18]) )
+ resmsg = sock.recv
+
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
+ expect( hdr ).to include(
+ 'success' => false,
+ 'reason' => /invalid request.*body must be nil, a map, or an array of maps/i,
+ 'category' => 'client'
+ )
+ expect( body ).to be_nil
+ end
+
+
+ it "send an error response if missing a version" do
+ sock.send( MessagePack.pack([{action: 'list'}]) )
+ resmsg = sock.recv
+
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
+ expect( hdr ).to include(
+ 'success' => false,
+ 'reason' => /invalid request.*missing required header 'version'/i,
+ 'category' => 'client'
+ )
+ expect( body ).to be_nil
+ end
+
+
+ it "send an error response if missing an action" do
+ sock.send( MessagePack.pack([{version: 1}]) )
+ resmsg = sock.recv
+
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
+ expect( hdr ).to include(
+ 'success' => false,
+ 'reason' => /invalid request.*missing required header 'action'/i,
+ 'category' => 'client'
+ )
+ expect( body ).to be_nil
+ end
+
+
+ it "send an error response for unknown actions" do
+ badmsg = Arborist::TreeAPI.encode( :slap )
+ sock.send( badmsg )
+ resmsg = sock.recv
+
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
+ expect( hdr ).to include(
+ 'success' => false,
+ 'reason' => /invalid request.*no such action 'slap'/i,
+ 'category' => 'client'
+ )
+ expect( body ).to be_nil
+ end
end
-end