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 )