App-Envdoctor
view release on metacpan or search on metacpan
t/scanner.t view on Meta::CPAN
my $dir5 = File::Temp->newdir;
open my $ec, '>', File::Spec->catfile( "$dir5", '.env' ) or die $!;
print {$ec} "PORT=8080\n";
close $ec;
open my $edd, '>', File::Spec->catfile( "$dir5", '.env.production' ) or die $!;
print {$edd} "PORT=9090\n";
close $edd;
open my $s5, '>', File::Spec->catfile( "$dir5", 'app.pl' ) or die $!;
print {$s5} "\$ENV{PORT};\n";
close $s5;
my $f5 = App::Envdoctor::Scanner::scan("$dir5");
ok( !( grep { $_->{rule} eq 'type-mismatch' } @$f5 ),
'two integers across envs -> no type-mismatch' );
# diff + sync subcommands
{
my $d2 = File::Temp->newdir;
open my $e, '>', File::Spec->catfile( "$d2", '.env' ) or die $!;
print {$e} "A=1\nB=2\n"; close $e;
open my $p, '>', File::Spec->catfile( "$d2", '.env.production' ) or die $!;
print {$p} "A=9\n"; close $p;
my $diff = App::Envdoctor::Scanner::diff_labels( "$d2", 'default', 'production' );
is_deeply( $diff->{onlyInA}, ['B'], 'diff onlyInA' );
is_deeply( $diff->{onlyInB}, [], 'diff onlyInB' );
is_deeply( $diff->{common}, ['A'], 'diff common' );
my $dry = App::Envdoctor::Scanner::sync_labels( "$d2", 'default', 'production', 1 );
is_deeply( $dry, ['B'], 'sync --dry-run reports B' );
unlike( App::Envdoctor::Scanner::_read( File::Spec->catfile("$d2",'.env.production') ), qr/B=/, 'dry-run does not write' );
App::Envdoctor::Scanner::sync_labels( "$d2", 'default', 'production', 0 );
my $prod = App::Envdoctor::Scanner::_read( File::Spec->catfile("$d2",'.env.production') );
ok( $prod =~ /B=\n/ && $prod =~ /A=9/ && $prod !~ /B=2/, 'sync appends B= without value' );
}
# ---- Docker Compose + GitHub Actions + Kubernetes source scanning -----------
{
my $dir = File::Temp->newdir;
open my $e, '>', File::Spec->catfile( "$dir", '.env' ) or die $!;
print {$e} "DB_URL=postgres://localhost\n"; close $e;
open my $c, '>', File::Spec->catfile( "$dir", 'docker-compose.yml' ) or die $!;
print {$c} "services:\n app:\n environment:\n"
. " - SECRET=\${COMPOSE_SECRET}\n - URL=\${DB_URL}\n - LIT=\$\$NOT_A_VAR\n";
close $c;
my $wfdir = File::Spec->catdir( "$dir", '.github', 'workflows' );
File::Path::make_path($wfdir);
open my $w, '>', File::Spec->catfile( $wfdir, 'ci.yml' ) or die $!;
print {$w} "jobs:\n deploy:\n steps:\n"
. " - run: deploy --key \${{ secrets.DEPLOY_KEY }} --region \${{ vars.REGION }}\n";
close $w;
my $f = App::Envdoctor::Scanner::scan("$dir");
my %err = map { $_->{name} => $_ } grep { $_->{rule} eq 'undefined-in-source' } @$f;
ok( $err{COMPOSE_SECRET}, 'COMPOSE_SECRET referenced from compose' );
ok( $err{DEPLOY_KEY}, 'DEPLOY_KEY referenced from actions secrets context' );
ok( $err{REGION}, 'REGION referenced from actions vars context' );
is( $err{COMPOSE_SECRET}{message},
'referenced but not defined in any environment file',
'undefined-in-source uses new message' );
ok( !$err{NOT_A_VAR}, 'escaped $$ not treated as a variable' );
ok( !( grep { $_->{rule} eq 'unused' && $_->{name} eq 'DB_URL' } @$f ),
'DB_URL used via compose interpolation, not unused' );
my $blob = join "\n", map { $_->{message} } @$f;
ok( $blob !~ /postgres/, 'no values leak from infra scanning' );
}
# ---- Kubernetes manifest interpolation --------------------------------------
{
my $dir = File::Temp->newdir;
open my $k, '>', File::Spec->catfile( "$dir", 'deploy.yaml' ) or die $!;
print {$k} "apiVersion: apps/v1\nkind: Deployment\nspec:\n value: \${K8S_VAR}\n";
close $k;
my $f = App::Envdoctor::Scanner::scan("$dir");
ok( ( grep { $_->{rule} eq 'undefined-in-source' && $_->{name} eq 'K8S_VAR' } @$f ),
'K8S_VAR referenced from a kubernetes manifest' );
}
# ---- generate / init / fix --------------------------------------------------
{
my $dir = File::Temp->newdir;
open my $e, '>', File::Spec->catfile( "$dir", '.env' ) or die $!;
print {$e} "DB_URL=secretvalue\n";
close $e;
open my $s, '>', File::Spec->catfile( "$dir", 'app.pl' ) or die $!;
print {$s} 'my $p = $ENV{PORT};' . "\n";
close $s;
my ( $example, $md ) = App::Envdoctor::Scanner::generate("$dir");
is( $example,
"# Generated by envdoctor. Fill in values; do not commit secrets.\nDB_URL=\nPORT=\n",
'generate .env.example exact bytes' );
like( $md, qr/\Q| DB_URL | yes | no |\E/, 'ENVIRONMENT.md DB_URL row' );
like( $md, qr/\Q| PORT | no | yes |\E/, 'ENVIRONMENT.md PORT row' );
unlike( $example, qr/secretvalue/, 'no values in .env.example' );
unlike( $md, qr/secretvalue/, 'no values in ENVIRONMENT.md' );
my $bin = "$FindBin::Bin/../bin/envdoctor";
my $ex_path = File::Spec->catfile( "$dir", '.env.example' );
my $md_path = File::Spec->catfile( "$dir", 'ENVIRONMENT.md' );
my $slurp = sub {
open my $r, '<', $_[0] or die $!;
local $/;
my $c = <$r>;
close $r;
return $c;
};
my $out1 = qx{$^X "$bin" init -d "$dir"};
like( $out1, qr/created \.env\.example/, 'init creates .env.example' );
like( $out1, qr/created ENVIRONMENT\.md/, 'init creates ENVIRONMENT.md' );
ok( -e $ex_path && -e $md_path, 'init wrote both files' );
my $out2 = qx{$^X "$bin" init -d "$dir"};
like( $out2, qr/skipped \.env\.example \(exists\)/, 'init skips existing' );
open my $t, '>', $ex_path or die $!;
print {$t} "TAMPER\n";
close $t;
qx{$^X "$bin" init -d "$dir"};
( run in 1.628 second using v1.01-cache-2.11-cpan-54e63673c56 )