inspec 0.12.0 → 0.14.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +39 -2
- data/bin/inspec +11 -9
- data/docs/matchers.rst +129 -0
- data/docs/resources.rst +64 -37
- data/inspec.gemspec +1 -1
- data/lib/bundles/inspec-compliance/cli.rb +1 -1
- data/lib/bundles/inspec-compliance/configuration.rb +1 -0
- data/lib/bundles/inspec-compliance/target.rb +16 -32
- data/lib/bundles/inspec-init/cli.rb +2 -0
- data/lib/bundles/inspec-supermarket.rb +13 -0
- data/lib/bundles/inspec-supermarket/api.rb +2 -0
- data/lib/bundles/inspec-supermarket/cli.rb +2 -2
- data/lib/bundles/inspec-supermarket/target.rb +11 -15
- data/lib/fetchers/local.rb +31 -0
- data/lib/fetchers/tar.rb +48 -0
- data/lib/fetchers/url.rb +100 -0
- data/lib/fetchers/zip.rb +47 -0
- data/lib/inspec.rb +2 -3
- data/lib/inspec/fetcher.rb +22 -0
- data/lib/inspec/metadata.rb +4 -2
- data/lib/inspec/plugins.rb +2 -0
- data/lib/inspec/plugins/fetcher.rb +97 -0
- data/lib/inspec/plugins/source_reader.rb +36 -0
- data/lib/inspec/profile.rb +92 -81
- data/lib/inspec/resource.rb +1 -0
- data/lib/inspec/runner.rb +15 -35
- data/lib/inspec/source_reader.rb +32 -0
- data/lib/inspec/version.rb +1 -1
- data/lib/matchers/matchers.rb +5 -6
- data/lib/resources/file.rb +8 -2
- data/lib/resources/passwd.rb +71 -45
- data/lib/resources/service.rb +13 -9
- data/lib/resources/shadow.rb +135 -0
- data/lib/source_readers/flat.rb +38 -0
- data/lib/source_readers/inspec.rb +78 -0
- data/lib/utils/base_cli.rb +2 -2
- data/lib/utils/parser.rb +1 -1
- data/lib/utils/plugin_registry.rb +93 -0
- data/test/docker_test.rb +1 -1
- data/test/helper.rb +62 -2
- data/test/integration/cookbooks/os_prepare/recipes/service.rb +4 -2
- data/test/integration/test/integration/default/compare_matcher_spec.rb +11 -0
- data/test/integration/test/integration/default/service_spec.rb +16 -1
- data/test/unit/fetchers.rb +61 -0
- data/test/unit/fetchers/local_test.rb +67 -0
- data/test/unit/fetchers/tar_test.rb +36 -0
- data/test/unit/fetchers/url_test.rb +152 -0
- data/test/unit/fetchers/zip_test.rb +36 -0
- data/test/unit/mock/files/passwd +1 -1
- data/test/unit/mock/files/shadow +2 -0
- data/test/unit/mock/profiles/complete-profile/libraries/testlib.rb +1 -0
- data/test/unit/plugin_test.rb +0 -1
- data/test/unit/profile_test.rb +32 -53
- data/test/unit/resources/passwd_test.rb +69 -14
- data/test/unit/resources/shadow_test.rb +67 -0
- data/test/unit/source_reader_test.rb +17 -0
- data/test/unit/source_readers/flat_test.rb +61 -0
- data/test/unit/source_readers/inspec_test.rb +38 -0
- data/test/unit/utils/passwd_parser_test.rb +1 -1
- metadata +40 -21
- data/lib/inspec/targets.rb +0 -10
- data/lib/inspec/targets/archive.rb +0 -33
- data/lib/inspec/targets/core.rb +0 -56
- data/lib/inspec/targets/dir.rb +0 -144
- data/lib/inspec/targets/file.rb +0 -33
- data/lib/inspec/targets/folder.rb +0 -38
- data/lib/inspec/targets/tar.rb +0 -61
- data/lib/inspec/targets/url.rb +0 -78
- data/lib/inspec/targets/zip.rb +0 -55
- data/test/unit/targets.rb +0 -132
@@ -0,0 +1,38 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# author: Dominik Richter
|
3
|
+
# author: Christoph Hartmann
|
4
|
+
|
5
|
+
require 'inspec/fetcher'
|
6
|
+
require 'inspec/metadata'
|
7
|
+
|
8
|
+
module SourceReaders
|
9
|
+
class Flat < Inspec.source_reader(1)
|
10
|
+
name 'flat'
|
11
|
+
priority 5
|
12
|
+
|
13
|
+
def self.resolve(target)
|
14
|
+
# TODO: eventually remove the metadata.rb exception here
|
15
|
+
# when we have fully phased out metadata.rb in 1.0
|
16
|
+
files = target.files.find_all { |x|
|
17
|
+
x.end_with?('.rb') && !x.include?('/') && x != 'metadata.rb'
|
18
|
+
}
|
19
|
+
return nil if files.empty?
|
20
|
+
new(target, files)
|
21
|
+
end
|
22
|
+
|
23
|
+
attr_reader :metadata, :tests, :libraries
|
24
|
+
|
25
|
+
def initialize(target, files)
|
26
|
+
@target = target
|
27
|
+
@metadata = ::Inspec::Metadata.new(nil)
|
28
|
+
@tests = load_tests(files)
|
29
|
+
@libraries = {}
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def load_tests(files)
|
35
|
+
Hash[files.map { |x| [x, @target.read(x)] }]
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# author: Dominik Richter
|
3
|
+
# author: Christoph Hartmann
|
4
|
+
|
5
|
+
require 'inspec/fetcher'
|
6
|
+
require 'inspec/metadata'
|
7
|
+
|
8
|
+
module SourceReaders
|
9
|
+
class InspecReader < Inspec.source_reader(1)
|
10
|
+
name 'inspec'
|
11
|
+
priority 10
|
12
|
+
|
13
|
+
def self.resolve(target)
|
14
|
+
return new(target, 'inspec.yml') if target.files.include?('inspec.yml')
|
15
|
+
# TODO: deprecated for 1.0.0 release
|
16
|
+
if target.files.include?('metadata.rb') &&
|
17
|
+
(
|
18
|
+
target.files.include?('controls') ||
|
19
|
+
target.files.include?('test')
|
20
|
+
)
|
21
|
+
return new(target, 'metadata.rb')
|
22
|
+
end
|
23
|
+
nil
|
24
|
+
end
|
25
|
+
|
26
|
+
attr_reader :metadata, :tests, :libraries
|
27
|
+
|
28
|
+
def initialize(target, metadata_source)
|
29
|
+
@target = target
|
30
|
+
@metadata = Inspec::Metadata.from_ref(
|
31
|
+
metadata_source,
|
32
|
+
@target.read(metadata_source),
|
33
|
+
nil)
|
34
|
+
|
35
|
+
@tests = load_tests
|
36
|
+
@libraries = load_libs
|
37
|
+
prepare_load_paths
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def load_tests
|
43
|
+
tests = @target.files.find_all do |path|
|
44
|
+
path.start_with?('controls', 'test') && path.end_with?('.rb')
|
45
|
+
end
|
46
|
+
Hash[tests.map { |x| [x, @target.read(x)] }]
|
47
|
+
end
|
48
|
+
|
49
|
+
def load_libs
|
50
|
+
tests = @target.files.find_all do |path|
|
51
|
+
path.start_with?('libraries') && path.end_with?('.rb')
|
52
|
+
end
|
53
|
+
Hash[tests.map { |x| [x, @target.read(x)] }]
|
54
|
+
end
|
55
|
+
|
56
|
+
# Ensure each test directory exists on the $LOAD_PATH. This
|
57
|
+
# will ensure traditional RSpec-isms like `require 'spec_helper'`
|
58
|
+
# continue to work. The method outlined here is only meant to be temporary!
|
59
|
+
def prepare_load_paths
|
60
|
+
t = @target
|
61
|
+
t = @target.parent unless @target.parent.nil?
|
62
|
+
unless t.is_a?(Fetchers::Local)
|
63
|
+
return # no need to mess with load-paths if this is not on disk
|
64
|
+
end
|
65
|
+
|
66
|
+
rel_dirs = (@libraries.keys + @tests.keys)
|
67
|
+
.map { |x| File.dirname(x) }.uniq
|
68
|
+
|
69
|
+
abs_dirs = rel_dirs.map { |x| @target.abs_path(x) }
|
70
|
+
.find_all { |x| File.directory?(x) }
|
71
|
+
.map { |x| File.expand_path(x) }
|
72
|
+
|
73
|
+
abs_dirs.each do |dir|
|
74
|
+
$LOAD_PATH.unshift dir unless $LOAD_PATH.include?(dir)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
data/lib/utils/base_cli.rb
CHANGED
@@ -42,13 +42,13 @@ module Inspec
|
|
42
42
|
private
|
43
43
|
|
44
44
|
# helper method to run tests
|
45
|
-
def run_tests(
|
45
|
+
def run_tests(targets, opts)
|
46
46
|
o = opts.dup
|
47
47
|
o[:logger] = Logger.new(opts['format'] == 'json' ? nil : STDOUT)
|
48
48
|
o[:logger].level = get_log_level(o.log_level)
|
49
49
|
|
50
50
|
runner = Inspec::Runner.new(o)
|
51
|
-
runner.
|
51
|
+
targets.each { |target| runner.add_target(target, opts) }
|
52
52
|
exit runner.run
|
53
53
|
rescue RuntimeError => e
|
54
54
|
puts e.message
|
data/lib/utils/parser.rb
CHANGED
@@ -0,0 +1,93 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# author: Dominik Richter
|
3
|
+
# author: Christoph Hartmann
|
4
|
+
|
5
|
+
class PluginRegistry
|
6
|
+
attr_reader :registry
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@registry = {}
|
10
|
+
end
|
11
|
+
|
12
|
+
# Resolve a target via available plugins.
|
13
|
+
#
|
14
|
+
# @param [String] target to resolve
|
15
|
+
# @return [Plugin] plugin instance if it can be resolved, nil otherwise
|
16
|
+
def resolve(target)
|
17
|
+
modules.each do |m|
|
18
|
+
res = m.resolve(target)
|
19
|
+
return res unless res.nil?
|
20
|
+
end
|
21
|
+
nil
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
# Get all registered plugins sorted by priority, with highest first
|
27
|
+
#
|
28
|
+
# @return [Array[Plugin]] sorted list of plugins
|
29
|
+
def modules
|
30
|
+
@registry.values
|
31
|
+
.sort_by { |x| x.respond_to?(:priority) ? x.priority : 0 }
|
32
|
+
.reverse
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
class PluginRegistry
|
37
|
+
class Plugin
|
38
|
+
# Retrieve the plugin registry associated with this plugin
|
39
|
+
#
|
40
|
+
# @return [PluginRegistry] plugin registry for this plugin
|
41
|
+
def self.plugin_registry
|
42
|
+
fail "Plugin #{self} does not implement `self.plugin_registry()`. This method is required"
|
43
|
+
end
|
44
|
+
|
45
|
+
# Register a new plugin by name
|
46
|
+
#
|
47
|
+
# @param [String] the unique name of this plugin
|
48
|
+
# @return [nil] disregard
|
49
|
+
def self.name(name)
|
50
|
+
fail "Trying to register #{self} with name == nil" if name.nil?
|
51
|
+
@name = name
|
52
|
+
plugin_registry.registry[name] = self
|
53
|
+
end
|
54
|
+
|
55
|
+
# This plugin's priority. Set it by providing the priority as an
|
56
|
+
# argument. Higher numbers ensure that the plugin is
|
57
|
+
# called early to check if a target belongs to it. When called without
|
58
|
+
# an argument, it retrieves this plugin's priority. Defaults to 0.
|
59
|
+
#
|
60
|
+
# @param [Numeric] Priority as a number. Will only be set if != nil
|
61
|
+
# @return [Numeric] This plugin's priority
|
62
|
+
def self.priority(x = nil)
|
63
|
+
@priority = x unless x.nil?
|
64
|
+
@priority || 0
|
65
|
+
end
|
66
|
+
|
67
|
+
# Try to resolve the target. If this plugin cannot handle it, the result
|
68
|
+
# will be nil. If, however, the plugin can resolve it, the resulting
|
69
|
+
# object will be an instance of this plugin. This means, that the interface
|
70
|
+
# that this base class provides, is the basis for the returned type.
|
71
|
+
#
|
72
|
+
# @param [String] target to try to resolve
|
73
|
+
# @return [Plugin] instance if it can be resolved, nil otherwise
|
74
|
+
def self.resolve(_target)
|
75
|
+
fail "Plugin #{self} does not implement `self.resolve(target)`. This method is required"
|
76
|
+
end
|
77
|
+
|
78
|
+
# When a plugin's resolve doesn't lead to the final state, it can
|
79
|
+
# use this method to hand it back for another resolver to handle.
|
80
|
+
#
|
81
|
+
# @param [Any] the current target that needs resolving
|
82
|
+
# @param [Plugin] an instance of the calling resolver
|
83
|
+
# @return [Plugin] instance if it can be resolved, nil otherwise
|
84
|
+
def self.resolve_next(target, parent)
|
85
|
+
res = plugin_registry.resolve(target)
|
86
|
+
res.parent = parent
|
87
|
+
res
|
88
|
+
end
|
89
|
+
|
90
|
+
attr_reader :target
|
91
|
+
attr_accessor :parent
|
92
|
+
end
|
93
|
+
end
|
data/test/docker_test.rb
CHANGED
@@ -42,7 +42,7 @@ class DockerTester
|
|
42
42
|
puts "--> run test on docker #{container.id}"
|
43
43
|
opts = { 'target' => "docker://#{container.id}" }
|
44
44
|
runner = Inspec::Runner.new(opts)
|
45
|
-
runner.
|
45
|
+
@tests.each { |test| runner.add_target(test, opts) }
|
46
46
|
runner.tests.map { |g| g.run(report) }
|
47
47
|
end
|
48
48
|
end
|
data/test/helper.rb
CHANGED
@@ -14,12 +14,19 @@ SimpleCov.start do
|
|
14
14
|
add_group 'Backends', 'lib/inspec/backend'
|
15
15
|
end
|
16
16
|
|
17
|
+
require 'fileutils'
|
18
|
+
require 'pathname'
|
19
|
+
require 'tempfile'
|
20
|
+
require 'zip'
|
21
|
+
|
17
22
|
require 'utils/base_cli'
|
18
|
-
require 'inspec/
|
23
|
+
require 'inspec/fetcher'
|
24
|
+
require 'inspec/source_reader'
|
19
25
|
require 'inspec/resource'
|
20
26
|
require 'inspec/backend'
|
21
27
|
require 'inspec/profile'
|
22
|
-
|
28
|
+
require 'inspec/runner'
|
29
|
+
require 'inspec/runner_mock'
|
23
30
|
|
24
31
|
class MockLoader
|
25
32
|
# collects emulation operating systems
|
@@ -44,6 +51,8 @@ class MockLoader
|
|
44
51
|
undefined: { family: nil, release: nil, arch: nil },
|
45
52
|
}
|
46
53
|
|
54
|
+
@archives = {}
|
55
|
+
|
47
56
|
# pass the os identifier to emulate a specific operating system
|
48
57
|
def initialize(os = nil)
|
49
58
|
# selects operating system
|
@@ -86,6 +95,7 @@ class MockLoader
|
|
86
95
|
'/etc/ssh/ssh_config' => mockfile.call('ssh_config'),
|
87
96
|
'/etc/ssh/sshd_config' => mockfile.call('sshd_config'),
|
88
97
|
'/etc/passwd' => mockfile.call('passwd'),
|
98
|
+
'/etc/shadow' => mockfile.call('shadow'),
|
89
99
|
'/etc/ntp.conf' => mockfile.call('ntp.conf'),
|
90
100
|
'/etc/login.defs' => mockfile.call('login.defs'),
|
91
101
|
'/etc/security/limits.conf' => mockfile.call('limits.conf'),
|
@@ -238,6 +248,56 @@ class MockLoader
|
|
238
248
|
resource.inspec.backend
|
239
249
|
.mock_command(cmd, res[:stdout], res[:stderr], res[:exit_status])
|
240
250
|
end
|
251
|
+
|
252
|
+
def self.home
|
253
|
+
File.join(File.dirname(__FILE__), 'unit')
|
254
|
+
end
|
255
|
+
|
256
|
+
def self.profile_path(name)
|
257
|
+
dst = name
|
258
|
+
dst = "#{home}/mock/profiles/#{name}" unless name.start_with?(home)
|
259
|
+
dst
|
260
|
+
end
|
261
|
+
|
262
|
+
def self.load_profile(name, opts = {})
|
263
|
+
opts[:test_collector] = Inspec::RunnerMock.new
|
264
|
+
Inspec::Profile.for_target(profile_path(name), opts)
|
265
|
+
end
|
266
|
+
|
267
|
+
def self.profile_tgz(name)
|
268
|
+
path = File.join(home, 'mock', 'profiles', name)
|
269
|
+
archive = Tempfile.new([name, '.tar.gz'])
|
270
|
+
dst = archive.path
|
271
|
+
archive.close
|
272
|
+
|
273
|
+
# generate relative paths
|
274
|
+
files = Dir.glob("#{path}/**/*")
|
275
|
+
relatives = files.map { |e| Pathname.new(e).relative_path_from(Pathname.new(path)).to_s }
|
276
|
+
|
277
|
+
require 'inspec/archive/tar'
|
278
|
+
tag = Inspec::Archive::TarArchiveGenerator.new
|
279
|
+
tag.archive(path, relatives, dst)
|
280
|
+
@archives[dst] = archive
|
281
|
+
|
282
|
+
dst
|
283
|
+
end
|
284
|
+
|
285
|
+
def self.profile_zip(name, opts = {})
|
286
|
+
path = File.join(home, 'mock', 'profiles', name)
|
287
|
+
archive = Tempfile.new([name, '.zip'])
|
288
|
+
dst = archive.path
|
289
|
+
archive.close
|
290
|
+
|
291
|
+
# rubyzip only works relative paths
|
292
|
+
files = Dir.glob("#{path}/**/*")
|
293
|
+
relatives = files.map { |e| Pathname.new(e).relative_path_from(Pathname.new(path)).to_s }
|
294
|
+
|
295
|
+
require 'inspec/archive/zip'
|
296
|
+
zag = Inspec::Archive::ZipArchiveGenerator.new
|
297
|
+
zag.archive(path, relatives, dst)
|
298
|
+
@archives[dst] = archive
|
299
|
+
dst
|
300
|
+
end
|
241
301
|
end
|
242
302
|
|
243
303
|
def load_resource(*args)
|
@@ -12,6 +12,8 @@ when 'ubuntu'
|
|
12
12
|
|
13
13
|
when 'centos'
|
14
14
|
# install runit for alternative service mgmt
|
15
|
-
|
16
|
-
|
15
|
+
if node['platform_version'].to_i >= 6
|
16
|
+
include_recipe 'os_prepare::_runit_service_centos'
|
17
|
+
include_recipe 'os_prepare::_upstart_service_centos'
|
18
|
+
end
|
17
19
|
end
|
@@ -18,4 +18,15 @@ if os.linux?
|
|
18
18
|
its('LogLevel') { should cmp 'info' }
|
19
19
|
its('LogLevel') { should cmp 'InfO' }
|
20
20
|
end
|
21
|
+
|
22
|
+
describe passwd.passwords.uniq do
|
23
|
+
it { should eq ['x'] }
|
24
|
+
it { should cmp ['x'] }
|
25
|
+
it { should cmp 'x' }
|
26
|
+
end
|
27
|
+
|
28
|
+
describe passwd.usernames do
|
29
|
+
it { should include 'root' }
|
30
|
+
it { should_not cmp 'root' }
|
31
|
+
end
|
21
32
|
end
|
@@ -47,7 +47,7 @@ if os[:family] == 'ubuntu'
|
|
47
47
|
end
|
48
48
|
|
49
49
|
# extra tests for alt. runit on centos with runit_service
|
50
|
-
if os[:family] == 'centos'
|
50
|
+
if os[:family] == 'centos' && os[:release].to_i >= 6
|
51
51
|
describe runit_service('running-runit-service') do
|
52
52
|
it { should be_enabled }
|
53
53
|
it { should be_installed }
|
@@ -103,3 +103,18 @@ if os[:family] == 'centos'
|
|
103
103
|
it { should_not be_running }
|
104
104
|
end
|
105
105
|
end
|
106
|
+
|
107
|
+
# extra tests for sys-v runlevels
|
108
|
+
if os[:family] == 'centos' && os[:release].to_i <= 6
|
109
|
+
describe service('sshd').runlevels do
|
110
|
+
its('keys') { should include(2) }
|
111
|
+
end
|
112
|
+
|
113
|
+
describe service('sshd').runlevels(2, 4) do
|
114
|
+
it { should be_enabled }
|
115
|
+
end
|
116
|
+
|
117
|
+
describe service('sshd').runlevels(0, 1) do
|
118
|
+
it { should_not be_enabled }
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# author: Dominik Richter
|
3
|
+
# author: Christoph Hartmann
|
4
|
+
|
5
|
+
require 'helper'
|
6
|
+
|
7
|
+
describe Inspec::Fetcher do
|
8
|
+
it 'loads the local fetcher for this file' do
|
9
|
+
res = Inspec::Fetcher.resolve(__FILE__)
|
10
|
+
res.must_be_kind_of Fetchers::Local
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
describe Inspec::Plugins::RelFetcher do
|
15
|
+
def fetcher
|
16
|
+
src_fetcher.expects(:files).returns(in_files).at_least_once
|
17
|
+
Inspec::Plugins::RelFetcher.new(src_fetcher)
|
18
|
+
end
|
19
|
+
|
20
|
+
let(:src_fetcher) { mock() }
|
21
|
+
|
22
|
+
IN_AND_OUT = {
|
23
|
+
[] => [],
|
24
|
+
%w{file} => %w{file},
|
25
|
+
# don't prefix just by filename
|
26
|
+
%w{file file_a} => %w{file file_a},
|
27
|
+
%w{path/file path/file_a} => %w{file file_a},
|
28
|
+
%w{path/to/file} => %w{file},
|
29
|
+
%w{/path/to/file} => %w{file},
|
30
|
+
%w{alice bob} => %w{alice bob},
|
31
|
+
# mixed paths
|
32
|
+
%w{x/a y/b} => %w{x/a y/b},
|
33
|
+
%w{/x/a /y/b} => %w{x/a y/b},
|
34
|
+
%w{z/x/a z/y/b} => %w{x/a y/b},
|
35
|
+
%w{/z/x/a /z/y/b} => %w{x/a y/b},
|
36
|
+
# mixed with relative path
|
37
|
+
%w{a path/to/b} => %w{a path/to/b},
|
38
|
+
%w{path/to/b a} => %w{path/to/b a},
|
39
|
+
%w{path/to/b path/a} => %w{to/b a},
|
40
|
+
%w{path/to/b path/a c} => %w{path/to/b path/a c},
|
41
|
+
# mixed with absolute paths
|
42
|
+
%w{/path/to/b /a} => %w{path/to/b a},
|
43
|
+
%w{/path/to/b /path/a} => %w{to/b a},
|
44
|
+
%w{/path/to/b /path/a /c} => %w{path/to/b path/a c},
|
45
|
+
# mixing absolute and relative paths
|
46
|
+
%w{path/a /path/b} => %w{path/a /path/b},
|
47
|
+
%w{/path/a path/b} => %w{/path/a path/b},
|
48
|
+
# extract folder structure buildup
|
49
|
+
%w{/a /a/b /a/b/c} => %w{c},
|
50
|
+
%w{/a /a/b /a/b/c/d/e} => %w{e},
|
51
|
+
}.each do |ins, outs|
|
52
|
+
describe 'empty profile' do
|
53
|
+
let(:in_files) { ins }
|
54
|
+
|
55
|
+
it 'also has no files' do
|
56
|
+
fetcher.files.must_equal outs
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|