#! /usr/bin/env ruby require 'spec_helper' require 'puppet/indirector/indirection' shared_examples_for "Indirection Delegator" do it "should create a request object with the appropriate method name and all of the passed arguments" do request = Puppet::Indirector::Request.new(:indirection, :find, "me", nil) @indirection.expects(:request).with(@method, "mystuff", nil, :one => :two).returns request @terminus.stubs(@method) @indirection.send(@method, "mystuff", :one => :two) end it "should choose the terminus returned by the :terminus_class" do @indirection.expects(:terminus_class).returns :test_terminus @terminus.expects(@method) @indirection.send(@method, "me") end it "should let the appropriate terminus perform the lookup" do @terminus.expects(@method).with { |r| r.is_a?(Puppet::Indirector::Request) } @indirection.send(@method, "me") end end shared_examples_for "Delegation Authorizer" do before do # So the :respond_to? turns out correctly. class << @terminus def authorized? end end end it "should not check authorization if a node name is not provided" do @terminus.expects(:authorized?).never @terminus.stubs(@method) # The quotes are necessary here, else it looks like a block. @request.stubs(:options).returns({}) @indirection.send(@method, "/my/key") end it "should pass the request to the terminus's authorization method" do @terminus.expects(:authorized?).with { |r| r.is_a?(Puppet::Indirector::Request) }.returns(true) @terminus.stubs(@method) @indirection.send(@method, "/my/key", :node => "mynode") end it "should fail if authorization returns false" do @terminus.expects(:authorized?).returns(false) @terminus.stubs(@method) expect { @indirection.send(@method, "/my/key", :node => "mynode") }.to raise_error(ArgumentError) end it "should continue if authorization returns true" do @terminus.expects(:authorized?).returns(true) @terminus.stubs(@method) @indirection.send(@method, "/my/key", :node => "mynode") end end shared_examples_for "Request validator" do it "asks the terminus to validate the request" do @terminus.expects(:validate).raises(Puppet::Indirector::ValidationError, "Invalid") @terminus.expects(@method).never expect { @indirection.send(@method, "key") }.to raise_error Puppet::Indirector::ValidationError end end describe Puppet::Indirector::Indirection do describe "when initializing" do # (LAK) I've no idea how to test this, really. it "should store a reference to itself before it consumes its options" do expect { @indirection = Puppet::Indirector::Indirection.new(Object.new, :testingness, :not_valid_option) }.to raise_error(NoMethodError, /undefined method/) expect(Puppet::Indirector::Indirection.instance(:testingness)).to be_instance_of(Puppet::Indirector::Indirection) Puppet::Indirector::Indirection.instance(:testingness).delete end it "should keep a reference to the indirecting model" do model = mock 'model' @indirection = Puppet::Indirector::Indirection.new(model, :myind) expect(@indirection.model).to equal(model) end it "should set the name" do @indirection = Puppet::Indirector::Indirection.new(mock('model'), :myind) expect(@indirection.name).to eq(:myind) end it "should require indirections to have unique names" do @indirection = Puppet::Indirector::Indirection.new(mock('model'), :test) expect { Puppet::Indirector::Indirection.new(:test) }.to raise_error(ArgumentError) end it "should extend itself with any specified module" do mod = Module.new @indirection = Puppet::Indirector::Indirection.new(mock('model'), :test, :extend => mod) expect(@indirection.singleton_class.included_modules).to include(mod) end after do @indirection.delete if defined?(@indirection) end end describe "when an instance" do before :each do @terminus_class = mock 'terminus_class' @terminus = mock 'terminus' @terminus.stubs(:validate) @terminus_class.stubs(:new).returns(@terminus) @cache = stub 'cache', :name => "mycache" @cache_class = mock 'cache_class' Puppet::Indirector::Terminus.stubs(:terminus_class).with(:test, :cache_terminus).returns(@cache_class) Puppet::Indirector::Terminus.stubs(:terminus_class).with(:test, :test_terminus).returns(@terminus_class) @indirection = Puppet::Indirector::Indirection.new(mock('model'), :test) @indirection.terminus_class = :test_terminus @instance = stub 'instance', :expiration => nil, :expiration= => nil, :name => "whatever" @name = :mything #@request = stub 'instance', :key => "/my/key", :instance => @instance, :options => {} @request = mock 'instance' end it "should allow setting the ttl" do @indirection.ttl = 300 expect(@indirection.ttl).to eq(300) end it "should default to the :runinterval setting, converted to an integer, for its ttl" do Puppet[:runinterval] = 1800 expect(@indirection.ttl).to eq(1800) end it "should calculate the current expiration by adding the TTL to the current time" do @indirection.stubs(:ttl).returns(100) now = Time.now Time.stubs(:now).returns now expect(@indirection.expiration).to eq(Time.now + 100) end it "should have a method for creating an indirection request instance" do expect(@indirection).to respond_to(:request) end describe "creates a request" do it "should create it with its name as the request's indirection name" do Puppet::Indirector::Request.expects(:new).with { |name, *other| @indirection.name == name } @indirection.request(:funtest, "yayness") end it "should require a method and key" do Puppet::Indirector::Request.expects(:new).with { |name, method, key, *other| method == :funtest and key == "yayness" } @indirection.request(:funtest, "yayness") end it "should support optional arguments" do Puppet::Indirector::Request.expects(:new).with { |name, method, key, other| other == {:one => :two} } @indirection.request(:funtest, "yayness", :one => :two) end it "should not pass options if none are supplied" do Puppet::Indirector::Request.expects(:new).with { |*args| args.length < 4 } @indirection.request(:funtest, "yayness") end it "should return the request" do request = mock 'request' Puppet::Indirector::Request.expects(:new).returns request expect(@indirection.request(:funtest, "yayness")).to equal(request) end end describe "and looking for a model instance" do before { @method = :find } it_should_behave_like "Indirection Delegator" it_should_behave_like "Delegation Authorizer" it_should_behave_like "Request validator" it "should return the results of the delegation" do @terminus.expects(:find).returns(@instance) expect(@indirection.find("me")).to equal(@instance) end it "should return false if the instance is false" do @terminus.expects(:find).returns(false) expect(@indirection.find("me")).to equal(false) end it "should set the expiration date on any instances without one set" do @terminus.stubs(:find).returns(@instance) @indirection.expects(:expiration).returns :yay @instance.expects(:expiration).returns(nil) @instance.expects(:expiration=).with(:yay) @indirection.find("/my/key") end it "should not override an already-set expiration date on returned instances" do @terminus.stubs(:find).returns(@instance) @indirection.expects(:expiration).never @instance.expects(:expiration).returns(:yay) @instance.expects(:expiration=).never @indirection.find("/my/key") end it "should filter the result instance if the terminus supports it" do @terminus.stubs(:find).returns(@instance) @terminus.stubs(:respond_to?).with(:filter).returns(true) @terminus.expects(:filter).with(@instance) @indirection.find("/my/key") end describe "when caching is enabled" do before do @indirection.cache_class = :cache_terminus @cache_class.stubs(:new).returns(@cache) @instance.stubs(:expired?).returns false end it "should first look in the cache for an instance" do @terminus.stubs(:find).never @cache.expects(:find).returns @instance @indirection.find("/my/key") end it "should not look in the cache if the request specifies not to use the cache" do @terminus.expects(:find).returns @instance @cache.expects(:find).never @cache.stubs(:save) @indirection.find("/my/key", :ignore_cache => true) end it "should still save to the cache even if the cache is being ignored during readin" do @terminus.expects(:find).returns @instance @cache.expects(:save) @indirection.find("/my/key", :ignore_cache => true) end it "should not save to the cache if told to skip updating the cache" do @terminus.expects(:find).returns @instance @cache.expects(:find).returns nil @cache.expects(:save).never @indirection.find("/my/key", :ignore_cache_save => true) end it "should only look in the cache if the request specifies not to use the terminus" do @terminus.expects(:find).never @cache.expects(:find) @indirection.find("/my/key", :ignore_terminus => true) end it "should use a request to look in the cache for cached objects" do @cache.expects(:find).with { |r| r.method == :find and r.key == "/my/key" }.returns @instance @cache.stubs(:save) @indirection.find("/my/key") end it "should return the cached object if it is not expired" do @instance.stubs(:expired?).returns false @cache.stubs(:find).returns @instance expect(@indirection.find("/my/key")).to equal(@instance) end it "should not fail if the cache fails" do @terminus.stubs(:find).returns @instance @cache.expects(:find).raises ArgumentError @cache.stubs(:save) expect { @indirection.find("/my/key") }.not_to raise_error end it "should look in the main terminus if the cache fails" do @terminus.expects(:find).returns @instance @cache.expects(:find).raises ArgumentError @cache.stubs(:save) expect(@indirection.find("/my/key")).to equal(@instance) end it "should send a debug log if it is using the cached object" do Puppet.expects(:debug) @cache.stubs(:find).returns @instance @indirection.find("/my/key") end it "should not return the cached object if it is expired" do @instance.stubs(:expired?).returns true @cache.stubs(:find).returns @instance @terminus.stubs(:find).returns nil expect(@indirection.find("/my/key")).to be_nil end it "should send an info log if it is using the cached object" do Puppet.expects(:info) @instance.stubs(:expired?).returns true @cache.stubs(:find).returns @instance @terminus.stubs(:find).returns nil @indirection.find("/my/key") end it "should cache any objects not retrieved from the cache" do @cache.expects(:find).returns nil @terminus.expects(:find).returns(@instance) @cache.expects(:save) @indirection.find("/my/key") end it "should use a request to look in the cache for cached objects" do @cache.expects(:find).with { |r| r.method == :find and r.key == "/my/key" }.returns nil @terminus.stubs(:find).returns(@instance) @cache.stubs(:save) @indirection.find("/my/key") end it "should cache the instance using a request with the instance set to the cached object" do @cache.stubs(:find).returns nil @terminus.stubs(:find).returns(@instance) @cache.expects(:save).with { |r| r.method == :save and r.instance == @instance } @indirection.find("/my/key") end it "should send an info log that the object is being cached" do @cache.stubs(:find).returns nil @terminus.stubs(:find).returns(@instance) @cache.stubs(:save) Puppet.expects(:info) @indirection.find("/my/key") end it "should fail if saving to the cache fails but log the exception" do @cache.stubs(:find).returns nil @terminus.stubs(:find).returns(@instance) @cache.stubs(:save).raises RuntimeError Puppet.expects(:log_exception) expect { @indirection.find("/my/key") }.to raise_error RuntimeError end end end describe "and doing a head operation" do before { @method = :head } it_should_behave_like "Indirection Delegator" it_should_behave_like "Delegation Authorizer" it_should_behave_like "Request validator" it "should return true if the head method returned true" do @terminus.expects(:head).returns(true) expect(@indirection.head("me")).to eq(true) end it "should return false if the head method returned false" do @terminus.expects(:head).returns(false) expect(@indirection.head("me")).to eq(false) end describe "when caching is enabled" do before do @indirection.cache_class = :cache_terminus @cache_class.stubs(:new).returns(@cache) @instance.stubs(:expired?).returns false end it "should first look in the cache for an instance" do @terminus.stubs(:find).never @terminus.stubs(:head).never @cache.expects(:find).returns @instance expect(@indirection.head("/my/key")).to eq(true) end it "should not save to the cache" do @cache.expects(:find).returns nil @cache.expects(:save).never @terminus.expects(:head).returns true expect(@indirection.head("/my/key")).to eq(true) end it "should not fail if the cache fails" do @terminus.stubs(:head).returns true @cache.expects(:find).raises ArgumentError expect { @indirection.head("/my/key") }.not_to raise_error end it "should look in the main terminus if the cache fails" do @terminus.expects(:head).returns true @cache.expects(:find).raises ArgumentError expect(@indirection.head("/my/key")).to eq(true) end it "should send a debug log if it is using the cached object" do Puppet.expects(:debug) @cache.stubs(:find).returns @instance @indirection.head("/my/key") end it "should not accept the cached object if it is expired" do @instance.stubs(:expired?).returns true @cache.stubs(:find).returns @instance @terminus.stubs(:head).returns false expect(@indirection.head("/my/key")).to eq(false) end end end describe "and storing a model instance" do before { @method = :save } it "should return the result of the save" do @terminus.stubs(:save).returns "foo" expect(@indirection.save(@instance)).to eq("foo") end describe "when caching is enabled" do before do @indirection.cache_class = :cache_terminus @cache_class.stubs(:new).returns(@cache) @instance.stubs(:expired?).returns false end it "should return the result of saving to the terminus" do request = stub 'request', :instance => @instance, :node => nil, :ignore_cache_save? => false @indirection.expects(:request).returns request @cache.stubs(:save) @terminus.stubs(:save).returns @instance expect(@indirection.save(@instance)).to equal(@instance) end it "should use a request to save the object to the cache" do request = stub 'request', :instance => @instance, :node => nil, :ignore_cache_save? => false @indirection.expects(:request).returns request @cache.expects(:save).with(request) @terminus.stubs(:save) @indirection.save(@instance) end it "should not save to the cache if the normal save fails" do request = stub 'request', :instance => @instance, :node => nil @indirection.expects(:request).returns request @cache.expects(:save).never @terminus.expects(:save).raises "eh" expect { @indirection.save(@instance) }.to raise_error(RuntimeError, /eh/) end it "should not save to the cache if told to ignore saving to the cache" do @terminus.expects(:save) @cache.expects(:save).never @indirection.save(@instance, '/my/key', :ignore_cache_save => true) end end end describe "and removing a model instance" do before { @method = :destroy } it_should_behave_like "Indirection Delegator" it_should_behave_like "Delegation Authorizer" it_should_behave_like "Request validator" it "should return the result of removing the instance" do @terminus.stubs(:destroy).returns "yayness" expect(@indirection.destroy("/my/key")).to eq("yayness") end describe "when caching is enabled" do before do @indirection.cache_class = :cache_terminus @cache_class.expects(:new).returns(@cache) @instance.stubs(:expired?).returns false end it "should use a request instance to search in and remove objects from the cache" do destroy = stub 'destroy_request', :key => "/my/key", :node => nil find = stub 'destroy_request', :key => "/my/key", :node => nil @indirection.expects(:request).with(:destroy, "/my/key", nil, optionally(instance_of(Hash))).returns destroy @indirection.expects(:request).with(:find, "/my/key", nil, optionally(instance_of(Hash))).returns find cached = mock 'cache' @cache.expects(:find).with(find).returns cached @cache.expects(:destroy).with(destroy) @terminus.stubs(:destroy) @indirection.destroy("/my/key") end end end describe "and searching for multiple model instances" do before { @method = :search } it_should_behave_like "Indirection Delegator" it_should_behave_like "Delegation Authorizer" it_should_behave_like "Request validator" it "should set the expiration date on any instances without one set" do @terminus.stubs(:search).returns([@instance]) @indirection.expects(:expiration).returns :yay @instance.expects(:expiration).returns(nil) @instance.expects(:expiration=).with(:yay) @indirection.search("/my/key") end it "should not override an already-set expiration date on returned instances" do @terminus.stubs(:search).returns([@instance]) @indirection.expects(:expiration).never @instance.expects(:expiration).returns(:yay) @instance.expects(:expiration=).never @indirection.search("/my/key") end it "should return the results of searching in the terminus" do @terminus.expects(:search).returns([@instance]) expect(@indirection.search("/my/key")).to eq([@instance]) end end describe "and expiring a model instance" do describe "when caching is not enabled" do it "should do nothing" do @cache_class.expects(:new).never @indirection.expire("/my/key") end end describe "when caching is enabled" do before do @indirection.cache_class = :cache_terminus @cache_class.expects(:new).returns(@cache) @instance.stubs(:expired?).returns false @cached = stub 'cached', :expiration= => nil, :name => "/my/key" end it "should use a request to find within the cache" do @cache.expects(:find).with { |r| r.is_a?(Puppet::Indirector::Request) and r.method == :find } @indirection.expire("/my/key") end it "should do nothing if no such instance is cached" do @cache.expects(:find).returns nil @indirection.expire("/my/key") end it "should log when expiring a found instance" do @cache.expects(:find).returns @cached @cache.stubs(:save) Puppet.expects(:info) @indirection.expire("/my/key") end it "should set the cached instance's expiration to a time in the past" do @cache.expects(:find).returns @cached @cache.stubs(:save) @cached.expects(:expiration=).with { |t| t < Time.now } @indirection.expire("/my/key") end it "should save the now expired instance back into the cache" do @cache.expects(:find).returns @cached @cached.expects(:expiration=).with { |t| t < Time.now } @cache.expects(:save) @indirection.expire("/my/key") end it "does not expire an instance if told to skip cache saving" do @indirection.cache.expects(:find).never @indirection.cache.expects(:save).never @indirection.expire("/my/key", :ignore_cache_save => true) end it "should use a request to save the expired resource to the cache" do @cache.expects(:find).returns @cached @cached.expects(:expiration=).with { |t| t < Time.now } @cache.expects(:save).with { |r| r.is_a?(Puppet::Indirector::Request) and r.instance == @cached and r.method == :save }.returns(@cached) @indirection.expire("/my/key") end end end after :each do @indirection.delete end end describe "when managing indirection instances" do it "should allow an indirection to be retrieved by name" do @indirection = Puppet::Indirector::Indirection.new(mock('model'), :test) expect(Puppet::Indirector::Indirection.instance(:test)).to equal(@indirection) end it "should return nil when the named indirection has not been created" do expect(Puppet::Indirector::Indirection.instance(:test)).to be_nil end it "should allow an indirection's model to be retrieved by name" do mock_model = mock('model') @indirection = Puppet::Indirector::Indirection.new(mock_model, :test) expect(Puppet::Indirector::Indirection.model(:test)).to equal(mock_model) end it "should return nil when no model matches the requested name" do expect(Puppet::Indirector::Indirection.model(:test)).to be_nil end after do @indirection.delete if defined?(@indirection) end end describe "when routing to the correct the terminus class" do before do @indirection = Puppet::Indirector::Indirection.new(mock('model'), :test) @terminus = mock 'terminus' @terminus_class = stub 'terminus class', :new => @terminus Puppet::Indirector::Terminus.stubs(:terminus_class).with(:test, :default).returns(@terminus_class) end it "should fail if no terminus class can be picked" do expect { @indirection.terminus_class }.to raise_error(Puppet::DevError) end it "should choose the default terminus class if one is specified" do @indirection.terminus_class = :default expect(@indirection.terminus_class).to equal(:default) end it "should use the provided Puppet setting if told to do so" do Puppet::Indirector::Terminus.stubs(:terminus_class).with(:test, :my_terminus).returns(mock("terminus_class2")) Puppet[:node_terminus] = :my_terminus @indirection.terminus_setting = :node_terminus expect(@indirection.terminus_class).to equal(:my_terminus) end it "should fail if the provided terminus class is not valid" do Puppet::Indirector::Terminus.stubs(:terminus_class).with(:test, :nosuchclass).returns(nil) expect { @indirection.terminus_class = :nosuchclass }.to raise_error(ArgumentError) end after do @indirection.delete if defined?(@indirection) end end describe "when specifying the terminus class to use" do before do @indirection = Puppet::Indirector::Indirection.new(mock('model'), :test) @terminus = mock 'terminus' @terminus.stubs(:validate) @terminus_class = stub 'terminus class', :new => @terminus end it "should allow specification of a terminus type" do expect(@indirection).to respond_to(:terminus_class=) end it "should fail to redirect if no terminus type has been specified" do expect { @indirection.find("blah") }.to raise_error(Puppet::DevError) end it "should fail when the terminus class name is an empty string" do expect { @indirection.terminus_class = "" }.to raise_error(ArgumentError) end it "should fail when the terminus class name is nil" do expect { @indirection.terminus_class = nil }.to raise_error(ArgumentError) end it "should fail when the specified terminus class cannot be found" do Puppet::Indirector::Terminus.expects(:terminus_class).with(:test, :foo).returns(nil) expect { @indirection.terminus_class = :foo }.to raise_error(ArgumentError) end it "should select the specified terminus class if a terminus class name is provided" do Puppet::Indirector::Terminus.expects(:terminus_class).with(:test, :foo).returns(@terminus_class) expect(@indirection.terminus(:foo)).to equal(@terminus) end it "should use the configured terminus class if no terminus name is specified" do Puppet::Indirector::Terminus.stubs(:terminus_class).with(:test, :foo).returns(@terminus_class) @indirection.terminus_class = :foo expect(@indirection.terminus).to equal(@terminus) end after do @indirection.delete if defined?(@indirection) end end describe "when managing terminus instances" do before do @indirection = Puppet::Indirector::Indirection.new(mock('model'), :test) @terminus = mock 'terminus' @terminus_class = mock 'terminus class' Puppet::Indirector::Terminus.stubs(:terminus_class).with(:test, :foo).returns(@terminus_class) end it "should create an instance of the chosen terminus class" do @terminus_class.stubs(:new).returns(@terminus) expect(@indirection.terminus(:foo)).to equal(@terminus) end # Make sure it caches the terminus. it "should return the same terminus instance each time for a given name" do @terminus_class.stubs(:new).returns(@terminus) expect(@indirection.terminus(:foo)).to equal(@terminus) expect(@indirection.terminus(:foo)).to equal(@terminus) end it "should not create a terminus instance until one is actually needed" do Puppet::Indirector.expects(:terminus).never Puppet::Indirector::Indirection.new(mock('model'), :lazytest) end after do @indirection.delete end end describe "when deciding whether to cache" do before do @indirection = Puppet::Indirector::Indirection.new(mock('model'), :test) @terminus = mock 'terminus' @terminus_class = mock 'terminus class' @terminus_class.stubs(:new).returns(@terminus) Puppet::Indirector::Terminus.stubs(:terminus_class).with(:test, :foo).returns(@terminus_class) @indirection.terminus_class = :foo end it "should provide a method for setting the cache terminus class" do expect(@indirection).to respond_to(:cache_class=) end it "should fail to cache if no cache type has been specified" do expect { @indirection.cache }.to raise_error(Puppet::DevError) end it "should fail to set the cache class when the cache class name is an empty string" do expect { @indirection.cache_class = "" }.to raise_error(ArgumentError) end it "should allow resetting the cache_class to nil" do @indirection.cache_class = nil expect(@indirection.cache_class).to be_nil end it "should fail to set the cache class when the specified cache class cannot be found" do Puppet::Indirector::Terminus.expects(:terminus_class).with(:test, :foo).returns(nil) expect { @indirection.cache_class = :foo }.to raise_error(ArgumentError) end after do @indirection.delete end end describe "when using a cache" do before :each do @terminus_class = mock 'terminus_class' @terminus = mock 'terminus' @terminus_class.stubs(:new).returns(@terminus) @cache = mock 'cache' @cache_class = mock 'cache_class' Puppet::Indirector::Terminus.stubs(:terminus_class).with(:test, :cache_terminus).returns(@cache_class) Puppet::Indirector::Terminus.stubs(:terminus_class).with(:test, :test_terminus).returns(@terminus_class) @indirection = Puppet::Indirector::Indirection.new(mock('model'), :test) @indirection.terminus_class = :test_terminus end describe "and managing the cache terminus" do it "should not create a cache terminus at initialization" do # This is weird, because all of the code is in the setup. If we got # new called on the cache class, we'd get an exception here. end it "should reuse the cache terminus" do @cache_class.expects(:new).returns(@cache) @indirection.cache_class = :cache_terminus expect(@indirection.cache).to equal(@cache) expect(@indirection.cache).to equal(@cache) end end describe "and saving" do end describe "and finding" do end after :each do @indirection.delete end end end