# # Copyright:: Copyright 2008-2016, Chef, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # require "spec_helper" require "train" require "train-rest" class RestResourceByQuery < Chef::Resource use "core::rest_resource" provides :rest_resource_by_query, target_mode: true property :address, String, required: true property :prefix, Integer, required: true property :gateway, String rest_api_collection "/api/v1/addresses" rest_api_document "/api/v1/address/?ip={address}" rest_property_map({ address: "address", prefix: "prefix", gateway: "gateway", }) end class RestResourceByPath < RestResourceByQuery provides :rest_resource_by_path, target_mode: true rest_api_document "/api/v1/address/{address}" end describe "rest_resource using query-based addressing" do let(:train) { Train.create( "rest", { endpoint: "https://api.example.com/api/v1/", debug_rest: true, logger: Chef::Log, } ).connection } let(:run_context) do cookbook_collection = Chef::CookbookCollection.new([]) node = Chef::Node.new node.name "node1" events = Chef::EventDispatch::Dispatcher.new Chef::RunContext.new(node, cookbook_collection, events) end let(:resource) do RestResourceByQuery.new("set_address", run_context).tap do |resource| resource.address = "192.0.2.1" resource.prefix = 24 resource.action :configure end end let(:provider) do resource.provider_for_action(:configure).tap do |provider| provider.current_resource = resource # for some stubby tests that don't call LCR allow(provider).to receive(:api_connection).and_return(train) end end before(:each) do allow(Chef::Provider).to receive(:new).and_return(provider) end it "should include :configure action" do expect(provider).to respond_to(:action_configure) end it "should include :delete action" do expect(provider).to respond_to(:action_delete) end it "should include :nothing action" do expect(provider).to respond_to(:action_nothing) end it "sets the default action as :configure" do expect(resource.action).to eql([:configure]) end it "supports :configure action" do expect { resource.action :configure }.not_to raise_error end it "supports :delete action" do expect { resource.action :delete }.not_to raise_error end it "should mixin RestResourceDSL" do expect(resource.class.ancestors).to include(Chef::DSL::RestResource) end describe "#rest_postprocess" do before do provider.singleton_class.send(:public, :rest_postprocess) end it "should have a default rest_postprocess implementation" do expect(provider).to respond_to(:rest_postprocess) end it "should have a non-mutating rest_postprocess implementation" do response = "{ data: nil }" expect(provider.rest_postprocess(response.dup)).to eq(response) end end describe "#rest_errorhandler" do before do provider.singleton_class.send(:public, :rest_errorhandler) end it "should have a default rest_errorhandler implementation" do expect(provider).to respond_to(:rest_errorhandler) end it "should have a non-mutating rest_errorhandler implementation" do error_obj = StandardError.new expect(provider.rest_errorhandler(error_obj.dup)).to eq(error_obj) end end describe "#required_properties" do before do provider.singleton_class.send(:public, :required_properties) end it "should include required properties only" do expect(provider.required_properties).to contain_exactly(:address, :prefix) end end describe "#property_map" do before do provider.singleton_class.send(:public, :property_map) end it "should map resource properties to values properly" do expect(provider.property_map).to eq({ address: "192.0.2.1", prefix: 24, gateway: nil, name: "set_address", }) end end describe "#rest_url_collection" do before do provider.singleton_class.send(:public, :rest_url_collection) end it "should return collection URLs properly" do expect(provider.rest_url_collection).to eq("/api/v1/addresses") end end describe "#rest_url_document" do before do provider.singleton_class.send(:public, :rest_url_document) end it "should apply URI templates to document URLs using query syntax properly" do expect(provider.rest_url_document).to eq("/api/v1/address/?ip=192.0.2.1") end end # TODO: Test with path-style URLs describe "#rest_identity_implicit" do before do provider.singleton_class.send(:public, :rest_identity_implicit) end it "should return implicit identity properties properly" do expect(provider.rest_identity_implicit).to eq({ "ip" => :address }) end end describe "#rest_identity_values" do before do provider.singleton_class.send(:public, :rest_identity_values) end it "should return implicit identity properties and values properly" do expect(provider.rest_identity_values).to eq({ "ip" => "192.0.2.1" }) end end # TODO: changed_value # TODO: load_current_value # this might be a functional test, but it runs on any O/S so I leave it here describe "when managing a resource" do before { WebMock.disable_net_connect! } let(:addresses_exists) { JSON.generate([{ "address": "192.0.2.1" }]) } let(:addresses_other) { JSON.generate([{ "address": "172.16.32.85" }]) } let(:address_exists) { JSON.generate({ "address": "192.0.2.1", "prefix": 24, "gateway": "192.0.2.1" }) } let(:prefix_wrong) { JSON.generate({ "address": "192.0.2.1", "prefix": 25, "gateway": "192.0.2.1" }) } it "should be idempotent" do stub_request(:get, "https://api.example.com/api/v1/addresses") .to_return(status: 200, body: addresses_exists, headers: { "Content-Type" => "application/json" }) stub_request(:get, "https://api.example.com/api/v1/address/?ip=192.0.2.1") .to_return(status: 200, body: address_exists, headers: { "Content-Type" => "application/json" }) resource.run_action(:configure) expect(resource.updated_by_last_action?).to be false end it "should PATCH if a property is incorrect" do stub_request(:get, "https://api.example.com/api/v1/addresses") .to_return(status: 200, body: addresses_exists, headers: { "Content-Type" => "application/json" }) stub_request(:get, "https://api.example.com/api/v1/address/?ip=192.0.2.1") .to_return(status: 200, body: prefix_wrong, headers: { "Content-Type" => "application/json" }) stub_request(:patch, "https://api.example.com/api/v1/address/?ip=192.0.2.1") .with( body: "{\"address\":\"192.0.2.1\",\"prefix\":25}", headers: { "Accept" => "application/json", "Content-Type" => "application/json", } ) .to_return(status: 200, body: address_exists, headers: { "Content-Type" => "application/json" }) resource.run_action(:configure) expect(resource.updated_by_last_action?).to be true end it "should POST if there's no resources at all" do stub_request(:get, "https://api.example.com/api/v1/addresses") .to_return(status: 200, body: "[]", headers: { "Content-Type" => "application/json" }) stub_request(:post, "https://api.example.com/api/v1/addresses") .with( body: "{\"address\":\"192.0.2.1\",\"prefix\":24,\"ip\":\"192.0.2.1\"}" ) .to_return(status: 200, body: address_exists, headers: { "Content-Type" => "application/json" }) resource.run_action(:configure) expect(resource.updated_by_last_action?).to be true end it "should POST if the specific resource does not exist" do stub_request(:get, "https://api.example.com/api/v1/addresses") .to_return(status: 200, body: addresses_other, headers: { "Content-Type" => "application/json" }) stub_request(:get, "https://api.example.com/api/v1/address/?ip=192.0.2.1") .to_return(status: 404, body: "", headers: {}) stub_request(:post, "https://api.example.com/api/v1/addresses") .with( body: "{\"address\":\"192.0.2.1\",\"prefix\":24,\"ip\":\"192.0.2.1\"}" ) .to_return(status: 200, body: address_exists, headers: { "Content-Type" => "application/json" }) resource.run_action(:configure) expect(resource.updated_by_last_action?).to be true end it "should be idempotent if the resouces needs deleting and there are no resources at all" do stub_request(:get, "https://api.example.com/api/v1/addresses") .to_return(status: 200, body: "[]", headers: { "Content-Type" => "application/json" }) resource.run_action(:delete) expect(resource.updated_by_last_action?).to be false end it "should be idempotent if the resource doesn't exist" do stub_request(:get, "https://api.example.com/api/v1/addresses") .to_return(status: 200, body: addresses_other, headers: { "Content-Type" => "application/json" }) stub_request(:get, "https://api.example.com/api/v1/address/?ip=192.0.2.1") .to_return(status: 404, body: "", headers: {}) resource.run_action(:delete) expect(resource.updated_by_last_action?).to be false end it "should DELETE the resource if it exists and matches" do stub_request(:get, "https://api.example.com/api/v1/addresses") .to_return(status: 200, body: addresses_exists, headers: { "Content-Type" => "application/json" }) stub_request(:get, "https://api.example.com/api/v1/address/?ip=192.0.2.1") .to_return(status: 200, body: address_exists, headers: { "Content-Type" => "application/json" }) stub_request(:delete, "https://api.example.com/api/v1/address/?ip=192.0.2.1") .to_return(status: 200, body: "", headers: {}) resource.run_action(:delete) expect(resource.updated_by_last_action?).to be true end it "should DELETE the resource if it exists and doesn't match" do stub_request(:get, "https://api.example.com/api/v1/addresses") .to_return(status: 200, body: addresses_exists, headers: { "Content-Type" => "application/json" }) stub_request(:get, "https://api.example.com/api/v1/address/?ip=192.0.2.1") .to_return(status: 200, body: prefix_wrong, headers: { "Content-Type" => "application/json" }) stub_request(:delete, "https://api.example.com/api/v1/address/?ip=192.0.2.1") .to_return(status: 200, body: "", headers: {}) resource.run_action(:delete) expect(resource.updated_by_last_action?).to be true end end end describe "rest_resource using path-based addressing" do let(:train) { Train.create( "rest", { endpoint: "https://api.example.com/api/v1/", debug_rest: true, logger: Chef::Log, } ).connection } let(:run_context) do cookbook_collection = Chef::CookbookCollection.new([]) node = Chef::Node.new node.name "node1" events = Chef::EventDispatch::Dispatcher.new Chef::RunContext.new(node, cookbook_collection, events) end let(:resource) do RestResourceByPath.new("set_address", run_context).tap do |resource| resource.address = "192.0.2.1" resource.prefix = 24 resource.action :configure end end let(:provider) do resource.provider_for_action(:configure).tap do |provider| provider.current_resource = resource # for some stubby tests that don't call LCR allow(provider).to receive(:api_connection).and_return(train) end end before(:each) do allow(Chef::Provider).to receive(:new).and_return(provider) end describe "#rest_url_document" do before do provider.singleton_class.send(:public, :rest_url_document) end it "should apply URI templates to document URLs using path syntax properly" do expect(provider.rest_url_document).to eq("/api/v1/address/192.0.2.1") end end describe "#rest_identity_implicit" do before do provider.singleton_class.send(:public, :rest_identity_implicit) end it "should return implicit identity properties properly" do expect(provider.rest_identity_implicit).to eq({ "address" => :address }) end end describe "#rest_identity_values" do before do provider.singleton_class.send(:public, :rest_identity_values) end it "should return implicit identity properties and values properly" do expect(provider.rest_identity_values).to eq({ "address" => "192.0.2.1" }) end end end