Create a class to handle the plugins.json file (#3575)

* unit tests for plugin conf file class, all skip
* File path stuff works
* Validation works
* Add works
* Added remove_entry
* Save works - ready to refactor others
* Rework Loader to use ConfigFile
* Modify loader and installer to use the config file class
* linting

Signed-off-by: Clinton Wolfe <clintoncwolfe@gmail.com>
This commit is contained in:
Clinton Wolfe 2018-11-16 17:03:09 -05:00 committed by Jared Quick
parent bf5815ed78
commit 3c8697e5e2
20 changed files with 686 additions and 174 deletions

View file

@ -24,6 +24,7 @@ module Inspec
end
require 'inspec/globals'
require 'inspec/plugin/v2/config_file'
require 'inspec/plugin/v2/registry'
require 'inspec/plugin/v2/loader'
require 'inspec/plugin/v2/plugin_base'

View file

@ -0,0 +1,148 @@
require 'json'
module Inspec::Plugin::V2
# Represents the plugin config file on disk.
class ConfigFile
include Enumerable
attr_reader :path
def initialize(path = nil)
@path = path || self.class.default_path
@data = blank_structure
read_and_validate_file if File.exist?(@path)
end
# Returns the defaut path for a config file.
# This respects ENV['INSPEC_CONFIG_DIR'].
def self.default_path
File.join(Inspec.config_dir, 'plugins.json')
end
# Implement Enumerable. All Enumerable methds act
# on the plugins list, and yield Hashes that represent
# an entry.
def each(&block)
@data[:plugins].each(&block)
end
# Look for a plugin by name.
def plugin_by_name(name)
detect { |entry| entry[:name] == name.to_sym }
end
# Check for a plugin
def existing_entry?(name)
!plugin_by_name(name).nil?
end
# Add an entry with full validation.
def add_entry(proposed_entry)
unless proposed_entry.keys.all? { |field| field.is_a? Symbol }
raise Inspec::Plugin::V2::ConfigError, 'All keys to ConfigFile#add_entry must be symbols'
end
validate_entry(proposed_entry)
if existing_entry?(proposed_entry[:name])
raise Inspec::Plugin::V2::ConfigError, "Duplicate plugin name in call to ConfigFile#add_entry: '#{proposed_entry[:name]}'"
end
@data[:plugins] << proposed_entry
end
# Removes an entry specified by plugin name.
def remove_entry(name)
unless existing_entry?(name)
raise Inspec::Plugin::V2::ConfigError, "No such entry with plugin name '#{name}'"
end
@data[:plugins].delete_if { |entry| entry[:name] == name.to_sym }
end
# Save the file to disk as a JSON structure at the path.
def save
dir = File.dirname(path)
FileUtils.mkdir_p(dir)
File.write(path, JSON.pretty_generate(@data))
end
private
def blank_structure
{
plugins_config_version: '1.0.0',
plugins: [],
}
end
def read_and_validate_file
@data = JSON.parse(File.read(path), symbolize_names: true)
validate_file
rescue JSON::ParserError => e
raise Inspec::Plugin::V2::ConfigError, "Failed to load plugins JSON configuration from #{path}:\n#{e}"
end
def validate_file # rubocop: disable Metrics/AbcSize
unless @data[:plugins_config_version]
raise Inspec::Plugin::V2::ConfigError, "Missing 'plugins_config_version' entry at #{path} - currently support versions: 1.0.0"
end
unless @data[:plugins_config_version] == '1.0.0'
raise Inspec::Plugin::V2::ConfigError, "Unsupported plugins.json file version #{@data[:plugins_config_version]} at #{path} - currently support versions: 1.0.0"
end
plugin_entries = @data[:plugins]
if plugin_entries.nil?
raise Inspec::Plugin::V2::ConfigError, "Malformed plugins.json file at #{path} - missing top-level key named 'plugins', whose value should be an array"
end
unless plugin_entries.is_a?(Array)
raise Inspec::Plugin::V2::ConfigError, "Malformed plugins.json file at #{path} - top-level key named 'plugins' should be an array"
end
plugin_entries.each_with_index do |plugin_entry, idx|
begin
validate_entry(plugin_entry)
rescue Inspec::Plugin::V2::ConfigError => ex
# append some context to the message
raise Inspec::Plugin::V2::ConfigError, 'Malformed plugins.json file - ' + ex.message + " at index #{idx}"
end
# Check for duplicates
plugin_entries.each_with_index do |other_entry, other_idx|
next if idx == other_idx
next unless other_entry.is_a? Hash # We'll catch that invalid entry later
next if plugin_entry[:name] != other_entry[:name]
indices = [idx, other_idx].sort
raise Inspec::Plugin::V2::ConfigError, "Malformed plugins.json file - duplicate plugin entry '#{plugin_entry[:name]}' detected at index #{indices[0]} and #{indices[1]}"
end
end
end
def validate_entry(plugin_entry)
unless plugin_entry.is_a? Hash
raise Inspec::Plugin::V2::ConfigError, "each 'plugins' entry should be a Hash / JSON object"
end
unless plugin_entry.key? :name
raise Inspec::Plugin::V2::ConfigError, "'plugins' entry missing 'name' field"
end
# Symbolize the name.
plugin_entry[:name] = plugin_entry[:name].to_sym
if plugin_entry.key? :installation_type
seen_type = plugin_entry[:installation_type]
unless [:gem, :path].include? seen_type.to_sym
raise Inspec::Plugin::V2::ConfigError, "'plugins' entry with unrecognized installation_type (must be one of 'gem' or 'path')"
end
plugin_entry[:installation_type] = seen_type.to_sym
if plugin_entry[:installation_type] == :path && !plugin_entry.key?(:installation_path)
raise Inspec::Plugin::V2::ConfigError, "'plugins' entry with a 'path' installation_type missing installation path"
end
end
end
end
end

View file

@ -24,7 +24,7 @@ module Inspec::Plugin::V2
Gem.configuration['verbose'] = false
attr_reader :loader, :registry
attr_reader :conf_file, :loader, :registry
def_delegator :loader, :plugin_gem_path, :gem_path
def_delegator :loader, :plugin_conf_file_path
def_delegator :loader, :list_managed_gems
@ -459,45 +459,25 @@ module Inspec::Plugin::V2
#===================================================================#
# plugins.json Maintenance Methods #
#===================================================================#
# TODO: refactor the plugin.json file to have its own class, which Installer consumes
def update_plugin_config_file(plugin_name, opts)
config = update_plugin_config_data(plugin_name, opts)
FileUtils.mkdir_p(Inspec.config_dir)
File.write(plugin_conf_file_path, JSON.pretty_generate(config))
end
# Be careful no to initialize this until just before we write.
# Under testing, ENV['INSPEC_CONFIG_DIR'] may have changed.
@conf_file = Inspec::Plugin::V2::ConfigFile.new
# TODO: refactor the plugin.json file to have its own class, which Installer consumes
def update_plugin_config_data(plugin_name, opts)
config = read_or_init_config_data
config['plugins'].delete_if { |entry| entry['name'] == plugin_name }
return config if opts[:action] == :uninstall
entry = { 'name' => plugin_name }
# Parsing by Requirement handles lot of awkward formattoes
entry['version'] = Gem::Requirement.new(opts[:version]).to_s if opts.key?(:version)
if opts.key?(:path)
entry['installation_type'] = 'path'
entry['installation_path'] = opts[:path]
# Remove, then optionally rebuild, the entry for the plugin being modified.
conf_file.remove_entry(plugin_name) if conf_file.existing_entry?(plugin_name)
unless opts[:action] == :uninstall
entry = { name: plugin_name }
# Parsing by Requirement handles lot of awkward formattoes
entry[:version] = Gem::Requirement.new(opts[:version]).to_s if opts.key?(:version)
if opts.key?(:path)
entry[:installation_type] = :path
entry[:installation_path] = opts[:path]
end
conf_file.add_entry(entry)
end
config['plugins'] << entry
config
end
# TODO: check for validity
# TODO: refactor the plugin.json file to have its own class, which Installer consumes
def read_or_init_config_data
if File.exist?(plugin_conf_file_path)
JSON.parse(File.read(plugin_conf_file_path))
else
{
'plugins_config_version' => '1.0.0',
'plugins' => [],
}
end
conf_file.save
end
end
end

View file

@ -1,5 +1,5 @@
require 'json'
require 'inspec/log'
require 'inspec/plugin/v2/config_file'
# Add the current directory of the process to the load path
$LOAD_PATH.unshift('.') unless $LOAD_PATH.include?('.')
@ -9,13 +9,13 @@ $LOAD_PATH.unshift(folder) unless $LOAD_PATH.include?('folder')
module Inspec::Plugin::V2
class Loader
attr_reader :registry, :options
attr_reader :conf_file, :registry, :options
def initialize(options = {})
@options = options
@registry = Inspec::Plugin::V2::Registry.instance
read_conf_file
unpack_conf_file
@conf_file = Inspec::Plugin::V2::ConfigFile.new
read_conf_file_into_registry
# Old-style (v0, v1) co-distributed plugins were called 'bundles'
# and were located in lib/bundles
@ -157,16 +157,6 @@ module Inspec::Plugin::V2
self.class.list_managed_gems
end
# TODO: refactor the plugin.json file to have its own class, which Loader consumes
def plugin_conf_file_path
self.class.plugin_conf_file_path
end
# TODO: refactor the plugin.json file to have its own class, which Loader consumes
def self.plugin_conf_file_path
File.join(Inspec.config_dir, 'plugins.json')
end
private
# 'Activating' a gem adds it to the load path, so 'require "gemname"' will work.
@ -271,71 +261,22 @@ module Inspec::Plugin::V2
end
end
# TODO: DRY up re: Installer read_or_init_config_file
# TODO: refactor the plugin.json file to have its own class, which Loader consumes
def read_conf_file
if File.exist?(plugin_conf_file_path)
@plugin_file_contents = JSON.parse(File.read(plugin_conf_file_path))
else
@plugin_file_contents = {
'plugins_config_version' => '1.0.0',
'plugins' => [],
}
end
rescue JSON::ParserError => e
raise Inspec::Plugin::V2::ConfigError, "Failed to load plugins JSON configuration from #{plugin_conf_file_path}:\n#{e}"
end
# TODO: refactor the plugin.json file to have its own class, which Loader consumes
def unpack_conf_file
validate_conf_file
@plugin_file_contents['plugins'].each do |plugin_json|
def read_conf_file_into_registry
conf_file.each do |plugin_entry|
status = Inspec::Plugin::V2::Status.new
status.name = plugin_json['name'].to_sym
status.name = plugin_entry[:name]
status.loaded = false
status.installation_type = (plugin_json['installation_type'] || :gem).to_sym
status.installation_type = (plugin_entry[:installation_type] || :gem)
case status.installation_type
when :gem
status.entry_point = status.name.to_s
status.version = plugin_json['version']
status.version = plugin_entry[:version]
when :path
status.entry_point = plugin_json['installation_path']
status.entry_point = plugin_entry[:installation_path]
end
registry[status.name] = status
end
end
# TODO: refactor the plugin.json file to have its own class, which Loader consumes
def validate_conf_file
unless @plugin_file_contents['plugins_config_version'] == '1.0.0'
raise Inspec::Plugin::V2::ConfigError, "Unsupported plugins.json file version #{@plugin_file_contents['plugins_config_version']} at #{plugin_conf_file_path} - currently support versions: 1.0.0"
end
plugin_entries = @plugin_file_contents['plugins']
unless plugin_entries.is_a?(Array)
raise Inspec::Plugin::V2::ConfigError, "Malformed plugins.json file - should have a top-level key named 'plugins', whose value is an array"
end
plugin_entries.each do |plugin_entry|
unless plugin_entry.is_a? Hash
raise Inspec::Plugin::V2::ConfigError, "Malformed plugins.json file - each 'plugins' entry should be a Hash / JSON object"
end
unless plugin_entry.key? 'name'
raise Inspec::Plugin::V2::ConfigError, "Malformed plugins.json file - each 'plugins' entry must have a 'name' field"
end
next unless plugin_entry.key?('installation_type')
unless %w{gem path}.include? plugin_entry['installation_type']
raise Inspec::Plugin::V2::ConfigError, "Malformed plugins.json file - each 'installation_type' must be one of 'gem' or 'path'"
end
next unless plugin_entry['installation_type'] == 'path'
unless plugin_entry.key?('installation_path')
raise Inspec::Plugin::V2::ConfigError, "Malformed plugins.json file - each 'plugins' entry with a 'path' installation_type must provide an 'installation_path' field"
end
end
end
end
end

View file

@ -7,31 +7,6 @@ require 'functional/helper'
describe 'plugin loader' do
include FunctionalHelper
it 'handles a corrupt plugins.json correctly' do
outcome = inspec_with_env('version', INSPEC_CONFIG_DIR: File.join(config_dir_path, 'corrupt'))
outcome.exit_status.must_equal 2
outcome.stdout.wont_include('Inspec::Plugin::V2::ConfigError', 'No stacktrace in error by default')
outcome.stdout.must_include('Failed to load plugins JSON configuration', 'Friendly message in error')
outcome.stdout.must_include('unit/mock/config_dirs/corrupt/plugins.json', 'Location of bad file in error')
outcome = inspec_with_env('version --debug', INSPEC_CONFIG_DIR: File.join(config_dir_path, 'corrupt'))
outcome.exit_status.must_equal 2
outcome.stdout.must_include('Inspec::Plugin::V2::ConfigError', 'Include stacktrace in error with --debug')
end
it 'handles a misversioned plugins.json correctly' do
outcome = inspec_with_env('version', INSPEC_CONFIG_DIR: File.join(config_dir_path, 'bad_plugin_conf_version'))
outcome.exit_status.must_equal 2
outcome.stdout.wont_include('Inspec::Plugin::V2::ConfigError', 'No stacktrace in error by default')
outcome.stdout.must_include('Unsupported plugins.json file version', 'Friendly message in error')
outcome.stdout.must_include('unit/mock/config_dirs/bad_plugin_conf_version/plugins.json', 'Location of bad file in error')
outcome.stdout.must_include('99.99.9', 'Incorrect version in error')
outcome = inspec_with_env('version --debug', INSPEC_CONFIG_DIR: File.join(config_dir_path, 'bad_plugin_conf_version'))
outcome.exit_status.must_equal 2
outcome.stdout.must_include('Inspec::Plugin::V2::ConfigError', 'Include stacktrace in error with --debug')
end
it 'handles an unloadable plugin correctly' do
outcome = inspec_with_env('version', INSPEC_CONFIG_DIR: File.join(config_dir_path, 'plugin_error_on_load'))
outcome.exit_status.must_equal 2

View file

@ -0,0 +1,18 @@
{
"plugins_config_version" : "1.0.0",
"plugins": [
{
"name": "inspec-test-fixture-00"
},
{
"name": "inspec-test-fixture-01",
"installation_type": "gem",
"version": "0.2.0"
},
{
"name": "inspec-test-fixture-02",
"installation_type": "path",
"installation_path": "/somewhere/on/disk/lib/entrypoint.rb"
}
]
}

View file

@ -0,0 +1,18 @@
{
"plugins_config_version" : "1.0.0",
"plugins": [
{
"name": "inspec-test-fixture-00"
},
{
"name": "inspec-test-fixture-01",
"installation_type": "superglued",
"version": "0.2.0"
},
{
"name": "inspec-test-fixture-03",
"installation_type": "path",
"installation_path": "/somewhere/on/disk/lib/entrypoint.rb"
}
]
}

View file

@ -0,0 +1,21 @@
{
"plugins_config_version" : "1.0.0",
"plugins": [
{
"name": "inspec-test-fixture-00"
},
{
"name": "inspec-test-fixture-01",
"installation_type": "gem",
"version": "0.2.0"
},
{
"name": "inspec-test-fixture-03",
"installation_type": "path",
"installation_path": "/somewhere/on/disk/lib/entrypoint.rb"
},
{
"name": "inspec-test-fixture-01"
}
]
}

View file

@ -0,0 +1,17 @@
{
"plugins_config_version" : "1.0.0",
"plugins": [
{
"name": "inspec-test-fixture-00"
},
{
"installation_type": "gem",
"version": "0.2.0"
},
{
"name": "inspec-test-fixture-03",
"installation_type": "path",
"installation_path": "/somewhere/on/disk/lib/entrypoint.rb"
}
]
}

View file

@ -0,0 +1,17 @@
{
"plugins_config_version" : "1.0.0",
"plugins": [
{
"name": "inspec-test-fixture-00"
},
{
"name": "inspec-test-fixture-01",
"installation_type": "gem",
"version": "0.2.0"
},
{
"name": "inspec-test-fixture-03",
"installation_type": "path"
}
]
}

View file

@ -0,0 +1,14 @@
{
"plugins_config_version" : "1.0.0",
"plugins": [
{
"name": "inspec-test-fixture-00"
},
{
"name": "inspec-test-fixture-01",
"installation_type": "gem",
"version": "0.2.0"
},
"inspec-test-fixture-03"
]
}

View file

@ -0,0 +1,4 @@
{
"plugins_config_version" : "1.0.0",
"plugins": {}
}

View file

@ -0,0 +1,3 @@
{
"plugins": []
}

View file

@ -0,0 +1,3 @@
{
"plugins_config_version" : "1.0.0"
}

View file

@ -0,0 +1,5 @@
{
"plugins_config_version" : "1.0.0",
"plugins": [
]
}

View file

@ -56,8 +56,6 @@ module InstallerTestHelpers
# Clean up any activated gems
Gem.loaded_specs.delete('inspec-test-fixture')
# TODO: may need to edit the $LOAD_PATH, if it turns out that we need to "deactivate" gems after installation
end
end
@ -141,18 +139,10 @@ class PluginInstallerInstallationTests < MiniTest::Test
# Should now be present in plugin.json
plugin_json_path = File.join(ENV['INSPEC_CONFIG_DIR'], 'plugins.json')
assert File.exist?(plugin_json_path), 'plugins.json should now exist'
plugin_json_data = JSON.parse(File.read(plugin_json_path))
config_file = Inspec::Plugin::V2::ConfigFile.new(plugin_json_path)
assert_includes plugin_json_data.keys, 'plugins_config_version'
assert_equal '1.0.0', plugin_json_data['plugins_config_version'], 'Plugin config version should ve initted to 1.0.0'
assert_includes plugin_json_data.keys, 'plugins'
assert_kind_of Array, plugin_json_data['plugins']
assert_equal 1, plugin_json_data['plugins'].count, 'plugins.json should have one entry'
entry = plugin_json_data['plugins'].first
assert_kind_of Hash, entry
assert_includes entry.keys, 'name'
assert_equal 'inspec-test-fixture', entry['name']
# TODO: any other fields to check? gem version?
assert_equal 1, config_file.count, 'plugins.json should have one entry'
assert config_file.existing_entry?(:'inspec-test-fixture')
end
def test_install_a_gem_from_rubygems_org
@ -196,11 +186,10 @@ class PluginInstallerInstallationTests < MiniTest::Test
spec_path = File.join(@installer.gem_path, 'specifications', 'inspec-test-fixture-0.2.0.gemspec')
refute File.exist?(spec_path), 'After pinned installation from rubygems.org, the wrong gemspec version should be absent'
plugin_json_path = File.join(ENV['INSPEC_CONFIG_DIR'], 'plugins.json')
plugin_json_data = JSON.parse(File.read(plugin_json_path))
entry = plugin_json_data['plugins'].detect { |e| e["name"] == 'inspec-test-fixture'}
assert_includes entry.keys, 'version', 'plugins.json should include version pinning key'
assert_equal '= 0.1.0', entry['version'], 'plugins.json should include version pinning value'
config_file = Inspec::Plugin::V2::ConfigFile.new
entry = config_file.plugin_by_name(:'inspec-test-fixture')
assert_includes entry.keys, :version, 'plugins.json should include version pinning key'
assert_equal '= 0.1.0', entry[:version], 'plugins.json should include version pinning value'
end
def test_install_a_gem_with_conflicting_depends_from_rubygems_org
@ -230,14 +219,13 @@ class PluginInstallerInstallationTests < MiniTest::Test
specs = Dir.glob(File.join(@installer.gem_path, 'specifications', '*.gemspec'))
assert_empty specs, 'After install-from-path, no gemspecs should be installed'
plugin_json_path = File.join(ENV['INSPEC_CONFIG_DIR'], 'plugins.json')
plugin_json_data = JSON.parse(File.read(plugin_json_path))
entry = plugin_json_data['plugins'].detect { |e| e["name"] == 'inspec-test-fixture'}
assert_includes entry.keys, 'installation_type', 'plugins.json should include installation_type key'
assert_equal 'path', entry['installation_type'], 'plugins.json should include path installation_type'
config_file = Inspec::Plugin::V2::ConfigFile.new
entry = config_file.plugin_by_name(:'inspec-test-fixture')
assert_includes entry.keys, :installation_type, 'plugins.json should include installation_type key'
assert_equal :path, entry[:installation_type], 'plugins.json should include path installation_type'
assert_includes entry.keys, 'installation_path', 'plugins.json should include installation_path key'
assert_equal @plugin_fixture_src_path, entry['installation_path'], 'plugins.json should include correct value for installation path'
assert_includes entry.keys, :installation_path, 'plugins.json should include installation_path key'
assert_equal @plugin_fixture_src_path, entry[:installation_path], 'plugins.json should include correct value for installation path'
end
def test_refuse_to_install_gem_whose_name_is_on_the_reject_list
@ -335,11 +323,10 @@ class PluginInstallerUpdaterTests < MiniTest::Test
assert File.exist?(spec_path), 'After update, the 0.1.0 gemspec should remain'
# Plugins file entry should be version pinned
plugin_json_path = File.join(ENV['INSPEC_CONFIG_DIR'], 'plugins.json')
plugin_json_data = JSON.parse(File.read(plugin_json_path))
entry = plugin_json_data['plugins'].detect { |e| e["name"] == 'inspec-test-fixture'}
assert_includes entry.keys, 'version', 'plugins.json should include version pinning key'
assert_equal '= 0.2.0', entry['version'], 'plugins.json should include version pinning value'
config_file = Inspec::Plugin::V2::ConfigFile.new
entry = config_file.plugin_by_name(:'inspec-test-fixture')
assert_includes entry.keys, :version, 'plugins.json should include version pinning key'
assert_equal '= 0.2.0', entry[:version], 'plugins.json should include version pinning value'
end
# TODO: Prevent updating a gem if it will lead to unsolveable dependencies
@ -407,10 +394,8 @@ class PluginInstallerUninstallTests < MiniTest::Test
assert_raises(Gem::UnsatisfiableDependencyError) { request_set.resolve(universe_set) }
# Plugins file entry should be removed
plugin_json_path = File.join(ENV['INSPEC_CONFIG_DIR'], 'plugins.json')
plugin_json_data = JSON.parse(File.read(plugin_json_path))
entries = plugin_json_data['plugins'].select { |e| e["name"] == 'inspec-test-fixture'}
assert_empty entries, "After gem-based uninstall, plugin name should be removed from plugins.json"
config_file = Inspec::Plugin::V2::ConfigFile.new
refute config_file.existing_entry?(:'inspec-test-fixture'), "After gem-based uninstall, plugin name should be removed from plugins.json"
end
def test_uninstall_a_gem_plugin_removes_deps

View file

@ -101,16 +101,6 @@ class PluginLoaderTests < MiniTest::Test
end
end
def test_constuctor_when_the_plugin_config_is_corrupt_it_throws_an_exception
ENV['INSPEC_CONFIG_DIR'] = File.join(@config_dir_path, 'corrupt')
assert_raises(Inspec::Plugin::V2::ConfigError) { Inspec::Plugin::V2::Loader.new }
end
def test_constuctor_when_the_plugin_config_is_a_bad_version_it_throws_an_exception
ENV['INSPEC_CONFIG_DIR'] = File.join(@config_dir_path, 'bad_plugin_conf_version')
assert_raises(Inspec::Plugin::V2::ConfigError) { Inspec::Plugin::V2::Loader.new }
end
#====================================================================#
# basic loading #
#====================================================================#

View file

@ -0,0 +1,372 @@
require 'minitest/spec'
require 'minitest/autorun'
require 'tmpdir'
require_relative '../../../../lib/inspec/plugin/v2'
# This file relies on setting environment variables for some
# of its tests - it is NOT thread-safe.
describe 'Inspec::Plugin::V2::ConfigFile' do
orig_home = ENV['HOME']
let(:repo_path) { File.expand_path(File.join( __FILE__, '..', '..', '..', '..', '..')) }
let(:config_fixtures_path) { File.join(repo_path, 'test', 'unit', 'mock', 'config_dirs') }
let(:config_file_obj) { Inspec::Plugin::V2::ConfigFile.new(constructor_arg) }
let(:constructor_arg) { File.join(config_fixtures_path, 'plugin_config_files', fixture_name + '.json') }
after do
ENV['HOME'] = orig_home
ENV['INSPEC_CONFIG_DIR'] = nil
end
#----------------------------------------------------------#
# Path Handling
#----------------------------------------------------------#
describe 'locating the file' do
describe 'when no env var is set' do
let(:constructor_arg) { nil }
it 'defaults to the home directory' do
ENV['HOME'] = File.join(config_fixtures_path, 'fakehome')
expected_path = File.join(ENV['HOME'], '.inspec', 'plugins.json')
config_file_obj.path.must_equal expected_path
end
end
describe 'when an env var is set' do
let(:constructor_arg) { nil }
it 'looks to the dir specified by the env var' do
ENV['INSPEC_CONFIG_DIR'] = File.join(config_fixtures_path, 'meaning-by-path')
expected_path = File.join(ENV['INSPEC_CONFIG_DIR'], 'plugins.json')
config_file_obj.path.must_equal expected_path
end
end
describe 'when a path is provided to the constructor' do
let(:fixture_name) { 'no_plugins' }
it 'uses the provided path' do
config_file_obj.path.must_equal constructor_arg
end
end
end
#----------------------------------------------------------#
# Reading a File
#----------------------------------------------------------#
describe 'reading the file' do
describe 'when the file is missing' do
let(:fixture_name) { 'nonesuch' }
it 'creates a empty datastructure' do
Dir.mktmpdir do |tmp_dir|
constructor_arg = File.join(tmp_dir, 'plugins.json')
config_file_obj.count.must_equal 0
end
end
end
describe 'when the file is corrupt' do
let(:fixture_name) { 'corrupt' }
it 'throws an exception' do
ex = assert_raises(Inspec::Plugin::V2::ConfigError) { config_file_obj }
ex.message.must_include('Failed to load')
ex.message.must_include('JSON')
ex.message.must_include('unexpected token')
end
end
describe 'when the file is valid' do
let(:fixture_name) { 'basic' }
it 'can count plugins' do
config_file_obj.count.must_equal 3
end
it 'can look up plugins by name with a String' do
config_file_obj.plugin_by_name('inspec-test-fixture-01').wont_be_nil
config_file_obj.plugin_by_name('inspec-test-fixture-99').must_be_nil
end
it 'can look up plugins by name with a Symbol' do
config_file_obj.plugin_by_name(:'inspec-test-fixture-01').wont_be_nil
config_file_obj.plugin_by_name(:'inspec-test-fixture-99').must_be_nil
end
it 'symbolizes the keys of the entries' do
config_file_obj.each do |entry|
entry.keys.each do |key|
key.must_be_kind_of(Symbol)
end
end
end
it 'implements Enumerable' do
config_file_obj.select { |entry| entry[:name].to_s.start_with?('inspec-test-fixture') }.count.must_equal 3
end
end
#----------------------------------------------------------#
# Validation
#----------------------------------------------------------#
describe 'when the file is invalid' do
describe 'because the file version is wrong' do
let(:fixture_name) { 'bad_plugin_conf_version' }
it 'throws an exception' do
ex = assert_raises(Inspec::Plugin::V2::ConfigError) { config_file_obj }
ex.message.must_include('Unsupported')
ex.message.must_include('version')
ex.message.must_include('99.99.9')
ex.message.must_include('1.0.0')
end
end
describe 'because the file version is missing' do
let(:fixture_name) { 'missing_plugin_conf_version' }
it 'throws an exception' do
ex = assert_raises(Inspec::Plugin::V2::ConfigError) { config_file_obj }
ex.message.must_include('Missing')
ex.message.must_include('version')
ex.message.must_include('1.0.0')
end
end
describe 'because the plugins field is missing' do
let(:fixture_name) { 'missing_plugins_key' }
it 'throws an exception' do
ex = assert_raises(Inspec::Plugin::V2::ConfigError) { config_file_obj }
ex.message.must_include('missing')
ex.message.must_include("'plugins'")
ex.message.must_include('array')
end
end
describe 'because the plugins field is not an array' do
let(:fixture_name) { 'hash_plugins_key' }
it 'throws an exception' do
ex = assert_raises(Inspec::Plugin::V2::ConfigError) { config_file_obj }
ex.message.must_include('Malformed')
ex.message.must_include("'plugins'")
ex.message.must_include('array')
end
end
describe 'because a plugin entry is not a hash' do
let(:fixture_name) { 'entry_not_hash' }
it 'throws an exception' do
ex = assert_raises(Inspec::Plugin::V2::ConfigError) { config_file_obj }
ex.message.must_include('Malformed')
ex.message.must_include('Hash')
ex.message.must_include('at index 2')
end
end
describe 'because it contains duplicate plugin entries' do
let(:fixture_name) { 'entry_duplicate' }
it 'throws an exception' do
ex = assert_raises(Inspec::Plugin::V2::ConfigError) { config_file_obj }
ex.message.must_include('Malformed')
ex.message.must_include('duplicate')
ex.message.must_include('inspec-test-fixture-01')
ex.message.must_include('at index 1 and 3')
end
end
describe 'because a plugin entry does not have a name' do
let(:fixture_name) { 'entry_no_name' }
it 'throws an exception' do
ex = assert_raises(Inspec::Plugin::V2::ConfigError) { config_file_obj }
ex.message.must_include('Malformed')
ex.message.must_include("missing 'name'")
ex.message.must_include('at index 1')
end
end
describe 'because a plugin entry has an unrecognized installation type' do
let(:fixture_name) { 'entry_bad_installation_type' }
it 'throws an exception' do
ex = assert_raises(Inspec::Plugin::V2::ConfigError) { config_file_obj }
ex.message.must_include('Malformed')
ex.message.must_include('unrecognized installation_type')
ex.message.must_include("one of 'gem' or 'path'")
ex.message.must_include('at index 1')
end
end
describe 'because a path plugin entry does not have a path' do
let(:fixture_name) { 'entry_no_path_for_path_type' }
it 'throws an exception' do
ex = assert_raises(Inspec::Plugin::V2::ConfigError) { config_file_obj }
ex.message.must_include('Malformed')
ex.message.must_include('missing installation path')
ex.message.must_include('at index 2')
end
end
end
end
describe 'modifying the conf file' do
#----------------------------------------------------------#
# Adding Entries
#----------------------------------------------------------#
describe 'adding an entry' do
let(:fixture_name) { 'no_plugins' }
describe 'when the conf is empty' do
it 'should add one valid entry' do
config_file_obj.count.must_equal 0
config_file_obj.add_entry(name: 'inspec-test-fixture')
config_file_obj.count.must_equal 1
config_file_obj.plugin_by_name(:'inspec-test-fixture').wont_be_nil
end
end
describe 'when the conf has entries' do
let(:fixture_name) { 'basic' }
it 'should append one valid entry' do
config_file_obj.count.must_equal 3
config_file_obj.add_entry(name: 'inspec-test-fixture-03')
config_file_obj.count.must_equal 4
config_file_obj.plugin_by_name(:'inspec-test-fixture-03').wont_be_nil
end
end
describe 'when adding a gem entry' do
it 'should add a gem entry' do
config_file_obj.add_entry(
name: 'inspec-test-fixture-03',
installation_type: :gem,
)
entry = config_file_obj.plugin_by_name(:'inspec-test-fixture-03')
entry.wont_be_nil
entry[:installation_type].must_equal :gem
end
end
describe 'when adding a path entry' do
it 'should add a path entry' do
config_file_obj.add_entry(
name: 'inspec-test-fixture-03',
installation_type: :path,
installation_path: '/my/path.rb',
)
entry = config_file_obj.plugin_by_name(:'inspec-test-fixture-03')
entry.wont_be_nil
entry[:installation_type].must_equal :path
entry[:installation_path].must_equal '/my/path.rb'
end
end
describe 'when adding a duplicate plugin name' do
let(:fixture_name) { 'basic' }
it 'should throw an exception' do
assert_raises(Inspec::Plugin::V2::ConfigError) { config_file_obj.add_entry(name: 'inspec-test-fixture-02') }
end
end
describe 'when adding an invalid entry' do
it 'should throw an exception' do
[
{ name: 'inspec-test-fixture', installation_type: :path },
{ installation_type: :gem },
{ name: 'inspec-test-fixture', installation_type: :invalid },
{ 'name' => 'inspec-test-fixture' },
].each do |proposed_entry|
assert_raises(Inspec::Plugin::V2::ConfigError) { config_file_obj.add_entry(proposed_entry) }
end
end
end
end
#----------------------------------------------------------#
# Removing Entries
#----------------------------------------------------------#
describe 'removing an entry' do
let(:fixture_name) { 'basic' }
describe 'when the entry exists' do
it 'should remove the entry by symbol name' do
config_file_obj.count.must_equal 3
config_file_obj.plugin_by_name(:'inspec-test-fixture-01').wont_be_nil
config_file_obj.remove_entry(:'inspec-test-fixture-01')
config_file_obj.count.must_equal 2
config_file_obj.plugin_by_name(:'inspec-test-fixture-01').must_be_nil
end
it 'should remove the entry by String name' do
config_file_obj.count.must_equal 3
config_file_obj.plugin_by_name('inspec-test-fixture-01').wont_be_nil
config_file_obj.remove_entry('inspec-test-fixture-01')
config_file_obj.count.must_equal 2
config_file_obj.plugin_by_name('inspec-test-fixture-01').must_be_nil
end
end
describe 'when the entry does not exist' do
let(:fixture_name) { 'basic' }
it 'should throw an exception' do
config_file_obj.count.must_equal 3
config_file_obj.plugin_by_name(:'inspec-test-fixture-99').must_be_nil
ex = assert_raises(Inspec::Plugin::V2::ConfigError) { config_file_obj.remove_entry(:'inspec-test-fixture-99') }
ex.message.must_include 'No such entry'
ex.message.must_include 'inspec-test-fixture-99'
config_file_obj.count.must_equal 3
end
end
end
describe 'writing the file' do
let(:fixture_name) { 'unused' }
describe 'when the file does not exist' do
it 'is created' do
Dir.mktmpdir do |tmp_dir|
path = File.join(tmp_dir, 'plugins.json')
File.exist?(path).must_equal false
cfo_writer = Inspec::Plugin::V2::ConfigFile.new(path)
cfo_writer.add_entry(name: :'inspec-resource-lister')
cfo_writer.save
File.exist?(path).must_equal true
cfo_reader = Inspec::Plugin::V2::ConfigFile.new(path)
cfo_reader.existing_entry?(:'inspec-resource-lister').must_equal true
end
end
end
describe 'when the directory does not exist' do
it 'is created' do
Dir.mktmpdir do |tmp_dir|
path = File.join(tmp_dir, 'subdir', 'plugins.json')
File.exist?(path).must_equal false
cfo_writer = Inspec::Plugin::V2::ConfigFile.new(path)
cfo_writer.add_entry(name: :'inspec-resource-lister')
cfo_writer.save
File.exist?(path).must_equal true
cfo_reader = Inspec::Plugin::V2::ConfigFile.new(path)
cfo_reader.existing_entry?(:'inspec-resource-lister').must_equal true
end
end
end
describe 'when the file does exist' do
it 'is overwritten' do
Dir.mktmpdir do |tmp_dir|
path = File.join(tmp_dir, 'plugins.json')
cfo_writer = Inspec::Plugin::V2::ConfigFile.new(path)
cfo_writer.add_entry(name: :'inspec-resource-lister')
cfo_writer.save
File.exist?(path).must_equal true
cfo_modifier = Inspec::Plugin::V2::ConfigFile.new(path)
cfo_modifier.remove_entry(:'inspec-resource-lister')
cfo_modifier.add_entry(name: :'inspec-test-fixture')
cfo_modifier.save
cfo_reader = Inspec::Plugin::V2::ConfigFile.new(path)
cfo_reader.existing_entry?(:'inspec-resource-lister').must_equal false
cfo_reader.existing_entry?(:'inspec-test-fixture').must_equal true
end
end
end
end
end
end