# # Author:: Bryan McLellan (btm@loftninjas.org) # Copyright:: Copyright 2009-2020, Bryan McLellan # 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" describe Chef::Provider::Cron do let(:logger) { double("Mixlib::Log::Child").as_null_object } before do @node = Chef::Node.new @events = Chef::EventDispatch::Dispatcher.new @run_context = Chef::RunContext.new(@node, {}, @events) allow(@run_context).to receive(:logger).and_return(logger) @new_resource = Chef::Resource::Cron.new("cronhole some stuff", @run_context) @new_resource.user "root" @new_resource.minute "30" @new_resource.command "/bin/true" @provider = Chef::Provider::Cron.new(@new_resource, @run_context) end describe "when with special time string" do before do @new_resource.time :reboot @provider = Chef::Provider::Cron.new(@new_resource, @run_context) end context "with a matching entry in the user's crontab" do before :each do allow(@provider).to receive(:read_crontab).and_return(<<~CRONTAB) 0 2 * * * /some/other/command # Chef Name: cronhole some stuff @reboot /bin/true param1 param2 # Chef Name: something else 2 * 1 * * /bin/false # Another comment CRONTAB end it "should set cron_exists" do @provider.load_current_resource expect(@provider.cron_exists).to eq(true) expect(@provider.cron_empty).to eq(false) end it "should pull the details out of the cron line" do cron = @provider.load_current_resource expect(cron.time).to eq(:reboot) expect(cron.command).to eq("/bin/true param1 param2") end it "should pull env vars out" do allow(@provider).to receive(:read_crontab).and_return(<<~CRONTAB) 0 2 * * * /some/other/command # Chef Name: cronhole some stuff MAILTO=foo@example.com SHELL=/bin/foosh PATH=/bin:/foo HOME=/home/foo @reboot /bin/true param1 param2 # Chef Name: something else 2 * 1 * * /bin/false # Another comment CRONTAB cron = @provider.load_current_resource expect(cron.mailto).to eq("foo@example.com") expect(cron.shell).to eq("/bin/foosh") expect(cron.path).to eq("/bin:/foo") expect(cron.home).to eq("/home/foo") expect(cron.time).to eq(:reboot) expect(cron.command).to eq("/bin/true param1 param2") end it "should parse and load generic and standard environment variables from cron entry" do allow(@provider).to receive(:read_crontab).and_return(<<~CRONTAB) # Chef Name: cronhole some stuff MAILTO=warn@example.com TEST=lol FLAG=1 @reboot /bin/true CRONTAB cron = @provider.load_current_resource expect(cron.mailto).to eq("warn@example.com") expect(cron.environment).to eq({ "TEST" => "lol", "FLAG" => "1" }) end it "should not break with variables that match the cron resource internals" do allow(@provider).to receive(:read_crontab).and_return(<<~CRONTAB) # Chef Name: cronhole some stuff MINUTE=40 REBOOT=midnight TEST=lol ENVIRONMENT=production @reboot /bin/true CRONTAB cron = @provider.load_current_resource expect(cron.time).to eq(:reboot) expect(cron.environment).to eq({ "MINUTE" => "40", "REBOOT" => "midnight", "TEST" => "lol", "ENVIRONMENT" => "production" }) end it "should report the match" do expect(logger).to receive(:trace).with("Found cron '#{@new_resource.name}'") @provider.load_current_resource end describe "action_create" do before :each do allow(@provider).to receive(:write_crontab) allow(@provider).to receive(:read_crontab).and_return(nil) end context "when there is no existing crontab" do before :each do @provider.cron_exists = false @provider.cron_empty = true end it "should create a crontab with the entry" do expect(@provider).to receive(:write_crontab).with(<<~ENDCRON) # Chef Name: cronhole some stuff @reboot /bin/true ENDCRON @provider.run_action(:create) end end end end end describe "when examining the current system state" do context "with no crontab for the user" do before :each do allow(@provider).to receive(:read_crontab).and_return(nil) end it "should set cron_empty" do @provider.load_current_resource expect(@provider.cron_empty).to eq(true) expect(@provider.cron_exists).to eq(false) end it "should report an empty crontab" do expect(logger).to receive(:trace).with("Cron empty for '#{@new_resource.user}'") @provider.load_current_resource end end context "with no matching entry in the user's crontab" do before :each do allow(@provider).to receive(:read_crontab).and_return(<<~CRONTAB) 0 2 * * * /some/other/command # Chef Name: something else * 5 * * * /bin/true # Another comment CRONTAB end it "should not set cron_exists or cron_empty" do @provider.load_current_resource expect(@provider.cron_exists).to eq(false) expect(@provider.cron_empty).to eq(false) end it "should report no entry found" do expect(logger).to receive(:trace).with("Cron '#{@new_resource.name}' not found") @provider.load_current_resource end it "should not fail if there's an existing cron with a numerical argument" do allow(@provider).to receive(:read_crontab).and_return(<<~CRONTAB) # Chef Name: foo[bar] (baz) 21 */4 * * * some_prog 1234567 CRONTAB expect do @provider.load_current_resource end.not_to raise_error end end context "with a matching entry in the user's crontab" do before :each do allow(@provider).to receive(:read_crontab).and_return(<<~CRONTAB) 0 2 * * * /some/other/command # Chef Name: cronhole some stuff * 5 * 1 * /bin/true param1 param2 # Chef Name: something else 2 * 1 * * /bin/false # Another comment CRONTAB end it "should set cron_exists" do @provider.load_current_resource expect(@provider.cron_exists).to eq(true) expect(@provider.cron_empty).to eq(false) end it "should pull the details out of the cron line" do cron = @provider.load_current_resource expect(cron.minute).to eq("*") expect(cron.hour).to eq("5") expect(cron.day).to eq("*") expect(cron.month).to eq("1") expect(cron.weekday).to eq("*") expect(cron.time).to eq(nil) expect(cron.command).to eq("/bin/true param1 param2") end it "should pull env vars out" do allow(@provider).to receive(:read_crontab).and_return(<<~CRONTAB) 0 2 * * * /some/other/command # Chef Name: cronhole some stuff MAILTO=foo@example.com SHELL=/bin/foosh PATH=/bin:/foo HOME=/home/foo * 5 * 1 * /bin/true param1 param2 # Chef Name: something else 2 * 1 * * /bin/false # Another comment CRONTAB cron = @provider.load_current_resource expect(cron.mailto).to eq("foo@example.com") expect(cron.shell).to eq("/bin/foosh") expect(cron.path).to eq("/bin:/foo") expect(cron.home).to eq("/home/foo") expect(cron.minute).to eq("*") expect(cron.hour).to eq("5") expect(cron.day).to eq("*") expect(cron.month).to eq("1") expect(cron.weekday).to eq("*") expect(cron.time).to eq(nil) expect(cron.command).to eq("/bin/true param1 param2") end it "should parse and load generic and standard environment variables from cron entry" do allow(@provider).to receive(:read_crontab).and_return(<<~CRONTAB) # Chef Name: cronhole some stuff MAILTO=warn@example.com TEST=lol FLAG=1 * 5 * * * /bin/true CRONTAB cron = @provider.load_current_resource expect(cron.mailto).to eq("warn@example.com") expect(cron.environment).to eq({ "TEST" => "lol", "FLAG" => "1" }) end it "should not break with variabels that match the cron resource internals" do allow(@provider).to receive(:read_crontab).and_return(<<~CRONTAB) # Chef Name: cronhole some stuff MINUTE=40 HOUR=midnight TEST=lol ENVIRONMENT=production * 5 * * * /bin/true CRONTAB cron = @provider.load_current_resource expect(cron.minute).to eq("*") expect(cron.hour).to eq("5") expect(cron.environment).to eq({ "MINUTE" => "40", "HOUR" => "midnight", "TEST" => "lol", "ENVIRONMENT" => "production" }) end it "should report the match" do expect(logger).to receive(:trace).with("Found cron '#{@new_resource.name}'") @provider.load_current_resource end end context "with a matching entry in the user's crontab using month names and weekday names (#CHEF-3178)" do before :each do allow(@provider).to receive(:read_crontab).and_return(<<~CRONTAB) 0 2 * * * /some/other/command # Chef Name: cronhole some stuff * 5 * Jan Mon /bin/true param1 param2 # Chef Name: something else 2 * 1 * * /bin/false # Another comment CRONTAB end it "should set cron_exists" do @provider.load_current_resource expect(@provider.cron_exists).to eq(true) expect(@provider.cron_empty).to eq(false) end it "should pull the details out of the cron line" do cron = @provider.load_current_resource expect(cron.minute).to eq("*") expect(cron.hour).to eq("5") expect(cron.day).to eq("*") expect(cron.month).to eq("Jan") expect(cron.weekday).to eq("Mon") expect(cron.command).to eq("/bin/true param1 param2") end it "should report the match" do expect(logger).to receive(:trace).with("Found cron '#{@new_resource.name}'") @provider.load_current_resource end end context "with a matching entry without a crontab line" do it "should set cron_exists and leave current_resource values at defaults" do allow(@provider).to receive(:read_crontab).and_return(<<~CRONTAB) 0 2 * * * /some/other/command # Chef Name: cronhole some stuff CRONTAB cron = @provider.load_current_resource expect(@provider.cron_exists).to eq(true) expect(cron.minute).to eq("*") expect(cron.hour).to eq("*") expect(cron.day).to eq("*") expect(cron.month).to eq("*") expect(cron.weekday).to eq("*") expect(cron.time).to eq(nil) expect(cron.command).to eq(nil) end it "should not pick up a commented out crontab line" do allow(@provider).to receive(:read_crontab).and_return(<<~CRONTAB) 0 2 * * * /some/other/command # Chef Name: cronhole some stuff #* 5 * 1 * /bin/true param1 param2 CRONTAB cron = @provider.load_current_resource expect(@provider.cron_exists).to eq(true) expect(cron.minute).to eq("*") expect(cron.hour).to eq("*") expect(cron.day).to eq("*") expect(cron.month).to eq("*") expect(cron.weekday).to eq("*") expect(cron.time).to eq(nil) expect(cron.command).to eq(nil) end it "should not pick up a later crontab entry" do allow(@provider).to receive(:read_crontab).and_return(<<~CRONTAB) 0 2 * * * /some/other/command # Chef Name: cronhole some stuff #* 5 * 1 * /bin/true param1 param2 # Chef Name: something else 2 * 1 * * /bin/false # Another comment CRONTAB cron = @provider.load_current_resource expect(@provider.cron_exists).to eq(true) expect(cron.minute).to eq("*") expect(cron.hour).to eq("*") expect(cron.day).to eq("*") expect(cron.month).to eq("*") expect(cron.weekday).to eq("*") expect(cron.time).to eq(nil) expect(cron.command).to eq(nil) end end end describe "cron_different?" do before :each do @current_resource = Chef::Resource::Cron.new("cronhole some stuff") @current_resource.user "root" @current_resource.minute "30" @current_resource.command "/bin/true" @provider.current_resource = @current_resource end %i{minute hour day month weekday command mailto path shell home}.each do |property| it "should return true if #{property} doesn't match" do @new_resource.send(property, "1") # we use 1 in order to pass resource validation. We're just using a value that's different. expect(@provider.cron_different?).to eql(true) end end it "should return true if special time string doesn't match" do @new_resource.send(:time, :reboot) expect(@provider.cron_different?).to eql(true) end it "should return true if environment doesn't match" do @new_resource.environment "FOO" => "something_else" expect(@provider.cron_different?).to eql(true) end it "should return true if mailto doesn't match" do @current_resource.mailto "foo@bar.com" @new_resource.mailto(nil) expect(@provider.cron_different?).to eql(true) end it "should return false if the objects are identical" do expect(@provider.cron_different?).to eq(false) end end describe "action_create" do before :each do allow(@provider).to receive(:write_crontab) allow(@provider).to receive(:read_crontab).and_return(nil) end context "when there is no existing crontab" do before :each do @provider.cron_exists = false @provider.cron_empty = true end it "should create a crontab with the entry" do expect(@provider).to receive(:write_crontab).with(<<~ENDCRON) # Chef Name: cronhole some stuff 30 * * * * /bin/true ENDCRON @provider.run_action(:create) end it "should include env variables that are set" do @new_resource.mailto "foo@example.com" @new_resource.path "/usr/bin:/my/custom/path" @new_resource.shell "/bin/foosh" @new_resource.home "/home/foo" @new_resource.environment "TEST" => "LOL" expect(@provider).to receive(:write_crontab).with(<<~ENDCRON) # Chef Name: cronhole some stuff MAILTO="foo@example.com" PATH="/usr/bin:/my/custom/path" SHELL="/bin/foosh" HOME="/home/foo" TEST=LOL 30 * * * * /bin/true ENDCRON @provider.run_action(:create) end it "should mark the resource as updated" do @provider.run_action(:create) expect(@new_resource).to be_updated_by_last_action end it "should log the action" do expect(logger).to receive(:info).with("cron[cronhole some stuff] added crontab entry") @provider.run_action(:create) end end context "when there is a crontab with no matching section" do before :each do @provider.cron_exists = false allow(@provider).to receive(:read_crontab).and_return(<<~CRONTAB) 0 2 * * * /some/other/command # Chef Name: something else 2 * 1 * * /bin/false # Another comment CRONTAB end it "should add the entry to the crontab" do expect(@provider).to receive(:write_crontab).with(<<~ENDCRON) 0 2 * * * /some/other/command # Chef Name: something else 2 * 1 * * /bin/false # Another comment # Chef Name: cronhole some stuff 30 * * * * /bin/true ENDCRON @provider.run_action(:create) end it "should include env variables that are set" do @new_resource.mailto "foo@example.com" @new_resource.path "/usr/bin:/my/custom/path" @new_resource.shell "/bin/foosh" @new_resource.home "/home/foo" @new_resource.environment "TEST" => "LOL" expect(@provider).to receive(:write_crontab).with(<<~ENDCRON) 0 2 * * * /some/other/command # Chef Name: something else 2 * 1 * * /bin/false # Another comment # Chef Name: cronhole some stuff MAILTO="foo@example.com" PATH="/usr/bin:/my/custom/path" SHELL="/bin/foosh" HOME="/home/foo" TEST=LOL 30 * * * * /bin/true ENDCRON @provider.run_action(:create) end it "should mark the resource as updated" do @provider.run_action(:create) expect(@new_resource).to be_updated_by_last_action end it "should log the action" do expect(logger).to receive(:info).with("cron[cronhole some stuff] added crontab entry") @provider.run_action(:create) end end context "when there is a crontab with a matching but different section" do before :each do @provider.cron_exists = true allow(@provider).to receive(:cron_different?).and_return(true) allow(@provider).to receive(:read_crontab).and_return(<<~CRONTAB) 0 2 * * * /some/other/command # Chef Name: cronhole some stuff 30 * * 3 * /bin/true # Chef Name: something else 2 * 1 * * /bin/false # Another comment CRONTAB end it "should update the crontab entry" do expect(@provider).to receive(:write_crontab).with(<<~ENDCRON) 0 2 * * * /some/other/command # Chef Name: cronhole some stuff 30 * * * * /bin/true # Chef Name: something else 2 * 1 * * /bin/false # Another comment ENDCRON @provider.run_action(:create) end it "should include env variables that are set" do @new_resource.mailto "foo@example.com" @new_resource.path "/usr/bin:/my/custom/path" @new_resource.shell "/bin/foosh" @new_resource.home "/home/foo" @new_resource.environment "TEST" => "LOL" expect(@provider).to receive(:write_crontab).with(<<~ENDCRON) 0 2 * * * /some/other/command # Chef Name: cronhole some stuff MAILTO="foo@example.com" PATH="/usr/bin:/my/custom/path" SHELL="/bin/foosh" HOME="/home/foo" TEST=LOL 30 * * * * /bin/true # Chef Name: something else 2 * 1 * * /bin/false # Another comment ENDCRON @provider.run_action(:create) end it "should mark the resource as updated" do @provider.run_action(:create) expect(@new_resource).to be_updated_by_last_action end it "should log the action" do expect(logger).to receive(:info).with("cron[cronhole some stuff] updated crontab entry") @provider.run_action(:create) end end context "when there is a crontab with a matching section with no crontab line in it" do before :each do @provider.cron_exists = true allow(@provider).to receive(:cron_different?).and_return(true) end it "should add the crontab to the entry" do allow(@provider).to receive(:read_crontab).and_return(<<~CRONTAB) 0 2 * * * /some/other/command # Chef Name: cronhole some stuff CRONTAB expect(@provider).to receive(:write_crontab).with(<<~ENDCRON) 0 2 * * * /some/other/command # Chef Name: cronhole some stuff 30 * * * * /bin/true ENDCRON @provider.run_action(:create) end it "should not blat any following entries" do allow(@provider).to receive(:read_crontab).and_return(<<~CRONTAB) 0 2 * * * /some/other/command # Chef Name: cronhole some stuff #30 * * * * /bin/true # Chef Name: something else 2 * 1 * * /bin/false # Another comment CRONTAB expect(@provider).to receive(:write_crontab).with(<<~ENDCRON) 0 2 * * * /some/other/command # Chef Name: cronhole some stuff 30 * * * * /bin/true #30 * * * * /bin/true # Chef Name: something else 2 * 1 * * /bin/false # Another comment ENDCRON @provider.run_action(:create) end it "should handle env vars with no crontab" do allow(@provider).to receive(:read_crontab).and_return(<<~CRONTAB) 0 2 * * * /some/other/command # Chef Name: cronhole some stuff MAILTO=bar@example.com PATH=/usr/bin:/my/custom/path SHELL=/bin/barsh HOME=/home/foo # Chef Name: something else 2 * 1 * * /bin/false # Another comment CRONTAB @new_resource.mailto "foo@example.com" @new_resource.path "/usr/bin:/my/custom/path" @new_resource.shell "/bin/foosh" @new_resource.home "/home/foo" expect(@provider).to receive(:write_crontab).with(<<~ENDCRON) 0 2 * * * /some/other/command # Chef Name: cronhole some stuff MAILTO="foo@example.com" PATH="/usr/bin:/my/custom/path" SHELL="/bin/foosh" HOME="/home/foo" 30 * * * * /bin/true # Chef Name: something else 2 * 1 * * /bin/false # Another comment ENDCRON @provider.run_action(:create) end end context "when there is a crontab with a matching and identical section" do context "when environment variable is not used" do before :each do @provider.cron_exists = true allow(@provider).to receive(:cron_different?).and_return(false) allow(@provider).to receive(:read_crontab).and_return(<<~CRONTAB) 0 2 * * * /some/other/command # Chef Name: cronhole some stuff SHELL=/bash * 5 * * * /bin/true # Another comment CRONTAB end it "should not update the crontab" do expect(@provider).not_to receive(:write_crontab) @provider.run_action(:create) end it "should not mark the resource as updated" do @provider.run_action(:create) expect(@new_resource).not_to be_updated_by_last_action end it "should log nothing changed" do expect(logger).to receive(:trace).with("Found cron '#{@new_resource.name}'") expect(logger).to receive(:trace).with("Skipping existing cron entry '#{@new_resource.name}'") @provider.run_action(:create) end end context "when environment variable is used" do before :each do @provider.cron_exists = true allow(@provider).to receive(:read_crontab).and_return(<<~CRONTAB) 0 2 * * * /some/other/command # Chef Name: cronhole some stuff SHELL=/bash ENV=environment 30 * * * * /bin/true # Another comment CRONTAB end context "contains an entry that can also be specified as a `property`" do before :each do @new_resource.environment = { "SHELL" => "/bash", "ENV" => "environment" } end it "should raise a warning for idempotency" do expect(logger).to receive(:warn).with("cronhole some stuff: the environment property contains the 'SHELL' variable, which should be set separately as a property.") @provider.run_action(:create) end it "should not update the crontab" do expect(@provider).not_to receive(:write_crontab) @provider.run_action(:create) end it "should not mark the resource as updated" do expect(@new_resource).not_to be_updated_by_last_action @provider.run_action(:create) end end context "contains an entry that cannot be specified as a `property`" do before :each do @new_resource.environment = { "ENV" => "environment" } @new_resource.shell "/bash" end it "should not raise a warning for idempotency" do expect(logger).not_to receive(:warn).with("cronhole some stuff: the environment property contains the 'SHELL' variable, which should be set separately as a property.") @provider.run_action(:create) end it "should not update the crontab" do expect(@provider).not_to receive(:write_crontab) @provider.run_action(:create) end it "should not mark the resource as updated" do @provider.run_action(:create) expect(@new_resource).not_to be_updated_by_last_action end end end context "when environment variable is used with property" do before :each do @provider.cron_exists = true allow(@provider).to receive(:read_crontab).and_return(<<~CRONTAB) 0 2 * * * /some/other/command # Chef Name: cronhole some stuff SHELL=/bash ENV=environment 30 * * * * /bin/true # Another comment CRONTAB end context "when environment variable is same as property" do it "should throw an error" do @new_resource.shell "/bash" @new_resource.environment "SHELL" => "/bash" expect do @provider.run_action(:create) end.to raise_error(Chef::Exceptions::Cron, /cronhole some stuff: the 'SHELL' property is set and environment property also contains the 'SHELL' variable. Remove the variable from the environment property./) end end context "when environment variable is different from property" do it "should not update the crontab" do @new_resource.shell "/bash" @new_resource.environment "ENV" => "environment" expect(@provider).not_to receive(:write_crontab) @provider.run_action(:create) end it "should not mark the resource as updated" do @new_resource.shell "/bash" @new_resource.environment "ENV" => "environment" @provider.run_action(:create) expect(@new_resource).not_to be_updated_by_last_action end end end end end describe "action_delete" do before :each do allow(@provider).to receive(:write_crontab) allow(@provider).to receive(:read_crontab).and_return(nil) end context "when the user's crontab has no matching section" do before :each do @provider.cron_exists = false end it "should do nothing" do expect(@provider).not_to receive(:write_crontab) expect(logger).not_to receive(:info) @provider.run_action(:delete) end it "should not mark the resource as updated" do @provider.run_action(:delete) expect(@new_resource).not_to be_updated_by_last_action end end context "when the user has a crontab with a matching section" do before :each do @provider.cron_exists = true allow(@provider).to receive(:read_crontab).and_return(<<~CRONTAB) 0 2 * * * /some/other/command # Chef Name: cronhole some stuff 30 * * 3 * /bin/true # Chef Name: something else 2 * 1 * * /bin/false # Another comment CRONTAB end it "should remove the entry" do expect(@provider).to receive(:write_crontab).with(<<~ENDCRON) 0 2 * * * /some/other/command # Chef Name: something else 2 * 1 * * /bin/false # Another comment ENDCRON @provider.run_action(:delete) end it "should remove any env vars with the entry" do allow(@provider).to receive(:read_crontab).and_return(<<~CRONTAB) 0 2 * * * /some/other/command # Chef Name: cronhole some stuff MAILTO=foo@example.com FOO=test 30 * * 3 * /bin/true # Chef Name: something else 2 * 1 * * /bin/false # Another comment CRONTAB expect(@provider).to receive(:write_crontab).with(<<~ENDCRON) 0 2 * * * /some/other/command # Chef Name: something else 2 * 1 * * /bin/false # Another comment ENDCRON @provider.run_action(:delete) end it "should mark the resource as updated" do @provider.run_action(:delete) expect(@new_resource).to be_updated_by_last_action end it "should log the action" do expect(logger).to receive(:info).with("#{@new_resource} deleted crontab entry") @provider.run_action(:delete) end end context "when the crontab has a matching section with no crontab line" do before :each do @provider.cron_exists = true end it "should remove the section" do allow(@provider).to receive(:read_crontab).and_return(<<~CRONTAB) 0 2 * * * /some/other/command # Chef Name: cronhole some stuff CRONTAB expect(@provider).to receive(:write_crontab).with(<<~ENDCRON) 0 2 * * * /some/other/command ENDCRON @provider.run_action(:delete) end it "should not blat following sections" do allow(@provider).to receive(:read_crontab).and_return(<<~CRONTAB) 0 2 * * * /some/other/command # Chef Name: cronhole some stuff #30 * * 3 * /bin/true # Chef Name: something else 2 * 1 * * /bin/false # Another comment CRONTAB expect(@provider).to receive(:write_crontab).with(<<~ENDCRON) 0 2 * * * /some/other/command #30 * * 3 * /bin/true # Chef Name: something else 2 * 1 * * /bin/false # Another comment ENDCRON @provider.run_action(:delete) end it "should remove any envvars with the section" do allow(@provider).to receive(:read_crontab).and_return(<<~CRONTAB) 0 2 * * * /some/other/command # Chef Name: cronhole some stuff MAILTO=foo@example.com #30 * * 3 * /bin/true # Chef Name: something else 2 * 1 * * /bin/false # Another comment CRONTAB expect(@provider).to receive(:write_crontab).with(<<~ENDCRON) 0 2 * * * /some/other/command #30 * * 3 * /bin/true # Chef Name: something else 2 * 1 * * /bin/false # Another comment ENDCRON @provider.run_action(:delete) end end end describe "read_crontab" do before :each do @stdout = <<~CRONTAB 0 2 * * * /some/other/command # Chef Name: something else * 5 * * * /bin/true # Another comment CRONTAB @status = double("Status", exitstatus: 0, stdout: @stdout) allow(@provider).to receive(:shell_out!).and_return(@status) end it "should call crontab -l with the user" do expect(@provider).to receive(:shell_out!).with("crontab -l -u #{@new_resource.user}", returns: [0, 1]).and_return(@status) @provider.send(:read_crontab) end it "should return the contents of the crontab" do crontab = @provider.send(:read_crontab) expect(crontab).to eq <<~CRONTAB 0 2 * * * /some/other/command # Chef Name: something else * 5 * * * /bin/true # Another comment CRONTAB end it "should return nil if the user has no crontab" do @status = double("Status", exitstatus: 1, stdout: "") allow(@provider).to receive(:shell_out!).and_return(@status) expect(@provider.send(:read_crontab)).to eq(nil) end it "should raise an exception if another error occurs" do @status = double("Status", exitstatus: 2) allow(@provider).to receive(:shell_out!).and_raise(Mixlib::ShellOut::ShellCommandFailed) expect { @provider.send(:read_crontab) }.to raise_error(Chef::Exceptions::Cron) end end describe "write_crontab" do before :each do @status = double("Status", exitstatus: 0) allow(@provider).to receive(:shell_out!).and_return(@status) end it "should call crontab for the user" do expect(@provider).to receive(:shell_out!).with("crontab -u #{@new_resource.user} -", input: "Foo").and_return(@status) @provider.send(:write_crontab, "Foo") end it "should raise an exception if the command returns non-zero" do @status = double("Status", exitstatus: 1) allow(@provider).to receive(:shell_out!).and_raise(Mixlib::ShellOut::ShellCommandFailed) expect do @provider.send(:write_crontab, "Foo") end.to raise_error(Chef::Exceptions::Cron) end end describe "weekday_in_crontab" do context "when weekday is symbol" do it "should return weekday in crontab format" do @new_resource.weekday :wednesday expect(@provider.send(:weekday_in_crontab)).to eq("3") end it "should raise an error with an unknown weekday" do expect { @new_resource.weekday :caturday }.to raise_error(RangeError) end end context "when weekday is a number in a string" do it "should return the string" do @new_resource.weekday "3" expect(@provider.send(:weekday_in_crontab)).to eq("3") end it "should raise an error with an out of range number" do expect { @new_resource.weekday "-1" }.to raise_error(RangeError) end end context "when weekday is string with the name of the week" do it "should return the string" do @new_resource.weekday "mon" expect(@provider.send(:weekday_in_crontab)).to eq("mon") end end context "when weekday is an integer" do it "should return the integer" do @new_resource.weekday 1 expect(@provider.send(:weekday_in_crontab)).to eq("1") end it "should raise an error with an out of range integer" do expect { @new_resource.weekday 45 }.to raise_error(RangeError) end end end describe "#env_var_str" do context "when no env vars are set" do it "returns an empty string" do expect(@provider.send(:env_var_str)).to be_empty end end let(:mailto) { "foo@example.com" } context "When set directly" do it "returns string with value" do @new_resource.mailto mailto expect(@provider.send(:env_var_str)).to include(mailto) end end context "When set within the hash" do context "env properties" do it "returns string with a warning" do @new_resource.environment "MAILTO" => mailto expect(logger).to receive(:warn).with("cronhole some stuff: the environment property contains the 'MAILTO' variable, which should be set separately as a property.") expect(@provider.send(:env_var_str)).to include(mailto) end end context "other properties" do it "returns string with no warning" do @new_resource.environment "FOOMAILTO" => mailto expect(logger).not_to receive(:warn).with("cronhole some stuff: the environment property contains the 'MAILTO' variable, which should be set separately as a property.") expect(@provider.send(:env_var_str)).to include(mailto) end it "and a line break within properties" do @new_resource.environment "FOOMAILTO" => mailto, "BARMAILTO" => mailto expect(@provider.send(:env_var_str)).to eq("FOOMAILTO=foo@example.com\nBARMAILTO=foo@example.com") end end context "both env and other properties" do it "returns string with line break within the properties" do @new_resource.mailto mailto @new_resource.environment "FOOMAILTO" => mailto expect(@provider.send(:env_var_str)).to eq("MAILTO=\"foo@example.com\"\nFOOMAILTO=foo@example.com") end end end end describe "#duration_str" do context "time as a frequency" do it "returns string" do @new_resource.time :yearly expect(@provider.send(:duration_str)).to eq("@yearly") end end context "time as a duration" do it "defaults to * (No Specific Value)" do @new_resource.minute "1" expect(@provider.send(:duration_str)).to eq("1 * * * *") end it "returns cron format string" do @new_resource.minute "1" @new_resource.hour "2" @new_resource.day "3" @new_resource.month "4" @new_resource.weekday "5" expect(@provider.send(:duration_str)).to eq("1 2 3 4 5") end end end describe "#time_out_str" do context "When not given" do it "Returns an empty string" do expect(@provider.send(:time_out_str)).to be_empty end end context "When given" do let(:time_out_str_val) { " timeout 10;" } context "as String" do it "returns string" do @new_resource.time_out "10" expect(@provider.send(:time_out_str)).to eq time_out_str_val end end context "as Integer" do it "returns string" do @new_resource.time_out "10" expect(@provider.send(:time_out_str)).to eq time_out_str_val end end context "as Hash" do it "returns string" do @new_resource.time_out "duration" => "10" expect(@provider.send(:time_out_str)).to eq time_out_str_val end it "also contains properties" do @new_resource.time_out "duration" => "10", "foreground" => "true", "signal" => "FOO" expect(@provider.send(:time_out_str)).to eq " timeout --foreground --signal FOO 10;" end end end end describe "#cmd_str" do context "With command" do let(:cmd) { "FOOBAR" } before { @new_resource.command cmd } it "returns a string with command" do expect(@provider.send(:cmd_str)).to include(cmd) end it "string ends with a next line" do expect(@provider.send(:cmd_str)[-1]).to eq("\n") end end context "Without command, passed" do context "as nil" do it "returns an empty string with a next line" do @new_resource.command nil expect(@provider.send(:cmd_str)).to eq(" \n") end end context "as an empty string" do it "returns an empty string with a next line" do @new_resource.command "" expect(@provider.send(:cmd_str)).to eq(" \n") end end end end end