HEX
Server: LiteSpeed
System: Linux s1049.use1.mysecurecloudhost.com 4.18.0-477.27.2.lve.el8.x86_64 #1 SMP Wed Oct 11 12:32:56 UTC 2023 x86_64
User: xedaptot (3356)
PHP: 8.3.31
Disabled: NONE
Upload Files
File: /home/xedaptot/ai.naniguide.com/tests/Unit/UpgradeManagerPatchTest.php
<?php

/**
 * Unit tests for UpgradeManager helpers added for POST /api/v1/upgrade/run.
 *
 * Covers:
 *   1. downloadPatchTo — throws on bad URL, cleans up partial file
 *   2. applyPatchFromUrl — cleans up tmp zip even on download failure
 *   3. applyLoadedPatch — dry_run path with hand-crafted no-op patch fixture
 *
 * applyLoadedPatch replaces files only — migrate + upgrade:finalize are a separate
 * HTTP request (POST /api/v1/upgrade/finalize) so a fresh PHP worker can load the
 * newly-written code.
 *
 * NOTE: Real patch apply (run) is NOT tested here — too risky even with rollback.
 *       That path is verified via the smoke recipe in .support/HANDBOOK_DETAILS.md
 *       "Method API" section against a dev instance.
 */

use App\Library\UpgradeManager;

uses(\Tests\TestCase::class);

beforeEach(function () {
    $this->tmpDir = storage_path('tmp');
    if (!is_dir($this->tmpDir)) {
        @mkdir($this->tmpDir, 0755, true);
    }
});

afterEach(function () {
    // Wipe patch source dir so other tests don't see stale state
    $patchDir = storage_path('tmp/patch');
    if (is_dir($patchDir)) {
        (new \Illuminate\Filesystem\Filesystem())->deleteDirectory($patchDir);
    }
    // Wipe leftover api-upgrade-* zips
    foreach (glob(storage_path('tmp/api-upgrade-*.zip')) ?: [] as $f) {
        @unlink($f);
    }
});

// -----------------------------------------------------------------------------
// downloadPatchTo
// -----------------------------------------------------------------------------

it('downloadPatchTo throws and cleans up partial file when URL returns non-200', function () {
    $mgr = new UpgradeManager();
    $dest = storage_path('tmp/test-download-' . uniqid() . '.zip');

    // Use a localhost port nothing is listening on — connect refused or timeout
    expect(fn () => $mgr->downloadPatchTo('http://127.0.0.1:1/nonexistent.zip', $dest, [
        'timeout' => 2,
    ]))->toThrow(\Exception::class);

    expect(is_file($dest))->toBeFalse();
});

it('downloadPatchTo throws on connection error (unreachable host)', function () {
    $mgr = new UpgradeManager();
    $dest = storage_path('tmp/test-download-' . uniqid() . '.zip');

    expect(fn () => $mgr->downloadPatchTo('http://0.0.0.0:1/x.zip', $dest, [
        'timeout' => 2,
    ]))->toThrow(\Exception::class);
});

// -----------------------------------------------------------------------------
// applyPatchFromUrl
// -----------------------------------------------------------------------------

it('applyPatchFromUrl cleans up tmp zip when download fails', function () {
    $mgr = new UpgradeManager();
    $beforeCount = count(glob(storage_path('tmp/api-upgrade-*.zip')) ?: []);

    expect(fn () => $mgr->applyPatchFromUrl('http://127.0.0.1:1/nope.zip', [
        'timeout' => 2,
    ]))->toThrow(\Exception::class);

    $afterCount = count(glob(storage_path('tmp/api-upgrade-*.zip')) ?: []);
    expect($afterCount)->toBe($beforeCount);
});

// -----------------------------------------------------------------------------
// applyLoadedPatch — dry_run with hand-crafted no-op patch (no files to mutate)
// -----------------------------------------------------------------------------

it('applyLoadedPatch returns dry_run summary without mutating VERSION', function () {
    $mgr = new UpgradeManager();
    $patchDir = storage_path('tmp/patch');

    if (!is_dir($patchDir)) {
        @mkdir($patchDir, 0755, true);
    }

    $currentVersion = trim(file_get_contents(base_path('VERSION')));
    // Bump to a higher version so validate() won't reject as same/older
    $newVersion = $currentVersion . '+test';

    file_put_contents($patchDir . '/' . UpgradeManager::META_FILE, json_encode([
        'version' => $newVersion,
        'last_supported' => $currentVersion,
        'updated' => [],
        'deleted' => [],
        'packages' => [],
        'dirs' => [],
    ]));

    $summary = $mgr->applyLoadedPatch(['dry_run' => true]);

    expect($summary['dry_run'])->toBeTrue();
    expect($summary['old_version'])->toBe($currentVersion);
    expect($summary['new_version'])->toBe($currentVersion); // unchanged in dry_run
    expect($summary['patch_target_version'])->toBe($newVersion);
    expect($summary['duration_ms'])->toBeInt();
    // applyLoadedPatch no longer runs migrate — that's the /upgrade/finalize endpoint's job
    expect($summary)->not->toHaveKey('migrations_ran');
    expect($summary)->not->toHaveKey('migrate_output_tail');
    expect($summary)->not->toHaveKey('finalize_output_tail');

    // VERSION file unchanged on disk
    expect(trim(file_get_contents(base_path('VERSION'))))->toBe($currentVersion);
});

it('applyLoadedPatch dry_run reports patch_last_supported correctly', function () {
    $mgr = new UpgradeManager();
    $patchDir = storage_path('tmp/patch');

    if (!is_dir($patchDir)) {
        @mkdir($patchDir, 0755, true);
    }

    $currentVersion = trim(file_get_contents(base_path('VERSION')));
    file_put_contents($patchDir . '/' . UpgradeManager::META_FILE, json_encode([
        'version' => $currentVersion . '+test2',
        'last_supported' => '0.0.1',
        'updated' => [],
        'deleted' => [],
        'packages' => [],
        'dirs' => [],
    ]));

    $summary = $mgr->applyLoadedPatch(['dry_run' => true]);

    expect($summary['patch_last_supported'])->toBe('0.0.1');
});