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 )