diff --git a/lib/resources/csv.rb b/lib/resources/csv.rb index d4416adf1e..451ef3ec78 100644 --- a/lib/resources/csv.rb +++ b/lib/resources/csv.rb @@ -34,6 +34,8 @@ def parse(content) # convert to hash csv.to_a.map(&:to_hash) + rescue => e + raise Inspec::Exceptions::ResourceFailed, "Unable to parse CSV: #{e.message}" end # override the value method from JsonConfig @@ -45,8 +47,12 @@ def value(key) @params.map { |x| x[key.first.to_s] }.compact end - def to_s - "Csv #{@path}" + private + + # used by JsonConfig to build up a full to_s method + # based on whether a file path, content, or command was supplied. + def resource_base_name + 'CSV' end end end diff --git a/lib/resources/ini.rb b/lib/resources/ini.rb index 4eeea18d04..85d34f3716 100644 --- a/lib/resources/ini.rb +++ b/lib/resources/ini.rb @@ -18,8 +18,12 @@ def parse(content) SimpleConfig.new(content).params end - def to_s - "INI #{@path}" + private + + # used by JsonConfig to build up a full to_s method + # based on whether a file path, content, or command was supplied. + def resource_base_name + 'INI' end end end diff --git a/lib/resources/json.rb b/lib/resources/json.rb index 301e5ee16e..95a3330b60 100644 --- a/lib/resources/json.rb +++ b/lib/resources/json.rb @@ -26,45 +26,11 @@ class JsonConfig < Inspec.resource(1) include ObjectTraverser # make params readable - attr_reader :params + attr_reader :params, :raw_content def initialize(opts) - @opts = opts - if opts.is_a?(Hash) - if opts.key?(:content) - @file_content = opts[:content] - elsif opts.key?(:command) - @command = inspec.command(opts[:command]) - @file_content = @command.stdout - end - else - @path = opts - @file = inspec.file(@opts) - @file_content = @file.content - - # check if file is available - if !@file.file? - skip_resource "Can't find file \"#{@path}\"" - return @params = {} - end - - # check if file is readable - if @file_content.nil? && !@file.empty? - skip_resource "Can't read file \"#{@path}\"" - return @params = {} - end - end - - @params = parse(@file_content) - end - - def parse(content) - require 'json' - JSON.parse(content) - end - - def value(key) - extract_value(key, @params) + @raw_content = load_raw_content(opts) + @params = parse(@raw_content) end # Shorthand to retrieve a parameter name via `#its`. @@ -79,12 +45,65 @@ def method_missing(*keys) value(keys) end + def value(key) + # uses ObjectTraverser.extract_value to walk the hash looking for the key, + # which may be an Array of keys for a nested Hash. + extract_value(key, params) + end + def to_s - if @opts.is_a?(Hash) && @opts.key?(:content) - 'Json content' + "#{resource_base_name} #{@resource_name_supplement || 'content'}" + end + + private + + def parse(content) + require 'json' + JSON.parse(content) + rescue => e + raise Inspec::Exceptions::ResourceFailed, "Unable to parse JSON: #{e.message}" + end + + def load_raw_content(opts) + # if the opts isn't a hash, we assume it's a path to a file + unless opts.is_a?(Hash) + @resource_name_supplement = opts + return load_raw_from_file(opts) + end + + if opts.key?(:command) + @resource_name_supplement = "from command: #{opts[:command]}" + load_raw_from_command(opts[:command]) + elsif opts.key?(:content) + opts[:content] else - "Json #{@path}" + raise Inspec::Exceptions::ResourceFailed, 'No JSON content; must specify a file, command, or raw JSON content' end end + + def load_raw_from_file(path) + file = inspec.file(path) + + # these are currently ResourceSkipped to maintain consistency with the resource + # pre-refactor (which used skip_resource). These should likely be changed to + # ResourceFailed during a major version bump. + raise Inspec::Exceptions::ResourceSkipped, "No such file: #{path}" unless file.file? + raise Inspec::Exceptions::ResourceSkipped, "File #{path} is empty or is not readable by current user" if file.content.nil? || file.content.empty? + + file.content + end + + def load_raw_from_command(command) + command_output = inspec.command(command).stdout + raise Inspec::Exceptions::ResourceSkipped, "No output from command: #{command}" if command_output.nil? || command_output.empty? + + command_output + end + + # for resources the subclass JsonConfig, this allows specification of the resource + # base name in each subclass so we can build a good to_s method + def resource_base_name + 'JSON' + end end end diff --git a/lib/resources/toml.rb b/lib/resources/toml.rb index 421f5f97dc..c432ed50f6 100644 --- a/lib/resources/toml.rb +++ b/lib/resources/toml.rb @@ -17,10 +17,16 @@ class TomlConfig < JsonConfig def parse(content) Tomlrb.parse(content) + rescue => e + raise Inspec::Exceptions::ResourceFailed, "Unable to parse TOML: #{e.message}" end - def to_s - "TOML #{@path}" + private + + # used by JsonConfig to build up a full to_s method + # based on whether a file path, content, or command was supplied. + def resource_base_name + 'TOML' end end end diff --git a/lib/resources/xml.rb b/lib/resources/xml.rb index 284b266ffb..7e3a5d985a 100644 --- a/lib/resources/xml.rb +++ b/lib/resources/xml.rb @@ -14,14 +14,20 @@ class XmlConfig < JsonConfig def parse(content) require 'rexml/document' REXML::Document.new(content) + rescue => e + raise Inspec::Exceptions::ResourceFailed, "Unable to parse XML: #{e.message}" end def value(key) REXML::XPath.each(@params, key.first.to_s).map(&:text) end - def to_s - "XML #{@path}" + private + + # used by JsonConfig to build up a full to_s method + # based on whether a file path, content, or command was supplied. + def resource_base_name + 'XML' end end end diff --git a/lib/resources/yaml.rb b/lib/resources/yaml.rb index dbf2e07c8f..dadc142a10 100644 --- a/lib/resources/yaml.rb +++ b/lib/resources/yaml.rb @@ -30,10 +30,16 @@ class YamlConfig < JsonConfig # override file load and parse hash from yaml def parse(content) YAML.load(content) + rescue => e + raise Inspec::Exceptions::ResourceFailed, "Unable to parse YAML: #{e.message}" end - def to_s - "YAML #{@path}" + private + + # used by JsonConfig to build up a full to_s method + # based on whether a file path, content, or command was supplied. + def resource_base_name + 'YAML' end end end diff --git a/test/unit/resources/json_test.rb b/test/unit/resources/json_test.rb index 4b273a1a22..b36a4d13f5 100644 --- a/test/unit/resources/json_test.rb +++ b/test/unit/resources/json_test.rb @@ -37,7 +37,69 @@ let (:resource) { load_resource('json', 'nonexistent.json') } it 'produces an error' do - _(resource.resource_exception_message).must_equal 'Can\'t find file "nonexistent.json"' + _(resource.resource_exception_message).must_equal 'No such file: nonexistent.json' + end + end + + describe '#load_raw_from_file' do + let(:path) { '/path/to/file.txt' } + let(:resource) { Inspec::Resources::JsonConfig.allocate } + let(:inspec) { mock } + let(:file) { mock } + + before do + resource.stubs(:inspec).returns(inspec) + inspec.expects(:file).with(path).returns(file) + end + + it 'raises an exception when the file does not exist' do + file.expects(:file?).returns(false) + proc { resource.send(:load_raw_from_file, path) }.must_raise Inspec::Exceptions::ResourceSkipped + end + + it 'raises an exception if the file content is nil' do + file.expects(:file?).returns(true) + file.expects(:content).returns(nil) + proc { resource.send(:load_raw_from_file, path) }.must_raise Inspec::Exceptions::ResourceSkipped + end + + it 'raises an exception if the file content is empty' do + file.expects(:file?).returns(true) + file.expects(:content).at_least_once.returns('') + proc { resource.send(:load_raw_from_file, path) }.must_raise Inspec::Exceptions::ResourceSkipped + end + + it 'returns the file content' do + file.expects(:file?).returns(true) + file.expects(:content).at_least_once.returns('json goes here') + resource.send(:load_raw_from_file, path).must_equal 'json goes here' + end + end + + describe '#load_raw_from_file' do + let(:cmd_str) { 'curl localhost' } + let(:resource) { Inspec::Resources::JsonConfig.allocate } + let(:inspec) { mock } + let(:command) { mock } + + before do + resource.stubs(:inspec).returns(inspec) + inspec.expects(:command).with(cmd_str).returns(command) + end + + it 'raises an exception if command stdout is nil' do + command.expects(:stdout).returns(nil) + proc { resource.send(:load_raw_from_command, cmd_str) }.must_raise Inspec::Exceptions::ResourceSkipped + end + + it 'raises an exception if command stdout is empty' do + command.expects(:stdout).returns('') + proc { resource.send(:load_raw_from_command, cmd_str) }.must_raise Inspec::Exceptions::ResourceSkipped + end + + it 'returns the command output' do + command.expects(:stdout).returns('json goes here') + resource.send(:load_raw_from_command, cmd_str).must_equal 'json goes here' end end end