mirror of
https://github.com/inspec/inspec
synced 2024-11-10 15:14:23 +00:00
Create config object and units tests
Signed-off-by: Clinton Wolfe <clintoncwolfe@gmail.com>
This commit is contained in:
parent
caf920796d
commit
f1f5b27237
5 changed files with 717 additions and 123 deletions
286
lib/inspec/config.rb
Normal file
286
lib/inspec/config.rb
Normal file
|
@ -0,0 +1,286 @@
|
|||
# Represents InSpec configuration. Merges defaults, config file options,
|
||||
# and CLI arguments.
|
||||
|
||||
require 'pp'
|
||||
|
||||
module Inspec
|
||||
class Config
|
||||
# These are options that apply to any transport
|
||||
GENERIC_CREDENTIALS = %w{
|
||||
backend
|
||||
sudo
|
||||
sudo_password
|
||||
sudo_command
|
||||
sudo_options
|
||||
shell
|
||||
shell_options
|
||||
shell_command
|
||||
}.freeze
|
||||
|
||||
extend Forwardable
|
||||
|
||||
# Many parts of InSpec expect to treat the Config as a Hash
|
||||
def_delegators :@final_options, :each, :delete, :[], :[]=, :key?
|
||||
attr_reader :final_options
|
||||
|
||||
def initialize(cli_opts = {}, cfg_io = nil, command_name = nil)
|
||||
@command_name = command_name || (ARGV.empty? ? nil : ARGV[0].to_sym)
|
||||
@defaults = Defaults.for_command(@command_name)
|
||||
|
||||
@cli_opts = cli_opts.dup
|
||||
cfg_io = resolve_cfg_io(@cli_opts, cfg_io)
|
||||
@cfg_file_contents = read_cfg_file_io(cfg_io)
|
||||
|
||||
@merged_options = merge_options
|
||||
@final_options = finalize_options
|
||||
end
|
||||
|
||||
def diagnose
|
||||
return unless self[:diagnose]
|
||||
puts "InSpec version: #{Inspec::VERSION}"
|
||||
puts "Train version: #{Train::VERSION}"
|
||||
puts 'Command line configuration:'
|
||||
pp @cli_opts
|
||||
puts 'JSON configuration file:'
|
||||
pp @cfg_file_contents
|
||||
puts 'Merged configuration:'
|
||||
pp @merged_options
|
||||
puts
|
||||
end
|
||||
|
||||
#-----------------------------------------------------------------------#
|
||||
# Reading Config Files
|
||||
#-----------------------------------------------------------------------#
|
||||
|
||||
# Regardless of our situation, end up with a readable IO object
|
||||
def resolve_cfg_io(cli_opts, cfg_io)
|
||||
unless cfg_io
|
||||
# TODO: deprecate --json-config
|
||||
path = cli_opts[:config] || cli_opts[:json_config]
|
||||
if path == '-'
|
||||
Inspec::Log.warn 'Reading JSON config from standard input' if STDIN.tty?
|
||||
path = STDIN
|
||||
elsif path.nil?
|
||||
default_path = File.join(Inspec.config_dir, 'config.json')
|
||||
path = default_path if File.exist?(default_path)
|
||||
elsif !File.exist?(path)
|
||||
raise ArgumentError, "Could not read configuration file at #{path}"
|
||||
end
|
||||
cfg_io = File.open(path) if path
|
||||
cfg_io ||= StringIO.new('{ "version": "1.1" }')
|
||||
end
|
||||
cfg_io
|
||||
end
|
||||
|
||||
def read_cfg_file_io(cfg_io)
|
||||
contents = cfg_io.read
|
||||
begin
|
||||
@cfg_file_contents = JSON.parse(contents)
|
||||
validate_config_file_contents!
|
||||
rescue JSON::ParserError => e
|
||||
raise Inspec::ConfigError::MalformedJson, "Failed to load JSON configuration: #{e}\nConfig was: #{contents}"
|
||||
end
|
||||
@cfg_file_contents
|
||||
end
|
||||
|
||||
def file_version
|
||||
@cfg_file_contents['version'] || :legacy
|
||||
end
|
||||
|
||||
def legacy_file?
|
||||
file_version == :legacy
|
||||
end
|
||||
|
||||
def config_file_cli_options
|
||||
if legacy_file?
|
||||
# Assume everything in the file is a CLI option
|
||||
@cfg_file_contents
|
||||
else
|
||||
@cfg_file_contents['cli_options'] || {}
|
||||
end
|
||||
end
|
||||
|
||||
def config_file_reporter_options
|
||||
# This is assumed to be top-level in both legacy and 1.1.
|
||||
# Technically, you could sneak it in the 1.1 cli opts area.
|
||||
@cfg_file_contents.key?('reporter') ? { 'reporter' => @cfg_file_contents['reporter'] } : {}
|
||||
end
|
||||
|
||||
#-----------------------------------------------------------------------#
|
||||
# Validation
|
||||
#-----------------------------------------------------------------------#
|
||||
def validate_config_file_contents!
|
||||
version = @cfg_file_contents['version']
|
||||
|
||||
# Assume legacy format, which is unconstrained
|
||||
return unless version
|
||||
|
||||
unless version == '1.1'
|
||||
raise Inspec::ConfigError::Invalid, "Unsupported config file version '#{version}' - currently supported versions: 1.1"
|
||||
end
|
||||
|
||||
valid_fields = %w{version cli_options credentials compliance reporter}.sort
|
||||
@cfg_file_contents.keys.each do |seen_field|
|
||||
unless valid_fields.include?(seen_field)
|
||||
raise Inspec::ConfigError::Invalid, "Unrecognized top-level configuration field #{seen_field}. Recognized fields: #{valid_fields.join(', ')}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def validate_reporters(reporters)
|
||||
return if reporters.nil?
|
||||
# TODO: move this into a reporter plugin type system
|
||||
valid_types = [
|
||||
'automate',
|
||||
'cli',
|
||||
'documentation',
|
||||
'html',
|
||||
'json',
|
||||
'json-automate',
|
||||
'json-min',
|
||||
'json-rspec',
|
||||
'junit',
|
||||
'progress',
|
||||
'yaml',
|
||||
]
|
||||
|
||||
reporters.each do |reporter_name, reporter_config|
|
||||
raise NotImplementedError, "'#{reporter_name}' is not a valid reporter type." unless valid_types.include?(reporter_name)
|
||||
|
||||
next unless reporter_name == 'automate'
|
||||
%w{token url}.each do |option|
|
||||
raise Inspec::ReporterError, "You must specify a automate #{option} via the config file." if reporter_config[option].nil?
|
||||
end
|
||||
end
|
||||
|
||||
# check to make sure we are only reporting one type to stdout
|
||||
stdout_reporters = 0
|
||||
reporters.each_value do |reporter_config|
|
||||
stdout_reporters += 1 if reporter_config['stdout'] == true
|
||||
end
|
||||
|
||||
raise ArgumentError, 'The option --reporter can only have a single report outputting to stdout.' if stdout_reporters > 1
|
||||
end
|
||||
|
||||
#-----------------------------------------------------------------------#
|
||||
# Merging Options
|
||||
#-----------------------------------------------------------------------#
|
||||
def merge_options
|
||||
options = Thor::CoreExt::HashWithIndifferentAccess.new({})
|
||||
|
||||
# Lowest precedence: default, which may vary by command
|
||||
options.merge!(@defaults)
|
||||
|
||||
# Middle precedence: merge in any CLI options defined from the config file
|
||||
options.merge!(config_file_cli_options)
|
||||
# Reporter options may be defined top-level.
|
||||
options.merge!(config_file_reporter_options)
|
||||
|
||||
# Highest precedence: merge in any options defined via the CLI
|
||||
options.merge!(@cli_opts)
|
||||
|
||||
options
|
||||
end
|
||||
|
||||
#-----------------------------------------------------------------------#
|
||||
# Finalization
|
||||
#-----------------------------------------------------------------------#
|
||||
def finalize_options
|
||||
options = @merged_options.dup
|
||||
|
||||
finalize_set_top_level_command(options)
|
||||
finalize_parse_reporters(options)
|
||||
finalize_handle_sudo(options)
|
||||
finalize_compliance_login(options)
|
||||
|
||||
Thor::CoreExt::HashWithIndifferentAccess.new(options)
|
||||
end
|
||||
|
||||
def finalize_set_top_level_command(options)
|
||||
options[:type] = @command_name
|
||||
Inspec::BaseCLI.inspec_cli_command = @command_name # TODO: move to a more relevant location
|
||||
end
|
||||
|
||||
def finalize_parse_reporters(options) # rubocop:disable Metrics/AbcSize
|
||||
# default to cli report for ad-hoc runners
|
||||
options['reporter'] = ['cli'] if options['reporter'].nil?
|
||||
|
||||
# parse out cli to proper report format
|
||||
if options['reporter'].is_a?(Array)
|
||||
reports = {}
|
||||
options['reporter'].each do |report|
|
||||
reporter_name, destination = report.split(':', 2)
|
||||
if destination.nil? || destination.strip == '-'
|
||||
reports[reporter_name] = { 'stdout' => true }
|
||||
else
|
||||
reports[reporter_name] = {
|
||||
'file' => destination,
|
||||
'stdout' => false,
|
||||
}
|
||||
reports[reporter_name]['target_id'] = options['target_id'] if options['target_id']
|
||||
end
|
||||
end
|
||||
options['reporter'] = reports
|
||||
end
|
||||
|
||||
# add in stdout if not specified
|
||||
if options['reporter'].is_a?(Hash)
|
||||
options['reporter'].each do |reporter_name, config|
|
||||
options['reporter'][reporter_name] = {} if config.nil?
|
||||
options['reporter'][reporter_name]['stdout'] = true if options['reporter'][reporter_name].empty?
|
||||
options['reporter'][reporter_name]['target_id'] = options['target_id'] if options['target_id']
|
||||
end
|
||||
end
|
||||
|
||||
validate_reporters(options['reporter'])
|
||||
options
|
||||
end
|
||||
|
||||
def finalize_handle_sudo(options)
|
||||
# Due to limitations in Thor it is not possible to set an argument to be
|
||||
# both optional and its value to be mandatory. E.g. the user supplying
|
||||
# the --password argument is optional and not always required, but
|
||||
# whenever it is used, it requires a value. Handle options that were
|
||||
# defined in such a way and require a value here:
|
||||
%w{password sudo-password}.each do |option_name|
|
||||
snake_case_option_name = option_name.tr('-', '_').to_s
|
||||
next unless options[snake_case_option_name] == -1 # Dubious - does thor always set -1 for missing value?
|
||||
raise ArgumentError, "Please provide a value for --#{option_name}. For example: --#{option_name}=hello."
|
||||
end
|
||||
|
||||
# Infer `--sudo` if using `--sudo-password` without `--sudo`
|
||||
if options['sudo_password'] && !options['sudo']
|
||||
options['sudo'] = true
|
||||
Inspec::Log.warn '`--sudo-password` used without `--sudo`. Adding `--sudo`.'
|
||||
end
|
||||
end
|
||||
|
||||
def finalize_compliance_login(options)
|
||||
# check for compliance settings
|
||||
# This is always a hash, comes from config file, not CLI opts
|
||||
if options.key?('compliance')
|
||||
require 'plugins/inspec-compliance/lib/inspec-compliance/api'
|
||||
InspecPlugins::Compliance::API.login(options['compliance'])
|
||||
end
|
||||
end
|
||||
|
||||
class Defaults
|
||||
DEFAULTS = {
|
||||
exec: {
|
||||
'reporter' => ['cli'],
|
||||
'show_progress' => false,
|
||||
'color' => true,
|
||||
'create_lockfile' => true,
|
||||
'backend_cache' => true,
|
||||
},
|
||||
shell: {
|
||||
'reporter' => ['cli'],
|
||||
},
|
||||
}.freeze
|
||||
|
||||
def self.for_command(command_name)
|
||||
DEFAULTS[command_name] || {}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -13,6 +13,11 @@ module Inspec
|
|||
class ReporterError < Error; end
|
||||
class ImpactError < Error; end
|
||||
|
||||
# Config file loading
|
||||
class ConfigError < Error; end
|
||||
class ConfigError::MalformedJson < ConfigError; end
|
||||
class ConfigError::Invalid < ConfigError; end
|
||||
|
||||
class Attribute
|
||||
class Error < Inspec::Error; end
|
||||
class ValidationError < Error
|
||||
|
|
|
@ -7,49 +7,7 @@ require 'thor'
|
|||
describe 'BaseCLI' do
|
||||
let(:cli) { Inspec::BaseCLI.new }
|
||||
|
||||
describe 'opts' do
|
||||
it 'raises if `--password/--sudo-password` are used without value' do
|
||||
default_options = { mock: { sudo_password: -1 } }
|
||||
Inspec::BaseCLI.stubs(:default_options).returns(default_options)
|
||||
|
||||
e = proc { cli.send(:opts, :mock) }.must_raise(ArgumentError)
|
||||
e.message.must_match(/Please provide a value for --sudo-password/)
|
||||
end
|
||||
|
||||
it 'assumes `--sudo` if `--sudo-password` is used without it' do
|
||||
default_options = { mock: { sudo_password: 'p@ssw0rd' } }
|
||||
Inspec::BaseCLI.stubs(:default_options).returns(default_options)
|
||||
|
||||
opts = {}
|
||||
out, err = capture_io do
|
||||
cli.send(:opts, :mock)[:sudo].must_equal true
|
||||
end
|
||||
err.must_match /WARN: `--sudo-password` used without `--sudo`/
|
||||
end
|
||||
|
||||
it 'calls `Compliance::API.login` if `opts[:compliance] is passed`' do
|
||||
default_options = { mock: { compliance: 'mock' } }
|
||||
Inspec::BaseCLI.stubs(:default_options).returns(default_options)
|
||||
|
||||
InspecPlugins::Compliance::API.expects(:login).with('mock')
|
||||
|
||||
cli.send(:opts, :mock)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'merge_options' do
|
||||
let(:default_options) do
|
||||
{ exec: { 'reporter' => ['json'], 'backend_cache' => false }}
|
||||
end
|
||||
|
||||
it 'cli defaults populate correctly' do
|
||||
Inspec::BaseCLI.stubs(:default_options).returns(default_options)
|
||||
|
||||
opts = cli.send(:merged_opts, :exec)
|
||||
expected = {"backend_cache"=>false, "reporter"=>{"json"=>{"stdout"=>true}}, "type"=>:exec}
|
||||
opts.must_equal expected
|
||||
end
|
||||
|
||||
describe 'detect command' do
|
||||
it 'verify platform detect' do
|
||||
hash = { name: 'test-os', families: 'aws, cloud', release: 'aws-sdk-v1' }
|
||||
expect = <<EOF
|
||||
|
@ -57,55 +15,11 @@ describe 'BaseCLI' do
|
|||
Families: \e[1m\e[35maws, cloud\e[0m
|
||||
Release: \e[1m\e[35maws-sdk-v1\e[0m
|
||||
EOF
|
||||
_(Inspec::BaseCLI.detect(params: hash, indent: 2, color: 35)).must_equal expect
|
||||
end
|
||||
|
||||
it 'json-config options override cli defaults' do
|
||||
Inspec::BaseCLI.stubs(:default_options).returns(default_options)
|
||||
|
||||
parsed_json = { 'backend_cache' => true }
|
||||
cli.expects(:options_json).returns(parsed_json)
|
||||
|
||||
opts = cli.send(:merged_opts, :exec)
|
||||
expected = {"backend_cache"=>true, "reporter"=>{"json"=>{"stdout"=>true}}, "type"=>:exec}
|
||||
opts.must_equal expected
|
||||
end
|
||||
|
||||
it 'cli options override json-config and default' do
|
||||
Inspec::BaseCLI.stubs(:default_options).returns(default_options)
|
||||
|
||||
parsed_json = { 'backend_cache' => false }
|
||||
cli.expects(:options_json).returns(parsed_json)
|
||||
|
||||
cli_options = { 'backend_cache' => true }
|
||||
cli.instance_variable_set(:@options, cli_options)
|
||||
|
||||
opts = cli.send(:merged_opts, :exec)
|
||||
expected = {"backend_cache"=>true, "reporter"=>{"json"=>{"stdout"=>true}}, "type"=>:exec}
|
||||
opts.must_equal expected
|
||||
end
|
||||
|
||||
it 'make sure shell does not get exec defaults' do
|
||||
Inspec::BaseCLI.stubs(:default_options).returns(default_options)
|
||||
|
||||
opts = cli.send(:merged_opts)
|
||||
expected = {}
|
||||
opts.must_equal expected
|
||||
end
|
||||
|
||||
it 'make sure default reporter is overriden by json-config reporter' do
|
||||
default_options['reporter'] = ['cli']
|
||||
Inspec::BaseCLI.stubs(:default_options).returns(default_options)
|
||||
|
||||
parsed_json = { 'reporter' => ['json'] }
|
||||
cli.expects(:options_json).returns(parsed_json)
|
||||
|
||||
opts = cli.send(:merged_opts, :exec)
|
||||
expected = {"backend_cache"=>false, "reporter"=>{"json"=>{"stdout"=>true}}, "type"=>:exec}
|
||||
opts.must_equal expected
|
||||
_(Inspec::BaseCLI.format_platform_info(params: hash, indent: 2, color: 35)).must_equal expect
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
describe 'configure_logger' do
|
||||
let(:options) do
|
||||
o = {
|
||||
|
@ -146,40 +60,6 @@ EOF
|
|||
end
|
||||
end
|
||||
|
||||
describe 'parse_reporters' do
|
||||
it 'parse cli reporters' do
|
||||
opts = { 'reporter' => ['cli'] }
|
||||
parsed = Inspec::BaseCLI.parse_reporters(opts)
|
||||
expected_value = { 'reporter' => { 'cli' => { 'stdout' => true }}}
|
||||
parsed.must_equal expected_value
|
||||
end
|
||||
|
||||
it 'parses cli report and attaches target_id' do
|
||||
opts = { 'reporter' => ['cli'], 'target_id' => '1d3e399f-4d71-4863-ac54-84d437fbc444' }
|
||||
parsed = Inspec::BaseCLI.parse_reporters(opts)
|
||||
expected_value = {"reporter"=>{"cli"=>{"stdout"=>true, "target_id"=>"1d3e399f-4d71-4863-ac54-84d437fbc444"}}, "target_id"=>"1d3e399f-4d71-4863-ac54-84d437fbc444"}
|
||||
parsed.must_equal expected_value
|
||||
end
|
||||
end
|
||||
|
||||
describe 'validate_reporters' do
|
||||
it 'valid reporter' do
|
||||
stdout = { 'stdout' => true }
|
||||
reporters = { 'json' => stdout }
|
||||
Inspec::BaseCLI.validate_reporters(reporters)
|
||||
end
|
||||
|
||||
it 'invalid reporter type' do
|
||||
reporters = ['json', 'magenta']
|
||||
proc { Inspec::BaseCLI.validate_reporters(reporters) }.must_raise NotImplementedError
|
||||
end
|
||||
|
||||
it 'two reporters outputting to stdout' do
|
||||
stdout = { 'stdout' => true }
|
||||
reporters = { 'json' => stdout, 'cli' => stdout }
|
||||
proc { Inspec::BaseCLI.validate_reporters(reporters) }.must_raise ArgumentError
|
||||
end
|
||||
end
|
||||
|
||||
describe 'suppress_log_output?' do
|
||||
it 'suppresses json' do
|
||||
|
|
417
test/unit/config_test.rb
Normal file
417
test/unit/config_test.rb
Normal file
|
@ -0,0 +1,417 @@
|
|||
|
||||
require 'helper'
|
||||
require 'stringio'
|
||||
|
||||
require 'inspec/config'
|
||||
|
||||
describe 'Inspec::Config' do
|
||||
|
||||
# ========================================================================== #
|
||||
# Constructor
|
||||
# ========================================================================== #
|
||||
describe 'the constructor' do
|
||||
describe 'when no args are provided' do
|
||||
it 'should initialize properly' do
|
||||
cfg = Inspec::Config.new
|
||||
cfg.must_respond_to :final_options
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when CLI args are provided' do
|
||||
it 'should initialize properly' do
|
||||
cfg = Inspec::Config.new({color: true, log_level: 'warn'})
|
||||
cfg.must_respond_to :final_options
|
||||
end
|
||||
end
|
||||
|
||||
# TODO: add test for reading from default config path
|
||||
|
||||
end
|
||||
|
||||
# ========================================================================== #
|
||||
# File Validation
|
||||
# ========================================================================== #
|
||||
describe 'when validating a file' do
|
||||
let(:cfg) { Inspec::Config.new({}, cfg_io) }
|
||||
let(:cfg_io) { StringIO.new(ConfigTestHelper.fixture(fixture_name)) }
|
||||
let(:seen_fields) { cfg.final_options.keys.sort }
|
||||
|
||||
describe 'when the file is a legacy file' do
|
||||
let(:fixture_name) { 'legacy' }
|
||||
it 'should read the file successfully' do
|
||||
expected = ['color', 'reporter', 'target_id', 'type'].sort
|
||||
seen_fields.must_equal expected
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when the file is a valid v1.1 file' do
|
||||
let(:fixture_name) { 'basic' }
|
||||
it 'should read the file successfully' do
|
||||
expected = ['create_lockfile', 'reporter', 'type'].sort
|
||||
seen_fields.must_equal expected
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when the file is minimal' do
|
||||
let(:fixture_name) { 'minimal' }
|
||||
it 'should read the file successfully' do
|
||||
expected = ['reporter', 'type'].sort
|
||||
seen_fields.must_equal expected
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when the file has malformed json' do
|
||||
let(:fixture_name) { 'malformed_json' }
|
||||
it 'should throw an exception' do
|
||||
ex = proc { cfg }.must_raise(Inspec::ConfigError::MalformedJson)
|
||||
ex.message.must_include 'Failed to load JSON config'
|
||||
ex.message.must_include 'unexpected token'
|
||||
ex.message.must_include 'version'
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when the file has a bad file version' do
|
||||
let(:fixture_name) { 'bad_version' }
|
||||
it 'should throw an exception' do
|
||||
ex = proc { cfg }.must_raise(Inspec::ConfigError::Invalid)
|
||||
ex.message.must_include 'Unsupported config file version'
|
||||
ex.message.must_include '99.99'
|
||||
ex.message.must_include '1.1'
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when a 1.1 file has an invalid top-level entry' do
|
||||
let(:fixture_name) { 'bad_top_level' }
|
||||
it 'should throw an exception' do
|
||||
ex = proc { cfg }.must_raise(Inspec::ConfigError::Invalid)
|
||||
ex.message.must_include 'Unrecognized top-level'
|
||||
ex.message.must_include 'unsupported_field'
|
||||
ex.message.must_include 'compliance'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# ========================================================================== #
|
||||
# Defaults
|
||||
# ========================================================================== #
|
||||
describe 'reading defaults' do
|
||||
let(:cfg) { Inspec::Config.new({}, nil, command) }
|
||||
let(:final_options) { cfg.final_options }
|
||||
let(:seen_fields) { cfg.final_options.keys.sort }
|
||||
|
||||
describe 'when the exec command is used' do
|
||||
let(:command) { :exec }
|
||||
it 'should have the correct defaults' do
|
||||
expected = ['color', 'create_lockfile', 'backend_cache', 'reporter', 'show_progress', 'type'].sort
|
||||
seen_fields.must_equal expected
|
||||
final_options['reporter'].must_be_kind_of Hash
|
||||
final_options['reporter'].count.must_equal 1
|
||||
final_options['reporter'].keys.must_include 'cli'
|
||||
final_options['show_progress'].must_equal false
|
||||
final_options['color'].must_equal true
|
||||
final_options['create_lockfile'].must_equal true
|
||||
final_options['backend_cache'].must_equal true
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when the shell command is used' do
|
||||
let(:command) { :shell }
|
||||
it 'should have the correct defaults' do
|
||||
expected = ['reporter', 'type'].sort
|
||||
seen_fields.must_equal expected
|
||||
final_options['reporter'].must_be_kind_of Hash
|
||||
final_options['reporter'].count.must_equal 1
|
||||
final_options['reporter'].keys.must_include 'cli'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# ========================================================================== #
|
||||
# Reading CLI Options
|
||||
# ========================================================================== #
|
||||
# The config facility supports passing in CLI options in the constructor, so
|
||||
# that it can handle merging internally. That is tested here.
|
||||
#
|
||||
# This is different than storing options
|
||||
# in the config file with the same name as the CLI options, which is
|
||||
# tested under 'CLI Options Stored in File'
|
||||
describe 'reading CLI options' do
|
||||
let(:cfg) { Inspec::Config.new(cli_opts) }
|
||||
let(:final_options) { cfg.final_options }
|
||||
let(:seen_fields) { cfg.final_options.keys.sort }
|
||||
|
||||
describe 'when the CLI opts are present' do
|
||||
let(:cli_opts) do
|
||||
{
|
||||
color: true,
|
||||
'string_key' => 'string_value',
|
||||
array_value: [1,2,3],
|
||||
}
|
||||
end
|
||||
|
||||
it 'should transparently round-trip the options' do
|
||||
expected = ['color', 'array_value', 'reporter', 'string_key', 'type'].sort
|
||||
seen_fields.must_equal expected
|
||||
final_options[:color].must_equal true
|
||||
final_options['color'].must_equal true
|
||||
final_options['string_key'].must_equal 'string_value'
|
||||
final_options[:string_key].must_equal 'string_value'
|
||||
final_options['array_value'].must_equal [1,2,3]
|
||||
final_options[:array_value].must_equal [1,2,3]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# ========================================================================== #
|
||||
# CLI Options Stored in File
|
||||
# ========================================================================== #
|
||||
describe 'reading CLI options stored in the config file' do
|
||||
let(:cfg) { Inspec::Config.new({}, cfg_io) }
|
||||
let(:final_options) { cfg.final_options }
|
||||
let(:cfg_io) { StringIO.new(ConfigTestHelper.fixture(fixture_name)) }
|
||||
let(:seen_fields) { cfg.final_options.keys.sort }
|
||||
|
||||
# These two test cases have the same options but in different file versions.
|
||||
describe 'when the CLI opts are present in a 1.1 file' do
|
||||
let(:fixture_name) { :like_legacy }
|
||||
it 'should read the options' do
|
||||
expected = ['color', 'reporter', 'target_id', 'type'].sort
|
||||
seen_fields.must_equal expected
|
||||
final_options['color'].must_equal "true" # Dubious
|
||||
final_options['target_id'].must_equal 'mynode'
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when the CLI opts are present in a legacy file' do
|
||||
let(:fixture_name) { :legacy }
|
||||
it 'should read the options' do
|
||||
expected = ['color', 'reporter', 'target_id', 'type'].sort
|
||||
seen_fields.must_equal expected
|
||||
final_options['color'].must_equal "true" # Dubious
|
||||
final_options['target_id'].must_equal 'mynode'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# ========================================================================== #
|
||||
# Parsing and Validating Reporters
|
||||
# ========================================================================== #
|
||||
|
||||
# TODO: this should be moved into plugins for the reporters
|
||||
describe 'when parsing reporters' do
|
||||
let(:cfg) { Inspec::Config.new(cli_opts) }
|
||||
let(:seen_reporters) { cfg['reporter'] }
|
||||
|
||||
describe 'when paring CLI reporter' do
|
||||
let(:cli_opts) { { 'reporter' => ['cli'] } }
|
||||
it 'parse cli reporters' do
|
||||
expected_value = { 'cli' => { 'stdout' => true }}
|
||||
seen_reporters.must_equal expected_value
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when paring CLI reporter' do
|
||||
let(:cli_opts) { { 'reporter' => ['cli'], 'target_id' => '1d3e399f-4d71-4863-ac54-84d437fbc444' } }
|
||||
it 'parses cli report and attaches target_id' do
|
||||
expected_value = {"cli"=>{"stdout"=>true, "target_id"=>"1d3e399f-4d71-4863-ac54-84d437fbc444"}}
|
||||
seen_reporters.must_equal expected_value
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when validating reporters' do
|
||||
# validate_reporters is private, so we use .send
|
||||
let(:cfg) { Inspec::Config.new }
|
||||
it 'valid reporter' do
|
||||
reporters = { 'json' => { 'stdout' => true } }
|
||||
cfg.send(:validate_reporters, reporters)
|
||||
end
|
||||
|
||||
it 'invalid reporter type' do
|
||||
reporters = ['json', 'magenta']
|
||||
proc { cfg.send(:validate_reporters, reporters) }.must_raise NotImplementedError
|
||||
end
|
||||
|
||||
it 'two reporters outputting to stdout' do
|
||||
stdout = { 'stdout' => true }
|
||||
reporters = { 'json' => stdout, 'cli' => stdout }
|
||||
proc { cfg.send(:validate_reporters, reporters) }.must_raise ArgumentError
|
||||
end
|
||||
end
|
||||
|
||||
# ========================================================================== #
|
||||
# Miscellaneous Option Finalization
|
||||
# ========================================================================== #
|
||||
|
||||
describe 'option finalization' do
|
||||
it 'raises if `--password/--sudo-password` are used without value' do
|
||||
# When you invoke `inspec shell --password` (with no value for password,
|
||||
# though it is setup to expect a string) Thor will set the key with value -1
|
||||
ex = proc { Inspec::Config.new({'sudo_password' => -1}) }.must_raise(ArgumentError)
|
||||
ex.message.must_match(/Please provide a value for --sudo-password/)
|
||||
end
|
||||
|
||||
it 'assumes `--sudo` if `--sudo-password` is used without it' do
|
||||
cfg = Inspec::Config.new('sudo_password' => 'somepass')
|
||||
cfg.key?('sudo').must_equal true
|
||||
end
|
||||
|
||||
it 'calls `Compliance::API.login` if `opts[:compliance] is passed`' do
|
||||
InspecPlugins::Compliance::API.expects(:login)
|
||||
cfg_io = StringIO.new(ConfigTestHelper.fixture('with_compliance'))
|
||||
Inspec::Config.new({ backend: 'mock' }, cfg_io)
|
||||
end
|
||||
end
|
||||
|
||||
# ========================================================================== #
|
||||
# Merging Options
|
||||
# ========================================================================== #
|
||||
describe 'when merging options' do
|
||||
let(:cfg) { Inspec::Config.new(cli_opts, cfg_io, command) }
|
||||
let(:cfg_io) { StringIO.new(ConfigTestHelper.fixture(file_fixture_name)) }
|
||||
let(:seen_fields) { cfg.final_options.keys.sort }
|
||||
let(:command) { nil }
|
||||
|
||||
describe 'when there is both a default and a config file setting' do
|
||||
let(:file_fixture_name) { :override_check }
|
||||
let(:cli_opts) { {} }
|
||||
it 'the config file setting should prevail' do
|
||||
Inspec::Config::Defaults.stubs(:default_for_command).returns('target_id'=> 'value_from_default')
|
||||
expected = ['reporter', 'target_id', 'type'].sort
|
||||
seen_fields.must_equal expected
|
||||
cfg.final_options['target_id'].must_equal 'value_from_config_file'
|
||||
cfg.final_options[:target_id].must_equal 'value_from_config_file'
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when there is both a default and a CLI option' do
|
||||
let(:cli_opts) { { target_id: 'value_from_cli_opts' } }
|
||||
let(:cfg_io) { nil }
|
||||
it 'the CLI option should prevail' do
|
||||
Inspec::Config::Defaults.stubs(:default_for_command).returns('target_id'=> 'value_from_default')
|
||||
expected = ['reporter', 'target_id', 'type'].sort
|
||||
seen_fields.must_equal expected
|
||||
cfg.final_options['target_id'].must_equal 'value_from_cli_opts'
|
||||
cfg.final_options[:target_id].must_equal 'value_from_cli_opts'
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when there is both a config file setting and a CLI option' do
|
||||
let(:file_fixture_name) { :override_check }
|
||||
let(:cli_opts) { { target_id: 'value_from_cli_opts' } }
|
||||
it 'the CLI option should prevail' do
|
||||
expected = ['reporter', 'target_id', 'type'].sort
|
||||
seen_fields.must_equal expected
|
||||
cfg.final_options['target_id'].must_equal 'value_from_cli_opts'
|
||||
cfg.final_options[:target_id].must_equal 'value_from_cli_opts'
|
||||
end
|
||||
end
|
||||
|
||||
describe 'specifically check default vs config file override for "reporter" setting' do
|
||||
let(:cli_opts) { {} }
|
||||
let(:command) { :shell } # shell default is [ :cli ]
|
||||
let(:file_fixture_name) { :override_check } # This fixture sets the cfg file contents to request a json reporter
|
||||
it 'the config file setting should prevail' do
|
||||
expected = ['reporter', 'target_id', 'type'].sort
|
||||
seen_fields.must_equal expected
|
||||
cfg.final_options['reporter'].must_be_kind_of Hash
|
||||
cfg.final_options['reporter'].keys.must_equal ['json']
|
||||
cfg.final_options['reporter']['json']['path'].must_equal 'path/from/config/file'
|
||||
cfg.final_options[:reporter].must_be_kind_of Hash
|
||||
cfg.final_options[:reporter].keys.must_equal ['json']
|
||||
cfg.final_options[:reporter]['json']['path'].must_equal 'path/from/config/file'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# ========================================================================== #
|
||||
# Test Fixtures
|
||||
# ========================================================================== #
|
||||
|
||||
module ConfigTestHelper
|
||||
def fixture(fixture_name)
|
||||
case fixture_name.to_sym
|
||||
when :legacy
|
||||
# TODO - this is dubious, but based on https://www.inspec.io/docs/reference/reporters/#automate-reporter
|
||||
# Things that have 'compliance' as a toplevel have also been seen
|
||||
<<~EOJ1
|
||||
{
|
||||
"color": "true",
|
||||
"target_id": "mynode",
|
||||
"reporter": {
|
||||
"automate" : {
|
||||
"url" : "https://YOUR_A2_URL/data-collector/v0/",
|
||||
"token" : "YOUR_A2_ADMIN_TOKEN"
|
||||
}
|
||||
}
|
||||
}
|
||||
EOJ1
|
||||
when :basic
|
||||
<<~EOJ2
|
||||
{
|
||||
"version": "1.1",
|
||||
"cli_options": {
|
||||
"create_lockfile": "false"
|
||||
},
|
||||
"reporter": {
|
||||
"automate" : {
|
||||
"url": "http://some.where",
|
||||
"token" : "YOUR_A2_ADMIN_TOKEN"
|
||||
}
|
||||
}
|
||||
}
|
||||
EOJ2
|
||||
when :like_legacy
|
||||
<<~EOJ3
|
||||
{
|
||||
"version": "1.1",
|
||||
"cli_options": {
|
||||
"color": "true",
|
||||
"target_id": "mynode"
|
||||
},
|
||||
"reporter": {
|
||||
"automate" : {
|
||||
"url" : "https://YOUR_A2_URL/data-collector/v0/",
|
||||
"token" : "YOUR_A2_ADMIN_TOKEN"
|
||||
}
|
||||
}
|
||||
}
|
||||
EOJ3
|
||||
when :override_check
|
||||
<<~EOJ4
|
||||
{
|
||||
"version": "1.1",
|
||||
"cli_options": {
|
||||
"target_id": "value_from_config_file"
|
||||
},
|
||||
"reporter": {
|
||||
"json": {
|
||||
"path": "path/from/config/file"
|
||||
}
|
||||
}
|
||||
}
|
||||
EOJ4
|
||||
when :minimal
|
||||
'{ "version": "1.1" }'
|
||||
when :bad_version
|
||||
'{ "version": "99.99" }'
|
||||
when :bad_top_level
|
||||
'{ "version": "1.1", "unsupported_field": "some_value" }'
|
||||
when :malformed_json
|
||||
'{ "version": "1.1", '
|
||||
when :with_compliance
|
||||
# TODO - this is dubious, need to verify
|
||||
<<~EOJ5
|
||||
{
|
||||
"compliance": {
|
||||
"server":"https://some.host",
|
||||
"user":"someuser"
|
||||
}
|
||||
}
|
||||
EOJ5
|
||||
end
|
||||
end
|
||||
module_function :fixture
|
||||
end
|
6
test/unit/mock/config_dirs/.inspec/config.json
Normal file
6
test/unit/mock/config_dirs/.inspec/config.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"version": "1.1",
|
||||
"cli_options": {
|
||||
"target_id": "from-fakehome-config-file"
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue