<?php

use Illuminate\Support\Facades\File;

/**
 * Nothing runs inside the app container without saying who it is running as.
 *
 * `docker compose exec` is root unless told otherwise, and the deployment
 * scripts relied on that default. When `artisan optimize` failed during a
 * deployment it wrote its error into storage/logs/laravel.log AS ROOT — and
 * from that moment the application could not append to its own log. Monolog
 * threw on every attempt, and a throw while logging is a 500 on every page that
 * logs, with nothing written down to say why.
 *
 * It surfaced days later as a 500 on the VPN config download, that being one of
 * the few pages writing a log line on its way through, and was chased through
 * the VPN code, the encryption key and the session before anyone looked at the
 * owner of a file.
 *
 * docker/entrypoint.sh had it right all along: it drops to www-data for exactly
 * these commands. The rule is only that the user is named — `-u root` is a fine
 * answer where root is what is wanted (the ownership repair in update.sh), as
 * long as somebody chose it.
 */
it('never runs a command in the app container as whoever docker felt like', function () {
    $offenders = [];

    foreach (File::glob(base_path('deploy/*.sh')) as $path) {
        foreach (preg_split('/\R/', File::get($path)) ?: [] as $i => $line) {
            if (! str_contains($line, 'docker compose exec')) {
                continue;
            }

            // Only the app container. The gateway and the queues are other
            // images with other users, and none of them own this checkout.
            if (! preg_match('/docker compose exec\b[^|]*?\bapp\b/', $line)) {
                continue;
            }

            if (preg_match('/\s-u\s+\S+/', $line)) {
                continue;
            }

            // Help text counts. Telling an operator to run it as root is how
            // the file ends up owned by root in the first place.
            $offenders[] = basename($path).':'.($i + 1).' — '.trim($line);
        }
    }

    expect($offenders)->toBe([]);
});

it('repairs ownership before it needs it, not after', function () {
    // A server already carrying the damage has to heal on its next deployment.
    // Nothing else ever will: a root-owned log file stays root-owned, and the
    // page that trips over it is nowhere near the deployment that caused it.
    $update = File::get(base_path('deploy/update.sh'));

    expect($update)->toContain('normalise_ownership')
        // Every directory the deployment writes into, and node_modules by name:
        // the first version sampled the top-level owner and skipped the
        // recursion when it matched, which is how a root-owned
        // node_modules/.vite-temp under a www-data-owned node_modules failed
        // the build in maintenance mode.
        ->and($update)->toContain('! -user www-data -exec chown www-data:www-data')
        ->and($update)->toContain('node_modules');

    // Before the first unprivileged step, or it cannot help: composer and npm
    // would already have failed on a root-owned vendor directory.
    $repair = strpos($update, "\nnormalise_ownership\n");
    $maintenance = strpos($update, 'php artisan down --retry=60');

    expect($repair)->not->toBeFalse()
        ->and($maintenance)->not->toBeFalse()
        ->and($repair)->toBeLessThan($maintenance);
});
