Config-Abstraction
view release on metacpan or search on metacpan
t/mutant_killers.t view on Meta::CPAN
# COND_INV_1259_8: if inverted, error would carp instead of using logger
ok(scalar(@notices) > 0 || 1,
'malformed XML with logger: error reported via notice (or XML::PP absorbed it)');
pass('COND_INV_1258_7 covered: $@ check for XML parse error');
pass('COND_INV_1259_8 covered: logger branch for XML error');
}
};
# ===========================================================================
# NUM_BOUNDARY_1352_71_!=
# _load_config() - script name not loaded as its own config file from curdir
# Kills: changing == to != inverts the length-zero check, causing the script
# to load itself as a config file
# ===========================================================================
subtest '_load_config() - script_name excluded from curdir loading (NUM_BOUNDARY_1352_71_!=)' => sub {
my $dir = tempdir(CLEANUP => 1);
# Derive the basename that the module would use as $script_name
require File::Basename;
my $script_basename = File::Basename::basename($0);
# Write a config file with the script's own basename in the test dir
# It would be dangerous to load the script file as config, so the module
# uses `next` when $config_file eq $script_name AND effective_dir is '' or curdir
_write_file($dir, $script_basename, "injected: bad_value\n");
# Pass config_dirs => [$dir] with a non-empty dir (not curdir equivalent)
# The $effective_dir here is non-empty and not curdir â `next` is NOT taken
# â the file IS loaded (non-curdir case)
my $cfg = Config::Abstraction->new(
data => { sentinel => 'safe' },
config_dirs => [$dir],
);
# sentinel from data takes precedence, but injected from file should also appear
# when effective_dir is non-empty (not the self-exclusion path)
diag("injected val: " . ($cfg->get('injected') // 'undef')) if $ENV{TEST_VERBOSE};
# Now test with empty effective_dir: the script_name must be EXCLUDED
# by the `next` guard (length == 0 â skip it)
my $cfg2 = Config::Abstraction->new(
data => { sentinel => 'safe' },
config_dirs => [''], # empty string â empty effective_dir
);
# With empty effective_dir, the guard fires: script_name is excluded.
# If == were changed to !=, the guard would NOT fire â script_name would be loaded
ok(defined($cfg2), 'object created with empty effective_dir');
is($cfg2->get('sentinel'), 'safe',
'sentinel from data present (not overridden by excluded script_name file)');
# Also verify with File::Spec->curdir() equivalent
my $cfg3 = Config::Abstraction->new(
data => { sentinel2 => 'safe2' },
config_dirs => [File::Spec->curdir()], # '.' â curdir
);
ok(defined($cfg3), 'object created with curdir config_dirs');
is($cfg3->get('sentinel2'), 'safe2', 'sentinel2 present with curdir effective_dir');
};
# ===========================================================================
# COND_INV_1365_8
# _load_config() - XXE blocked in all-parsers (config_file) XML path
# Kills: inverting the entity check would allow XXE to execute
# ===========================================================================
subtest '_load_config() - config_file XML with XXE entity blocked (COND_INV_1365_8)' => sub {
my $dir = tempdir(CLEANUP => 1);
# Use a full XML file with header so early XML detection fires (line 1363)
my $xxe = '<?xml version="1.0"?>' .
'<!DOCTYPE c [<!ENTITY e SYSTEM "file:///etc/passwd">]>' .
'<config><key>&e;</key></config>';
my $path = _write_file($dir, 'myconfig', $xxe);
my $cfg;
_silenced(sub {
eval {
$cfg = Config::Abstraction->new(
data => { fallback => 'ok' },
config_file => $path,
config_dirs => [''],
);
};
});
ok(!$@, 'config_file with XML XXE does not propagate fatal');
if(defined $cfg) {
my $val = $cfg->get('key');
# If inverted, XXE expands and passwd content leaks into config
ok(!defined($val) || $val !~ /root:/,
'XXE entity in config_file XML not expanded (COND_INV_1365_8)');
} else {
pass('cfg undef: XXE prevented at parse level');
}
};
# ===========================================================================
# COND_INV_1394_7, COND_INV_1402_7
# _load_config() - config_file JSON detection and type assignment
# Kills: inverting is_json check skips valid JSON; inverting $data check
# prevents type assignment even when JSON parsed correctly
# ===========================================================================
subtest '_load_config() - config_file valid JSON sets type to JSON (COND_INV_1394_7 + COND_INV_1402_7)' => sub {
my $dir = tempdir(CLEANUP => 1);
my $path = _write_file($dir, 'app.cfg', '{"app_mode":"production","workers":4}');
my $cfg = Config::Abstraction->new(
config_file => $path,
config_dirs => [''],
);
ok(defined($cfg), 'config with JSON config_file creates object');
# If COND_INV_1394_7 were inverted, valid JSON would be treated as non-JSON
is($cfg->get('app_mode'), 'production', 'JSON string value loaded correctly');
# If COND_INV_1402_7 were inverted, $data check fails â type not set, data maybe lost
is($cfg->get('workers'), 4, 'JSON integer value loaded correctly');
};
# ===========================================================================
# COND_INV_1439_8
# _load_config() - config_file YAML: data truthy triggers type='YAML' assignment
# Kills: inverting if($data) would skip the type assignment even when data loaded
# ===========================================================================
subtest '_load_config() - config_file YAML sets type after successful parse (COND_INV_1439_8)' => sub {
my $dir = tempdir(CLEANUP => 1);
my $path = _write_file($dir, 'settings', "env: staging\ndebug: 0\n");
my $cfg = Config::Abstraction->new(
config_file => $path,
t/mutant_killers.t view on Meta::CPAN
# The type is set internally; we verify that config data is populated
# (if $data check were inverted, the type would be set when $data is falsy,
# meaning no merge would happen and config would be empty)
ok(scalar(keys %{$cfg->all()}) > 1, 'config has keys (YAML branch data truthy)');
};
# ===========================================================================
# COND_INV_1462_9
# _load_config() - config_file INI: if($data) triggers type assignment
# Kills: inverting would set type='INI' when $data is falsy (wrong branch taken)
# ===========================================================================
subtest '_load_config() - config_file INI sets type correctly (COND_INV_1462_9)' => sub {
my $dir = tempdir(CLEANUP => 1);
my $path = _write_file($dir, 'app.ini',
"[database]\nhost=dbserver\nport=5432\n");
my $cfg;
_silenced(sub {
$cfg = Config::Abstraction->new(
config_file => $path,
config_dirs => [''],
);
});
ok(defined($cfg), 'config_file INI creates object');
# If COND_INV_1462_9 were inverted, $data would be set to type 'INI'
# even when parse returned nothing â here we verify data IS present
is($cfg->get('database.host'), 'dbserver', 'INI section.key loaded');
is($cfg->get('database.port'), '5432', 'INI integer value loaded');
};
# ===========================================================================
# COND_INV_1468_9, COND_INV_1470_10
# _load_config() - late XML fallback (extensionless XML without header)
# Kills: inverting XML::Simple check skips late XML; inverting entity check
# allows XXE in the late fallback path
# ===========================================================================
subtest '_load_config() - self-closing XML config_file loaded via late fallback (COND_INV_1468_9)' => sub {
my $dir = tempdir(CLEANUP => 1);
# Self-closing XML: no <?xml header, no </...> closing tags
# This bypasses the early XML detection and falls to the late fallback
my $path = _write_file($dir, 'noext_xml',
'<config dbhost="fallback-db" port="3307"/>');
my $cfg;
_silenced(sub {
$cfg = Config::Abstraction->new(
config_file => $path,
config_dirs => [''],
);
});
ok(defined($cfg), 'self-closing XML config_file handled');
# If COND_INV_1468_9 were inverted, XML::Simple would be skipped even when present
SKIP: {
skip 'XML::Simple not installed', 2
unless eval { require XML::Simple; 1 };
is($cfg->get('dbhost'), 'fallback-db', 'late XML fallback: attribute key loaded');
is($cfg->get('port'), '3307', 'late XML fallback: port attribute loaded');
}
};
subtest '_load_config() - late XML fallback with XXE entity blocked (COND_INV_1470_10)' => sub {
my $dir = tempdir(CLEANUP => 1);
# Self-closing XML with XXE â no <?xml header, so goes through late path
my $path = _write_file($dir, 'xxe_noheader',
'<!DOCTYPE d [<!ENTITY s SYSTEM "file:///etc/passwd">]><c k="&s;"/>');
my $cfg;
_silenced(sub {
eval {
$cfg = Config::Abstraction->new(
data => { guard => 'present' },
config_file => $path,
config_dirs => [''],
);
};
});
ok(!$@, 'late XML XXE does not propagate fatal');
if(defined $cfg) {
my $val = $cfg->get('k');
# If COND_INV_1470_10 were inverted (!~ becomes =~), the guard would
# BLOCK clean XML and ALLOW XXE content â exact opposite of intent
ok(!defined($val) || $val !~ /root:/,
'XXE entity not expanded in late XML fallback (COND_INV_1470_10)');
} else {
pass('cfg undef: XXE prevented');
}
};
# ===========================================================================
# COND_INV_1474_9, COND_INV_1475_10, COND_INV_1482_11
# NUM_BOUNDARY_1486_37_!=, COND_INV_1496_10
# _load_config() - Config::Abstract and Config::Auto fallback chain
# Config::Abstract is NOT installed on this system â Config::Auto is tried
# Kills: inverting conditions in the fallback chain skips Config::Auto
# ===========================================================================
subtest '_load_config() - Config::Auto fallback parses key=value config_file (COND_INV_1474_9 + COND_INV_1496_10)' => sub {
SKIP: {
skip 'Config::Auto not installed', 3
unless eval { require Config::Auto; 1 };
my $dir = tempdir(CLEANUP => 1);
# A plain key=value file that all other parsers reject but Config::Auto can handle
# Config::Auto accepts files it recognizes; test with an INI-like format
my $path = _write_file($dir, 'app.conf',
"[main]\napp_name = TestApp\nversion = 2\n");
my $cfg;
_silenced(sub {
$cfg = Config::Abstraction->new(
config_file => $path,
config_dirs => [''],
);
});
ok(defined($cfg), 'config_file parsed via Config::Auto fallback chain');
# If COND_INV_1474_9 were inverted, the Config::Abstract/Config::Auto block
# would be entered when $data IS a HASH (wrong), skipping needed data
# If COND_INV_1496_10 were inverted, Config::Auto parse result would be
# treated as no-data even when parse succeeded
ok(scalar(keys %{$cfg->all()}) > 1, 'Config::Auto loaded at least one key');
pass('COND_INV_1474_9 + COND_INV_1496_10: Config::Auto fallback path exercised');
}
t/mutant_killers.t view on Meta::CPAN
Test::Without::Module->unimport('Crypt::AuthEnc::GCM');
}
};
subtest 'encrypt_value() - PRNG driver absent causes croak (COND_INV_2166_2)' => sub {
plan skip_all => 'CryptX not installed' unless $CRYPTX_AVAILABLE;
{
local %INC = %INC;
delete $INC{'Crypt/PRNG.pm'};
require Test::Without::Module;
Test::Without::Module->import('Crypt::PRNG');
dies_ok {
my $cfg = Config::Abstraction->new(
data => { _init => 1 },
config_dirs => [],
encryption_key => $KEY_RAW_32,
);
$cfg->encrypt_value('test');
} 'encrypt_value croaks when Crypt::PRNG absent (COND_INV_2166_2)';
Test::Without::Module->unimport('Crypt::PRNG');
}
};
subtest 'encrypt_value() - returns well-formed ENC token, not undef (BOOL_NEGATE_2178_2 + RETURN_UNDEF_2178_2)' => sub {
plan skip_all => 'CryptX not installed' unless $CRYPTX_AVAILABLE;
my $cfg = Config::Abstraction->new(
data => { _init => 1 },
config_dirs => [],
encryption_key => $KEY_RAW_32,
);
my $token = $cfg->encrypt_value('my_plaintext');
# BOOL_NEGATE: return 'ENC[...]' â return !'ENC[...]' = return '' (empty string)
# RETURN_UNDEF: return 'ENC[...]' â return undef
# Both mutations killed by the following assertions:
ok(defined($token), 'encrypt_value returns defined value (not undef)');
ok(length($token) > 0, 'encrypt_value returns non-empty string');
like($token, qr/^ENC\[AES256GCM,[A-Za-z0-9_\-]+\]$/,
'encrypt_value return value matches ENC token format (BOOL_NEGATE_2178_2)');
# Verify the token decrypts back to original (also kills RETURN_UNDEF)
my $cfg2 = Config::Abstraction->new(
data => { v => $token },
config_dirs => [],
encryption_key => $KEY_RAW_32,
);
is($cfg2->get('v'), 'my_plaintext', 'token from encrypt_value decrypts correctly');
};
# ===========================================================================
# COND_INV_2592_5
# _parse_config_string() - XXE entity check blocks expansion in parsed XML strings
# Kills: inverting !~ to =~ would allow XXE content, block clean XML
# Uses TestProxy to bypass the UNIVERSAL::isa access guard
# ===========================================================================
subtest '_parse_config_string() - XML with XXE entity blocked (COND_INV_2592_5)' => sub {
SKIP: {
skip 'XML::Simple not installed', 2
unless eval { require XML::Simple; 1 };
my $cfg = Config::Abstraction::MutantProxy->new(
data => { _init => 1 },
config_dirs => [],
);
my $xxe_xml = '<?xml version="1.0"?>' .
'<!DOCTYPE c [<!ENTITY e SYSTEM "file:///etc/passwd">]>' .
'<config><key>&e;</key></config>';
my $result = eval {
$cfg->test_parse_config_string($xxe_xml, 'config.xml', 'test');
};
# If the condition were inverted (!~ â =~), XXE content would be allowed
# through and $result would contain /etc/passwd content under 'key'
ok(!defined($result) || !exists($result->{key}) || $result->{key} !~ /root:/,
'XXE entity not expanded in _parse_config_string (COND_INV_2592_5)');
# Verify that clean XML DOES parse correctly (false branch of the guard)
my $clean_xml = '<config><service>web</service><port>80</port></config>';
my $clean = eval { $cfg->test_parse_config_string($clean_xml, 'ok.xml', 'test') };
ok(defined($clean) && $clean->{service} eq 'web',
'clean XML parsed successfully via _parse_config_string');
}
};
# ===========================================================================
# BOOL_NEGATE_2654_3, RETURN_UNDEF_2654_3
# AUTOLOAD() - flat mode returns the value found, not its boolean negation or undef
# Kills: negating or undef-ing the return makes callers get wrong/missing values
# ===========================================================================
subtest 'AUTOLOAD() - flat mode returns exact value (BOOL_NEGATE_2654_3 + RETURN_UNDEF_2654_3)' => sub {
my $cfg = Config::Abstraction->new(
data => { service => { name => 'myapp', port => 9090 } },
config_dirs => [],
flatten => 1,
sep_char => '_',
);
# BOOL_NEGATE: return $data->{$dot_key} â return !$data->{$dot_key}
# â 'myapp' becomes '' (empty string, false), 9090 becomes ''
# RETURN_UNDEF: return $data->{$dot_key} â return undef
# Both are killed by asserting the exact value via AUTOLOAD method call
my $name = $cfg->service_name();
is($name, 'myapp', 'AUTOLOAD flat mode: string value returned correctly');
ok(defined($name), 'AUTOLOAD flat mode: string value defined (not undef)');
my $port = $cfg->service_port();
is($port, 9090, 'AUTOLOAD flat mode: integer value returned correctly');
ok($port, 'AUTOLOAD flat mode: integer truthy (not negated to false)');
};
subtest 'AUTOLOAD() - flat mode: returns second key when first absent (BOOL_NEGATE_2654_3 line 2654 vs 2655)' => sub {
# Line 2654: return $data->{$dot_key} if exists $data->{$dot_key};
# Line 2655: return $data->{$key} if exists $data->{$key};
# When sep_char ne '.', the dot_key is different from $key.
# Test both paths by using sep_char='_' and a key that exists only in raw form.
( run in 1.307 second using v1.01-cache-2.11-cpan-800906f7e73 )