cookbooks/fb_timers/spec/default_spec.rb (258 lines of code) (raw):
# vim: syntax=ruby:expandtab:shiftwidth=2:softtabstop=2:tabstop=2
#
# Copyright (c) 2016-present, Facebook, Inc.
# All rights reserved.
#
# 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/spec_helper'
# rubocop:disable Style/MultilineBlockChain
recipe 'fb_timers::default', :unsupported => [:mac_os_x] do |tc|
let(:t_path) { '/etc/systemd/timers/' }
let(:s_path) { '/etc/systemd/system/' }
let(:unit_types) { %w{timer service} }
let(:dir_content) { %w{README} }
let(:timer_jobs) { %w{multiple simple complex params onboot} }
before do
[t_path, s_path].each do |path|
allow(::Dir).to receive(:glob).with("#{path}*").
and_return(dir_content.map { |f| "#{path}#{f}" })
end
dir_content.each do |unit|
allow(File).to receive(:symlink?).with("#{s_path}#{unit}").
and_return(true)
allow(File).to receive(:readlink).with("#{s_path}#{unit}").
and_return("#{t_path}#{unit}")
end
ml = double('systemctl')
allow(ml).to receive_messages(
:run_command => ml,
:stdout => "multi-user.target\n",
)
stubs_for_resource('execute[set default target]') do |resource|
allow(resource).to receive_shell_out('systemctl get-default').
and_return(ml)
end
end
context 'not managed by systemd' do
let(:chef_run) do
tc.chef_run(:step_into => ['fb_timers_setup']) do |node|
allow(node).to receive(:systemd?).and_return(false)
end
end
it 'should raise an error' do
expect { chef_run.converge(described_recipe) }.
to raise_error(RuntimeError,
/only available for use on systemd-managed machines/)
end
end
context 'missing required key' do
let(:chef_run) do
tc.chef_run(:step_into => ['fb_timers_setup']) do |node|
allow(node).to receive(:systemd?).and_return(true)
end.converge(described_recipe) do |node|
node.default['fb_timers']['jobs'] = {
'no calendar' => {
'command' => '/usr/local/bin/foobar.sh',
},
}
end
end
it 'should raise an error' do
expect { chef_run.converge(described_recipe) }.
to raise_error(RuntimeError, /fb_timers: Missing required key/)
end
end
context 'clean timer setup' do
cached(:chef_run) do
tc.chef_run(
:step_into => ['fb_timers_setup'],
) do |node|
allow(node).to receive(:systemd?).and_return(true)
end.converge('fb_systemd::reload', described_recipe) do |node|
node.default['fb_timers']['jobs'] = {
'simple' => {
'calendar' => '*:0/15:0',
'command' => '/usr/local/bin/foobar.sh',
},
'valid_user_set' => {
'calendar' => '*:0/15:0',
'command' => '/usr/local/bin/foobar.sh',
'service_options' => { 'User' => 'nobody' },
},
'complex' => {
'calendar' => 'Sat,Thu,Mon..Wed,Sat..Sun',
'command' => '/usr/local/bin/foobar.sh thing1 thing2',
'timeout' => '1d',
'timeout_stop' => '1h',
'accuracy' => '1h',
'persistent' => true,
'splay' => '0.5h',
'syslog' => true,
},
'params' => {
'calendar' => '0:0:0',
'command' => '/usr/local/bin/foobar.sh',
'description' => 'Custom set description field',
'timer_options' => {
'foo' => '19',
'bar' => '17',
},
'timer_unit_options' => {
'jkl' => 'aaaaah',
},
'service_options' => {
'asdf' => '7',
'baz' => '11',
'foobar' => ['1', '2', '3'],
},
'service_unit_options' => {
'jkl' => 'aaaaah',
'barbaz' => ['a', 'b', 'c'],
},
},
'no_start' => {
'calendar' => '1',
'command' => 'foo',
'autostart' => false,
},
'no_proc' => {
'calendar' => '1',
'command' => 'foo',
'only_if' => proc { false },
},
'yes_proc' => {
'calendar' => '1',
'command' => 'foo',
'only_if' => proc { true },
},
'multiple' => {
'calendar' => 'Mon,Wed',
'commands' => [
'/usr/local/bin/foobar.sh one',
'/usr/local/bin/foobar.sh two',
],
},
'onboot' => {
'command' => '/usr/local/bin/foobar.sh',
'timer_options' => { 'OnBootSec' => '1s' },
},
}
end
end
it 'should create timer unit files' do
unit_types.each do |type|
timer_jobs.each do |job|
expect(chef_run).to render_file("#{t_path}#{job}.#{type}").
with_content(tc.fixture("#{job}.#{type}"))
end
end
end
# TODO: T23654032 add a test to validate this correctly notifies
# fb_systemd_reload[system instance] to run immediately
it 'should create symlink for service units' do
unit_types.each do |type|
timer_jobs.each do |job|
expect(chef_run).to run_execute(
"link unit file #{t_path}#{job}.#{type}",
)
end
expect(chef_run).to_not run_execute(
"link unit file #{t_path}no_start.#{type}",
)
end
end
# TODO: T23654032 add a test to validate this correctly notifies
# fb_systemd_reload[system instance] to run immediately
it 'should enable the timer unit' do
timer_jobs.each do |job|
expect(chef_run).to enable_service("#{job}.timer")
expect(chef_run).to_not enable_service("#{job}.service")
end
end
it 'should start the timer unit' do
timer_jobs.each do |job|
expect(chef_run).to start_service("#{job}.timer")
expect(chef_run).to_not start_service("#{job}.service")
end
end
it 'should handle jobs with only_ifs' do
unit_types.each do |type|
expect(chef_run).to_not render_file("#{t_path}no_proc.#{type}")
expect(chef_run).to render_file("#{t_path}yes_proc.#{type}")
end
end
end
context 'prints warnings' do
cached(:chef_run) do
tc.chef_run(:step_into => ['fb_timers_setup']) do |node|
allow(node).to receive(:systemd?).and_return(true)
end.converge('fb_systemd::reload', described_recipe) do |node|
node.stub(:systemd?).and_return(true)
node.default['fb_timers']['jobs'] = {
'bad_keys' => {
'calendar' => '1',
'command' => 'foo',
'autostart' => false,
'FOO' => 'bad',
'USER' => 'bad',
'user' => 'bad',
'User' => 'bad',
},
}
end
end
it 'should issue warnings for unknown keys' do
# We can't be sure of ordering and state so we can't
# really make this test more specific without getting flakey
expect(Chef::Log).to receive(:warn).with(/fb_timers:/).at_least(2).times
chef_run
end
end
context 'removes unmanaged jobs' do
let(:dir_content) do
%w{
README
old.timer old.service current.timer current.service
only_if_disabled.timer only_if_disabled.service
only_if_enabled.timer only_if_enabled.service
}
end
cached(:chef_run) do
tc.chef_run(:step_into => ['fb_timers_setup']) do |node|
allow(node).to receive(:systemd?).and_return(true)
end.converge('fb_systemd::reload', described_recipe) do |node|
node.stub(:systemd?).and_return(true)
node.default['fb_timers']['jobs'] = {
'current' => {
'calendar' => 1,
'command' => 'bar',
},
# If the only_if becomes false, in which case we should disable
'only_if_disabled' => {
'only_if' => proc { false },
'calendar' => 1,
'command' => 'bar',
},
'only_if_enabled' => {
'only_if' => proc { true },
'calendar' => 1,
'command' => 'bar',
},
}
end
end
it 'should stop the old service units' do
unit_types.each do |type|
expect(chef_run).to stop_service("old.#{type}")
expect(chef_run).to stop_service("only_if_disabled.#{type}")
expect(chef_run).to_not stop_service("current.#{type}")
expect(chef_run).to_not stop_service("only_if_enabled.#{type}")
end
end
it 'should disable the old service units' do
unit_types.each do |type|
expect(chef_run).to disable_service("old.#{type}")
expect(chef_run).to disable_service("only_if_disabled.#{type}")
expect(chef_run).to_not disable_service("current.#{type}")
expect(chef_run).to_not disable_service("only_if_enabled.#{type}")
end
end
it 'should delete the old timer units' do
unit_types.each do |type|
expect(chef_run).to delete_file("#{t_path}old.#{type}")
expect(chef_run).to delete_file("#{t_path}only_if_disabled.#{type}")
expect(chef_run).to_not delete_file("#{t_path}current.#{type}")
expect(chef_run).to_not delete_file("#{t_path}only_if_enabled.#{type}")
end
end
it 'should not delete the README' do
expect(chef_run).to_not delete_file('README')
end
end
end
# rubocop:enable Style/MultilineBlockChain