spec/arborist/manager_spec.rb in arborist-0.2.0.pre20170519125456 vs spec/arborist/manager_spec.rb in arborist-0.2.0
- old
+ new
@@ -3,12 +3,17 @@
require_relative '../spec_helper'
require 'tmpdir'
require 'timecop'
require 'arborist/manager'
+require 'arborist/mixins'
require 'arborist/node/host'
+require 'arborist/event/node_update'
+using Arborist::TimeRefinements
+
+
describe Arborist::Manager do
after( :all ) do
Arborist::Manager.state_file = nil
end
@@ -19,13 +24,13 @@
Arborist::Node::Root.reset
end
let( :manager ) { described_class.new }
+ let( :tmpfile_pathname ) { Pathname(Dir::Tmpname.create(['arb', 'tree']) {}) }
-
#
# Examples
#
it "starts with a root node" do
@@ -162,11 +167,11 @@
expect( manager.restore_node_states ).to be_falsey
end
it "checkpoints the state file periodically if an interval is configured" do
- statefile = Pathname( Dir.tmpdir ) + Dir::Tmpname.make_tmpname( 'arb', 'tree' )
+ statefile = tmpfile_pathname()
described_class.configure( checkpoint_frequency: 20_000, state_file: statefile )
manager = described_class.new
manager.register_checkpoint_timer
expect( manager.checkpoint_timer ).to be_a( Timers::Timer )
@@ -210,12 +215,43 @@
described_class.configure( heartbeat_frequency: 0 )
}.to raise_error( Arborist::ConfigError, /positive and non-zero/i )
end
- it "is sent at the configured interval"
+ it "is sent at the configured interval" do
+ described_class.configure( heartbeat_frequency: 11 )
+ expect( manager.reactor ).to receive( :add_periodic_timer ).with( 11 )
+ manager.register_heartbeat_timer
+ end
+
+
+ it "doesn't try to publish the heartbeat if it's not been started" do
+ manager.start_time = nil
+
+ manager.publish_heartbeat_event
+ expect( manager.event_queue ).to be_empty
+ end
+
+
+ it "contains runtime information about the manager" do
+ time = Time.now
+ manager.start_time = time
+
+ manager.publish_heartbeat_event
+ event = manager.event_queue.shift
+
+ expect( event ).to be_a( CZTop::Message )
+ decoded = Arborist::EventAPI.decode( event )
+
+ expect( decoded ).to include(
+ 'run_id' => manager.run_id,
+ 'start_time' => time.iso8601,
+ 'version' => Arborist::VERSION
+ )
+ end
+
end
context "a new empty manager" do
@@ -255,21 +291,17 @@
'_', 'italian_lessons', 'french_laundry', 'german_oak_cats'
)
end
- it "can replace an existing node" do
+ it "complains if adding a node that already exists" do
manager.add_node( node )
another_node = testing_node( 'italian_lessons' )
- manager.add_node( another_node )
- expect( manager.nodes ).to include( 'italian_lessons' )
- expect( manager.nodes['italian_lessons'] ).to_not be( node )
- expect( manager.nodes['italian_lessons'] ).to be( another_node )
-
- expect( manager.nodecount ).to eq( 2 )
- expect( manager.nodelist ).to include( '_', 'italian_lessons' )
+ expect {
+ manager.add_node( another_node )
+ }.to raise_error( Arborist::NodeError, /already present/ )
end
it "can have a node removed from it" do
manager.add_node( node )
@@ -341,21 +373,10 @@
manager.add_node( new_node )
expect( manager.nodes['branch'].children ).to include( 'new' )
end
- it "replaces a node in the tree when a node with an existing identifier is added" do
- updated_node = testing_node( 'leaf' ) do
- parent 'trunk'
- end
-
- manager.add_node( updated_node )
- expect( manager.nodes['branch'].children ).to_not include( 'leaf' => leaf_node )
- expect( manager.nodes['trunk'].children ).to include( 'leaf' => updated_node )
- end
-
-
it "rebuilds the tree when a node is removed from it" do
manager.remove_node( 'branch' )
expect( manager.nodes['trunk'].children ).to_not include( 'branch' )
expect( manager.nodes ).to_not include( 'branch' )
@@ -363,11 +384,11 @@
end
end
- xdescribe "tree API", :testing_manager do
+ describe "tree API", :testing_manager do
before( :each ) do
@manager = nil
@manager_thread = Thread.new do
@manager = make_testing_manager()
@@ -399,35 +420,44 @@
end
raise "Manager didn't stop" if @manager.running?
end
- describe "status" do
+ let( :manager ) { @manager }
+ let( :sock ) do
+ sock = CZTop::Socket::REQ.new
+ sock.options.linger = 0
+ sock.connect( TESTING_API_SOCK )
+ sock
+ end
+
+ describe "status" do
+
it "returns a Map describing the manager and its state" do
- msg = Arborist::TreeAPI.encode( :status )
+ msg = Arborist::TreeAPI.request( :status )
- sock.send( msg )
- resmsg = sock.recv
+ msg.send_to( sock )
+ resmsg = sock.receive
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
+ describe "search" do
it "returns an array of full state maps for nodes matching specified criteria" do
- msg = Arborist::TreeAPI.encode( :fetch, type: 'service', port: 22 )
+ msg = Arborist::TreeAPI.request( :search, type: 'service', port: 22 )
- sock.send( msg )
- resmsg = sock.recv
+ msg.send_to( sock )
+ resmsg = sock.receive
hdr, body = Arborist::TreeAPI.decode( resmsg )
expect( hdr ).to include( 'success' => true )
expect( body ).to be_a( Hash )
@@ -437,14 +467,14 @@
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} ] )
+ msg = Arborist::TreeAPI.request( :search, [ {}, {type: 'service', port: 22} ] )
- sock.send( msg )
- resmsg = sock.recv
+ msg.send_to( sock )
+ resmsg = sock.receive
hdr, body = Arborist::TreeAPI.decode( resmsg )
expect( hdr ).to include( 'success' => true )
expect( body ).to be_a( Hash )
@@ -454,61 +484,61 @@
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} ] )
+ msg = Arborist::TreeAPI.request( :search, [ {type: 'service'}, {port: 22} ] )
- sock.send( msg )
- resmsg = sock.recv
+ msg.send_to( sock )
+ resmsg = sock.receive
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.length ).to eq( 18 )
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
+ it "omits nodes beneath downed nodes if asked to" do
manager.nodes['sidonie'].update( error: 'sunspots' )
- msg = Arborist::TreeAPI.encode( :fetch, type: 'service', port: 22 )
+ msg = Arborist::TreeAPI.request( :search, {exclude_down: true}, type: 'service', port: 22 )
- sock.send( msg )
- resmsg = sock.recv
+ msg.send_to( sock )
+ resmsg = sock.receive
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
+ it "include nodes beneath downed nodes by default" do
manager.nodes['sidonie'].update( error: 'plague of locusts' )
- msg = Arborist::TreeAPI.encode( :fetch, {include_down: true}, type: 'service', port: 22 )
+ msg = Arborist::TreeAPI.request( :search, type: 'service', port: 22 )
- sock.send( msg )
- resmsg = sock.recv
+ msg.send_to( sock )
+ resmsg = sock.receive
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 )
+ msg = Arborist::TreeAPI.request( :search, {return: nil}, type: 'service', port: 22 )
- sock.send( msg )
- resmsg = sock.recv
+ msg.send_to( sock )
+ resmsg = sock.receive
hdr, body = Arborist::TreeAPI.decode( resmsg )
expect( hdr ).to include( 'success' => true )
expect( body ).to be_a( Hash )
expect( body.length ).to eq( 3 )
@@ -516,15 +546,15 @@
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]},
+ msg = Arborist::TreeAPI.request( :search, {return: %w[status tags addresses]},
type: 'service', port: 22 )
- sock.send( msg )
- resmsg = sock.recv
+ msg.send_to( sock )
+ resmsg = sock.receive
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' )
@@ -533,16 +563,16 @@
end
- describe "list" do
+ describe "fetch" do
it "returns an array of node state" do
- msg = Arborist::TreeAPI.encode( :list )
- sock.send( msg )
- resmsg = sock.recv
+ msg = Arborist::TreeAPI.request( :fetch )
+ msg.send_to( sock )
+ resmsg = sock.receive
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) )
@@ -552,23 +582,69 @@
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 start at a node other than the root" do
+ msg = Arborist::TreeAPI.request( :fetch, {from: 'sidonie'}, nil )
+ msg.send_to( sock )
+ resmsg = sock.receive
+
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
+ expect( hdr ).to include( 'success' => true )
+ expect( body.length ).to eq( manager.nodes.keys.count {|id| id.include?('sidonie')} )
+ expect( body ).to all( be_a(Hash) )
+ expect( body ).to_not include( hash_including('identifier' => '_') )
+ expect( body ).to_not 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_not include( hash_including('identifier' => 'yevaud') )
+ end
+
+
+ it "can be fetched as a tree" do
+ msg = Arborist::TreeAPI.request( :fetch, {tree: true}, nil )
+ msg.send_to( sock )
+ resmsg = sock.receive
+
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
+ expect( hdr ).to include( 'success' => true )
+ expect( body.length ).to eq( 1 )
+ expect( body.first ).to be_a( Hash )
+ expect( body.first ).to include( 'children' )
+ expect( body.first['identifier'] ).to eq( '_' )
+ expect( body.first['children'].keys ).to include( 'duir', 'localhost' )
+ end
+
+
it "can be limited by depth" do
- msg = Arborist::TreeAPI.encode( :list, {depth: 1}, nil )
- sock.send( msg )
- resmsg = sock.recv
+ msg = Arborist::TreeAPI.request( :fetch, {depth: 1}, nil )
+ msg.send_to( sock )
+ resmsg = sock.receive
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
+
+
+ it "errors when fetching from a nonexistent node" do
+ msg = Arborist::TreeAPI.request( :fetch, {from: "nope-nope-nope"}, nil )
+ msg.send_to( sock )
+ resmsg = sock.receive
+
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
+ expect( hdr ).to include( 'success' => false )
+ expect( hdr ).to include( "reason" => "No such node nope-nope-nope." )
+ expect( body ).to be_nil
+ end
end
describe "update" do
@@ -588,13 +664,13 @@
ping: {
rtt: 843
}
}
}
- msg = Arborist::TreeAPI.encode( :update, update_data )
- sock.send( msg )
- resmsg = sock.recv
+ msg = Arborist::TreeAPI.request( :update, update_data )
+ msg.send_to( sock )
+ resmsg = sock.receive
hdr, body = Arborist::TreeAPI.decode( resmsg )
expect( hdr ).to include( 'success' => true )
expect( body ).to be_nil
@@ -603,22 +679,22 @@
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
+ msg = Arborist::TreeAPI.request( :update, charlie_humperton: {ping: { rtt: 8 }} )
+ msg.send_to( sock )
+ resmsg = sock.receive
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
+ msg = Arborist::TreeAPI.request( :update, nil )
+ msg.send_to( sock )
+ resmsg = sock.receive
hdr, body = Arborist::TreeAPI.decode( resmsg )
expect( hdr ).to include( 'success' => false )
expect( hdr['reason'] ).to match( /respond to #each/ )
end
@@ -626,101 +702,105 @@
describe "subscribe" do
it "adds a subscription for all event types to the root node by default" do
- msg = Arborist::TreeAPI.encode( :subscribe, [{}, {}] )
+ msg = Arborist::TreeAPI.request( :subscribe, [{}, {}] )
resmsg = nil
expect {
- sock.send( msg )
- resmsg = sock.recv
+ msg.send_to( sock )
+ resmsg = sock.receive
}.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 ])
+ expect( body ).to be_a( Hash )
+ expect( body ).to include( 'id' )
+ expect( manager.subscriptions.keys ).to include( body['id'] )
end
it "adds a subscription to the specified node if an identifier is specified" do
- msg = Arborist::TreeAPI.encode( :subscribe, {identifier: 'sidonie'}, [{}, {}] )
+ msg = Arborist::TreeAPI.request( :subscribe, {identifier: 'sidonie'}, [{}, {}] )
resmsg = nil
expect {
- sock.send( msg )
- resmsg = sock.recv
+ msg.send_to( sock )
+ resmsg = sock.receive
}.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 ])
+ expect( body ).to be_a( Hash )
+ expect( body ).to include( 'id' )
+ expect( manager.subscriptions.keys ).to include( body['id'] )
end
it "adds a subscription for particular event types if one is specified" do
- msg = Arborist::TreeAPI.encode( :subscribe, {event_type: 'node.acked'}, [{}, {}] )
+ msg = Arborist::TreeAPI.request( :subscribe, {event_type: 'node.acked'}, [{}, {}] )
resmsg = nil
expect {
- sock.send( msg )
- resmsg = sock.recv
+ msg.send_to( sock )
+ resmsg = sock.receive
}.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 ]
+ node = manager.subscriptions[ body['id'] ]
+ sub = node.subscriptions[ body['id'] ]
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, {}] )
+ msg = Arborist::TreeAPI.request( :subscribe, [criteria, {}] )
resmsg = nil
expect {
- sock.send( msg )
- resmsg = sock.recv
+ msg.send_to( sock )
+ resmsg = sock.receive
}.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( body ).to be_a( Hash ).and( include('id') )
+ node = manager.subscriptions[ body['id'] ]
+ sub = node.subscriptions[ body['id'] ]
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] )
+ msg = Arborist::TreeAPI.request( :subscribe, [{}, criteria] )
resmsg = nil
expect {
- sock.send( msg )
- resmsg = sock.recv
+ msg.send_to( sock )
+ resmsg = sock.receive
}.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( body ).to be_a( Hash ).and( include('id') )
+ node = manager.subscriptions[ body['id'] ]
+ sub = node.subscriptions[ body['id'] ]
expect( sub.event_type ).to be_nil
expect( sub.negative_criteria ).to eq({ 'type' => 'host' })
end
@@ -733,32 +813,32 @@
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 )
+ msg = Arborist::TreeAPI.request( :unsubscribe, {subscription_id: subscription.id}, nil )
resmsg = nil
expect {
- sock.send( msg )
- resmsg = sock.recv
+ msg.send_to( sock )
+ resmsg = sock.receive
}.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 )
+ msg = Arborist::TreeAPI.request( :unsubscribe, {subscription_id: 'the bears!'}, nil )
resmsg = nil
expect {
- sock.send( msg )
- resmsg = sock.recv
+ msg.send_to( sock )
+ resmsg = sock.receive
}.to_not change { manager.subscriptions.length }
hdr, body = Arborist::TreeAPI.decode( resmsg )
expect( body ).to be_nil
end
@@ -767,49 +847,51 @@
describe "prune" do
it "removes a single node" do
- msg = Arborist::TreeAPI.encode( :prune, {identifier: 'duir-ssh'}, nil )
- sock.send( msg )
- resmsg = sock.recv
+ msg = Arborist::TreeAPI.request( :prune, {identifier: 'duir-ssh'}, nil )
+ msg.send_to( sock )
+ resmsg = sock.receive
hdr, body = Arborist::TreeAPI.decode( resmsg )
expect( hdr ).to include( 'success' => true )
- expect( body ).to eq( true )
+ expect( body ).to be_a( Hash )
+ expect( body ).to include( 'identifier' => 'duir-ssh' )
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
+ msg = Arborist::TreeAPI.request( :prune, {identifier: 'shemp-ssh'}, nil )
+ msg.send_to( sock )
+ resmsg = sock.receive
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
+ msg = Arborist::TreeAPI.request( :prune, {identifier: 'duir'}, nil )
+ msg.send_to( sock )
+ resmsg = sock.receive
hdr, body = Arborist::TreeAPI.decode( resmsg )
expect( hdr ).to include( 'success' => true )
- expect( body ).to eq( true )
+ expect( body ).to be_a( Hash )
+ expect( body ).to include( 'identifier' => 'duir' )
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
+ msg = Arborist::TreeAPI.request( :prune )
+ msg.send_to( sock )
+ resmsg = sock.receive
hdr, body = Arborist::TreeAPI.decode( resmsg )
expect( hdr ).to include( 'success' => false )
expect( hdr['reason'] ).to match( /no identifier/i )
end
@@ -826,18 +908,18 @@
attributes = {
description: 'The evil penguin node of doom.',
addresses: ['10.2.66.8'],
tags: ['internal', 'football']
}
- msg = Arborist::TreeAPI.encode( :graft, header, attributes )
+ msg = Arborist::TreeAPI.request( :graft, header, attributes )
- sock.send( msg )
- resmsg = sock.recv
+ msg.send_to( sock )
+ resmsg = sock.receive
hdr, body = Arborist::TreeAPI.decode( resmsg )
expect( hdr ).to include( 'success' => true )
- expect( body ).to eq( 'guenter' )
+ expect( body ).to include( 'identifier' => '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] )
@@ -855,18 +937,18 @@
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 )
+ msg = Arborist::TreeAPI.request( :graft, header, attributes )
- sock.send( msg )
- resmsg = sock.recv
+ msg.send_to( sock )
+ resmsg = sock.receive
hdr, body = Arborist::TreeAPI.decode( resmsg )
expect( hdr ).to include( 'success' => true )
- expect( body ).to eq( 'orgalorg' )
+ expect( body ).to include( 'identifier' => '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] )
@@ -883,18 +965,18 @@
parent: 'duir'
}
attributes = {
description: 'Mmmmm AppleTalk.'
}
- msg = Arborist::TreeAPI.encode( :graft, header, attributes )
+ msg = Arborist::TreeAPI.request( :graft, header, attributes )
- sock.send( msg )
- resmsg = sock.recv
+ msg.send_to( sock )
+ resmsg = sock.receive
hdr, body = Arborist::TreeAPI.decode( resmsg )
expect( hdr ).to include( 'success' => true )
- expect( body ).to eq( 'duir-echo' )
+ expect( body ).to eq( 'identifier' => '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] )
@@ -911,20 +993,35 @@
type: 'service'
}
attributes = {
description: 'Mmmmm AppleTalk.'
}
- msg = Arborist::TreeAPI.encode( :graft, header, attributes )
+ msg = Arborist::TreeAPI.request( :graft, header, attributes )
- sock.send( msg )
- resmsg = sock.recv
+ msg.send_to( sock )
+ resmsg = sock.receive
hdr, body = Arborist::TreeAPI.decode( resmsg )
expect( hdr ).to include( 'success' => false )
expect( hdr['reason'] ).to match( /no host given/i )
end
+
+ it "errors if adding a node that already exists" do
+ header = {
+ identifier: 'duir',
+ type: 'host',
+ }
+ msg = Arborist::TreeAPI.request( :graft, header, {} )
+
+ msg.send_to( sock )
+ resmsg = sock.receive
+
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
+ expect( hdr ).to include( 'success' => false )
+ expect( hdr['reason'] ).to match( /exists/i )
+ end
end
describe "modify" do
@@ -934,14 +1031,14 @@
}
attributes = {
parent: '_',
addresses: ['192.168.32.32', '10.2.2.28']
}
- msg = Arborist::TreeAPI.encode( :modify, header, attributes )
+ msg = Arborist::TreeAPI.request( :modify, header, attributes )
- sock.send( msg )
- resmsg = sock.recv
+ msg.send_to( sock )
+ resmsg = sock.receive
hdr, body = Arborist::TreeAPI.decode( resmsg )
expect( hdr ).to include( 'success' => true )
node = manager.nodes[ 'sidonie' ]
@@ -957,14 +1054,14 @@
identifier: 'sidonie',
}
attributes = {
identifier: 'somethingelse'
}
- msg = Arborist::TreeAPI.encode( :modify, header, attributes )
+ msg = Arborist::TreeAPI.request( :modify, header, attributes )
- sock.send( msg )
- resmsg = sock.recv
+ msg.send_to( sock )
+ resmsg = sock.receive
hdr, body = Arborist::TreeAPI.decode( resmsg )
expect( hdr ).to include( 'success' => true )
expect( manager.nodes['sidonie'] ).to be_an( Arborist::Node )
@@ -977,14 +1074,14 @@
identifier: '_',
}
attributes = {
identifier: 'somethingelse'
}
- msg = Arborist::TreeAPI.encode( :modify, header, attributes )
+ msg = Arborist::TreeAPI.request( :modify, header, attributes )
- sock.send( msg )
- resmsg = sock.recv
+ msg.send_to( sock )
+ resmsg = sock.receive
hdr, body = Arborist::TreeAPI.decode( resmsg )
expect( hdr ).to include( 'success' => false )
expect( manager.nodes['_'].identifier ).to eq( '_' )
end
@@ -995,23 +1092,403 @@
identifier: 'nopenopenope',
}
attributes = {
identifier: 'somethingelse'
}
- msg = Arborist::TreeAPI.encode( :modify, header, attributes )
+ msg = Arborist::TreeAPI.request( :modify, header, attributes )
- sock.send( msg )
- resmsg = sock.recv
+ msg.send_to( sock )
+ resmsg = sock.receive
hdr, body = Arborist::TreeAPI.decode( resmsg )
expect( hdr ).to include( 'success' => false )
end
+
+
+ it "reparents a node whose parent is altered" do
+ header = {
+ identifier: 'sidonie'
+ }
+ attributes = {
+ parent: 'yevaud'
+ }
+
+ msg = Arborist::TreeAPI.request( :modify, header, attributes )
+
+ node = manager.nodes[ 'sidonie' ]
+ old_parent = manager.nodes[ 'duir' ]
+ expect( node.parent ).to eq( 'duir' )
+
+ msg.send_to( sock )
+ resmsg = sock.receive
+
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
+ expect( hdr ).to include( 'success' => true )
+
+ new_parent = manager.nodes[ 'yevaud' ]
+ expect( node.parent ).to eq( 'yevaud' )
+ expect( old_parent.children ).to_not include( 'sidonie' )
+ expect( new_parent.children ).to include( 'sidonie' )
+ end
+
end
+
+ describe "deps" do
+
+ it "returns a list of the identifiers of nodes that depend on it" do
+ msg = Arborist::TreeAPI.request( :deps, {from: 'sidonie'}, nil )
+ msg.send_to( sock )
+ resmsg = sock.receive
+
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
+ expect( hdr ).to include( 'success' => true )
+ expect( body ).to be_a( Hash )
+ expect( body ).to include( 'deps' )
+ expect( body['deps'] ).to_not include( 'sidonie' )
+ expect( body['deps'] ).to be_an( Array ).
+ and( include('sidonie-ssh', 'yevaud-cozy_frontend', 'sandbox01-canary') )
+ end
+
+ end
+
+
+ describe "ack" do
+
+ it "acknowledges a single node" do
+ msg = Arborist::TreeAPI.request( :ack,
+ {identifier: 'sidonie'},
+ {message: "Planned maintenance", sender: 'xerces'}
+ )
+ msg.send_to( sock )
+ resmsg = sock.receive
+
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
+ expect( hdr ).to include( 'success' => true )
+ expect( body ).to be_nil
+
+ expect( manager.nodes['sidonie'] ).to be_disabled
+ end
+
+
+ it "returns an error if the node to ack doesn't exist" do
+ msg = Arborist::TreeAPI.request( :ack,
+ {identifier: 'shemp-ssh'},
+ {message: "Planned maintenance", sender: 'xerces'}
+ )
+ msg.send_to( sock )
+ resmsg = sock.receive
+
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
+ expect( hdr ).to include( 'success' => false )
+ expect( hdr['reason'] ).to match( /no such node/i )
+ end
+
+
+ it "returns an error if the node to ack isn't specified" do
+ msg = Arborist::TreeAPI.request( :ack, {}, {message: "", sender: ''} )
+ msg.send_to( sock )
+ resmsg = sock.receive
+
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
+ expect( hdr ).to include( 'success' => false )
+ expect( hdr['reason'] ).to match( /no identifier/i )
+ end
+
+
+ it "returns an error if one of the required arguments isn't provided" do
+ msg = Arborist::TreeAPI.request( :ack,
+ {identifier: 'sidonie'},
+ {message: "Planned maintenance"}
+ )
+ msg.send_to( sock )
+ resmsg = sock.receive
+
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
+ expect( hdr ).to include( 'success' => false )
+ expect( hdr['reason'] ).to match( /missing required ack sender/i )
+ end
+
+
+ it "propagates events from an acknowledgement up the node tree" do
+ expect( manager.root ).to receive( :publish_events ).
+ at_least( :once ).
+ and_call_original
+ expect( manager.nodes['duir'] ).to receive( :publish_events ).
+ at_least( :once ).
+ and_call_original
+
+ manager.handle_ack_request( { 'identifier' => 'sidonie' }, {
+ 'message' => 'There will also be corn served.',
+ 'sender' => 'Smoove-B'
+ })
+ end
+ end
+
+
+ describe "unack" do
+
+ it "removes an acknowledgement from a single node" do
+ msg = Arborist::TreeAPI.request( :unack, {identifier: 'sidonie'}, nil )
+ msg.send_to( sock )
+ resmsg = sock.receive
+
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
+ expect( hdr ).to include( 'success' => true )
+ expect( body ).to be_nil
+
+ expect( manager.nodes['sidonie'] ).to be_unknown
+ end
+
+
+ it "returns an error if the node to unack doesn't exist" do
+ msg = Arborist::TreeAPI.request( :unack, {identifier: 'shemp-ssh'}, nil )
+ msg.send_to( sock )
+ resmsg = sock.receive
+
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
+ expect( hdr ).to include( 'success' => false )
+ expect( hdr['reason'] ).to match( /no such/i )
+ end
+
+
+ it "returns an error if the node to unack isn't specified" do
+ msg = Arborist::TreeAPI.request( :unack, {}, nil )
+ msg.send_to( sock )
+ resmsg = sock.receive
+
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
+ expect( hdr ).to include( 'success' => false )
+ expect( hdr['reason'] ).to match( /no identifier/i )
+ end
+
+
+ it "propagates events from an unack up the node tree" do
+ expect( manager.root ).to receive( :publish_events ).
+ at_least( :once ).
+ and_call_original
+ expect( manager.nodes['duir'] ).to receive( :publish_events ).
+ at_least( :once ).
+ and_call_original
+ manager.handle_unack_request( { 'identifier' => 'sidonie' }, nil )
+ end
+ end
+
+
+
+ describe "malformed requests" do
+
+ it "send an error response if the request can't be deserialized" do
+ sock << "whatevs, dude!"
+ resmsg = sock.receive
+
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
+ expect( hdr ).to include(
+ 'success' => false,
+ 'reason' => /invalid message/i,
+ 'category' => 'client'
+ )
+ expect( body ).to be_nil
+ end
+
+
+ it "send an error response if the request isn't a tuple" do
+ sock << MessagePack.pack({ version: 1, action: 'fetch' })
+ resmsg = sock.receive
+
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
+ expect( hdr ).to include(
+ 'success' => false,
+ 'reason' => /invalid message.*not an array/i,
+ 'category' => 'client'
+ )
+ expect( body ).to be_nil
+ end
+
+
+ it "send an error response if the request is empty" do
+ sock << MessagePack.pack([])
+ resmsg = sock.receive
+
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
+ expect( hdr ).to include(
+ 'success' => false,
+ 'reason' => /invalid message.*expected 1-2 parts, got 0/i,
+ 'category' => 'client'
+ )
+ expect( body ).to be_nil
+ end
+
+
+ it "send an error response if the request is an incorrect length" do
+ sock << MessagePack.pack( [{}, {}, {}] )
+ resmsg = sock.receive
+
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
+ expect( hdr ).to include(
+ 'success' => false,
+ 'reason' => /expected 1-2 parts, got 3/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 << MessagePack.pack( [nil, {}] )
+ resmsg = sock.receive
+
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
+ expect( hdr ).to include(
+ 'success' => false,
+ 'reason' => /no header/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 << MessagePack.pack( [{version: 1, action: 'fetch'}, 18] )
+ resmsg = sock.receive
+
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
+ expect( hdr ).to include(
+ 'success' => false,
+ 'reason' => /invalid message.*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 << MessagePack.pack( [{action: 'fetch'}] )
+ resmsg = sock.receive
+
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
+ expect( hdr ).to include(
+ 'success' => false,
+ 'reason' => /invalid message.*missing required header 'version'/i,
+ 'category' => 'client'
+ )
+ expect( body ).to be_nil
+ end
+
+
+ it "send an error response if missing an action" do
+ sock << MessagePack.pack( [{version: 1}] )
+ resmsg = sock.receive
+
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
+ expect( hdr ).to include(
+ 'success' => false,
+ 'reason' => /invalid message.*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.request( :slap )
+ badmsg.send_to( sock )
+ resmsg = sock.receive
+
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
+ expect( hdr ).to include(
+ 'success' => false,
+ 'reason' => /invalid message.*no such action 'slap'/i,
+ 'category' => 'client'
+ )
+ expect( body ).to be_nil
+ end
+
+
+ it "send an error response for the `tree` action" do
+ badmsg = Arborist::TreeAPI.request( :tree )
+ badmsg.send_to( sock )
+ resmsg = sock.receive
+
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
+ expect( hdr ).to include(
+ 'success' => false,
+ 'reason' => /invalid message.*no such action 'tree'/i,
+ 'category' => 'client'
+ )
+ expect( body ).to be_nil
+ end
+
+ end
+
end
+ describe "event API" 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
+
+
+ let( :manager ) { @manager }
+
+ let!( :sock ) do
+ sock = CZTop::Socket::SUB.new
+ sock.options.linger = 0
+ sock.subscribe( '' )
+ event_uri = manager.event_socket.last_endpoint
+ sock.connect( event_uri )
+ Loggability[ Arborist ].info "Connected subscribed socket to %p" % [ event_uri ]
+ sock
+ end
+
+
+ it "publishes messages via the event socket" do
+ node = Arborist::Node.create( :root )
+ event = Arborist::Event.create( :node_update, node )
+ manager.publish( 'identifier-00aa', event )
+
+ msg = nil
+ wait( 1.second ).for { msg = sock.receive }.to be_a( CZTop::Message )
+
+ expect( msg.frames.first.to_s ).to eq( 'identifier-00aa' )
+ expect( msg.frames.last.to_s ).to be_a_messagepacked( Hash )
+ end
+
+ end
+
+
describe "tree traversal" do
let( :tree ) do
# router
# host_a host_b host_c
@@ -1094,11 +1571,11 @@
end
end
- describe "node updates and events" do
+ describe "node updates" do
let( :tree ) do
# router
# host_a host_b host_c
# www smtp imap www nfs ssh www
@@ -1123,40 +1600,40 @@
instance.load_tree( tree )
instance
end
- it "can fetch a Hash of node states" do
- states = manager.fetch_matching_node_states( {}, [] )
+ it "can search a Hash of node states" do
+ states = manager.find_matching_node_states( {}, [] )
expect( states.size ).to eq( manager.nodes.size )
expect( states ).to include( 'host-b-nfs', 'host-c', 'router' )
expect( states['host-b-nfs'] ).to be_a( Hash )
expect( states['host-c'] ).to be_a( Hash )
expect( states['router'] ).to be_a( Hash )
end
- it "can fetch a Hash of node states for nodes which match specified criteria" do
- states = manager.fetch_matching_node_states( {'identifier' => 'host-c'}, [] )
+ it "can search a Hash of node states for nodes which match specified criteria" do
+ states = manager.find_matching_node_states( {'identifier' => 'host-c'}, [] )
expect( states.size ).to eq( 1 )
expect( states.keys.first ).to eq( 'host-c' )
expect( states['host-c'] ).to be_a( Hash )
end
- it "can fetch a Hash of node states for nodes which don't match specified negative criteria" do
- states = manager.fetch_matching_node_states( {}, [], false, {'identifier' => 'host-c'} )
+ it "can search a Hash of node states for nodes which don't match specified negative criteria" do
+ states = manager.find_matching_node_states( {}, [], false, {'identifier' => 'host-c'} )
expect( states.size ).to eq( manager.nodes.size - 1 )
expect( states ).to_not include( 'host-c' )
end
- it "can fetch a Hash of node states for nodes combining positive and negative criteria" do
+ it "can search a Hash of node states for nodes combining positive and negative criteria" do
positive = {'tag' => 'home'}
negative = {'identifier' => 'host-a-www'}
- states = manager.fetch_matching_node_states( positive, [], false, negative )
+ states = manager.find_matching_node_states( positive, [], false, negative )
expect( states.size ).to eq( 2 )
expect( states ).to_not include( 'host-a-www' )
end
@@ -1238,204 +1715,6 @@
end
end
-
-__END__
-
- let( :socket ) { instance_double( ZMQ::Socket::Pub ) }
- let( :pollitem ) { instance_double( ZMQ::Pollitem, pollable: socket ) }
- let( :zloop ) { instance_double( ZMQ::Loop ) }
-
- let( :manager ) { Arborist::Manager.new }
- let( :event ) { Arborist::Event.create(TestEvent, 'stuff') }
-
- let( :publisher ) { described_class.new(pollitem, manager, zloop) }
-
-
- it "starts out registered for writing" do
- expect( publisher ).to be_registered
- end
-
-
- 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
-
-
- 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
-
-
-
- let( :manager ) { @manager }
-
- let!( :sock ) do
- sock = Arborist.zmq_context.socket( :REQ )
- sock.linger = 0
- sock.connect( TESTING_API_SOCK )
- sock
- end
-
- let( :api_handler ) { described_class.new( rep_sock, manager ) }
-
-
- 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