From 98cf955354cb5560f0b67de70c68c41cacfba208 Mon Sep 17 00:00:00 2001 From: E98Developer Date: Mon, 17 Aug 2026 10:48:31 +0200 Subject: [PATCH] =?UTF-8?q?FIX=20DeploymentPackageTest=20felbont=C3=A1sa?= =?UTF-8?q?=20hat=20f=C3=A1jlra=20a=20p=C3=A1rhuzamos=20futtat=C3=A1shoz?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A paratest FÁJLONKÉNT osztja szét a munkát, ezért egyetlen 28 tesztes fájlból nem tudott párhuzamosítani: egy processz vitte a teljes 137 mp-et, a másik öt üresjáratban állt. A --parallel emiatt korábban egyenesen lassabb volt (136 -> 143 mp), a teljes suite-on pedig csak 11%-ot hozott. A felosztás témák szerint történt, de mért időkre kalibrálva, hogy egyik fájl se domináljon (a leghosszabb a Builder, ~35 mp): AccessTest 6 teszt ~7 mp jogosultság, feature flag, 403 GitTest 5 teszt ~18 mp GitRepository, ChangeSetAnalyzer BuilderTest 9 teszt ~35 mp csomagépítés, _TEENDOK, _torles.sh, verzió PageTest 4 teszt ~21 mp commitlista, keresés, kiválasztás CreationTest 2 teszt ~32 mp felületről indított csomagolás, kirakottnak jelölés DownloadTest 2 teszt ~25 mp naplóbejegyzés, ZIP letöltés, útvonal-ellenőrzés A két legdrágább teszt (21,7 és 20,4 mp) szándékosan külön fájlba került, együtt hagyva ők adnák a padlót. A közös segédfüggvények a DeploymentTestHelpers.php-ba kerültek. Ez nem *Test.php, ezért a phpunit nem szedi fel teszt-fájlként. A minta-repót takarító afterAll() helyére register_shutdown_function lépett: az afterAll fájlonként futna le, és a következő fájl alól húzná ki a mintát. Mért eredmény (6 mag): ezek a tesztek sorosan 137,00 mp, párhuzamosan 67,88 mp. Teljes suite párhuzamosan 149,78 -> 77,52 mp. Co-Authored-By: Claude Opus 5 --- .../DeploymentPackageAccessTest.php | 92 +++ .../DeploymentPackageBuilderTest.php | 168 +++++ .../DeploymentPackageCreationTest.php | 70 ++ .../DeploymentPackageDownloadTest.php | 70 ++ .../Deployment/DeploymentPackageGitTest.php | 95 +++ .../Deployment/DeploymentPackagePageTest.php | 90 +++ .../Deployment/DeploymentTestHelpers.php | 181 +++++ tests/Feature/DeploymentPackageTest.php | 668 ------------------ 8 files changed, 766 insertions(+), 668 deletions(-) create mode 100644 tests/Feature/Deployment/DeploymentPackageAccessTest.php create mode 100644 tests/Feature/Deployment/DeploymentPackageBuilderTest.php create mode 100644 tests/Feature/Deployment/DeploymentPackageCreationTest.php create mode 100644 tests/Feature/Deployment/DeploymentPackageDownloadTest.php create mode 100644 tests/Feature/Deployment/DeploymentPackageGitTest.php create mode 100644 tests/Feature/Deployment/DeploymentPackagePageTest.php create mode 100644 tests/Feature/Deployment/DeploymentTestHelpers.php delete mode 100644 tests/Feature/DeploymentPackageTest.php diff --git a/tests/Feature/Deployment/DeploymentPackageAccessTest.php b/tests/Feature/Deployment/DeploymentPackageAccessTest.php new file mode 100644 index 0000000..d45f4a7 --- /dev/null +++ b/tests/Feature/Deployment/DeploymentPackageAccessTest.php @@ -0,0 +1,92 @@ +isAllowed($developer))->toBeFalse() + ->and(app(DeploymentGuard::class)->denialReason($developer))->toContain('DEPLOYMENT_PACKAGE_ENABLED'); +}); + +test('a nem fejlesztői stage-en akkor sem érhető el, ha a kapcsoló be van kapcsolva', function () { + deploymentAllowEnvironment(); + Config::set('app.stage', 'PROD'); + + $developer = deploymentDeveloper(); + + expect(app(DeploymentGuard::class)->isAllowed($developer))->toBeFalse() + ->and(app(DeploymentGuard::class)->denialReason($developer))->toContain('fejlesztői környezetben'); +}); + +test('a developer szerep nélküli felhasználó nem éri el', function () { + deploymentAllowEnvironment(); + + Role::create(['name' => 'admin', 'display_name' => 'Admin']); + $admin = User::factory()->create(); + $admin->addRole(Role::where('name', 'admin')->first()); + + FeatureFlag::create([ + 'name' => 'DeploymentPackage', + 'label' => 'Deployment csomagoló', + 'enabled' => true, + 'stages' => null, + 'roles' => null, + ]); + app(FeatureFlagRegistrar::class)->registerAll(); + + expect(app(DeploymentGuard::class)->isAllowed($admin))->toBeFalse(); +}); + +test('a kikapcsolt feature flag elrejti a felületet a fejlesztő elől is', function () { + deploymentAllowEnvironment(); + + $developer = deploymentDeveloper(flagEnabled: false); + + expect(app(DeploymentGuard::class)->isAllowed($developer))->toBeFalse() + ->and(DeploymentPackage::canAccess())->toBeFalse(); +}); + +test('minden feltétel teljesülése esetén engedélyezett', function () { + deploymentAllowEnvironment(); + + $developer = deploymentDeveloper(); + + expect(app(DeploymentGuard::class)->isAllowed($developer))->toBeTrue() + ->and(app(DeploymentGuard::class)->denialReason($developer))->toBeNull(); +}); + +test('a jogosulatlan felhasználó 403-at kap az oldal betöltésekor', function () { + deploymentAllowEnvironment(); + + $developer = deploymentDeveloper(flagEnabled: false); + + // Szándékosan valódi HTTP kérés: a Livewire::test() a HttpException-t válasszá + // alakítja (RequestBroker: withoutExceptionHandling([HttpException::class])), így + // ott nem a tényleges belépési pontot mérnénk. + $this->actingAs($developer) + ->get(DeploymentPackage::getUrl()) + ->assertForbidden(); +}); diff --git a/tests/Feature/Deployment/DeploymentPackageBuilderTest.php b/tests/Feature/Deployment/DeploymentPackageBuilderTest.php new file mode 100644 index 0000000..385ecb1 --- /dev/null +++ b/tests/Feature/Deployment/DeploymentPackageBuilderTest.php @@ -0,0 +1,168 @@ +build('d2d', 'test', $first, $third); + $teendok = File::get($package['path'].'/_TEENDOK.md'); + + expect($teendok)->toContain('docker service update --force d2d-environment_queue') + ->and($teendok)->toContain('docker service update --force d2d-environment_scheduler') + // A 0 replikás scheduler miatt tudni kell, hogy a restart üresjárat lehet. + ->and($teendok)->toContain('0 replikán fut'); + + // A restartnak a cache ürítés UTÁN kell jönnie: a frissen induló worker különben + // a régi, cache-elt konfigot töltené be. + expect(strpos($teendok, 'docker service update'))->toBeGreaterThan(strpos($teendok, 'config:clear')); + + deploymentCleanup($repository['path']); +}); + +test('a queue worker nélküli környezetnél is kiderül, mit kell majd újraindítani', function () { + deploymentAllowEnvironment(); + $repository = deploymentTestRepository(); + [$first, , $third] = $repository['commits']; + + $package = app(DeploymentPackageBuilder::class)->build('e2e', 'main', $first, $third); + $teendok = File::get($package['path'].'/_TEENDOK.md'); + + expect($teendok)->toContain('docker service update --force t2t_scheduler') + ->and($teendok)->toContain('t2t_queue'); + + deploymentCleanup($repository['path']); +}); + +test('a teendők között szerepel a seeder futtatása a konkrét osztálynévvel', function () { + deploymentAllowEnvironment(); + $repository = deploymentTestRepository(); + [$first, , $third] = $repository['commits']; + + $package = app(DeploymentPackageBuilder::class)->build('e2e', 'main', $first, $third); + $teendok = File::get($package['path'].'/_TEENDOK.md'); + + expect($teendok)->toContain('php artisan db:seed --class=TesztSeeder') + // A seeder puszta felmásolása nem futtatja le - ezt ki kell mondani. + ->and($teendok)->toContain('NEM futtatja le') + // A Pennant purge feltételes: Eloquent mentésnél a FeatureFlagObserver elvégzi. + ->and($teendok)->toContain('pennant:purge') + ->and($teendok)->toContain('cache:clear'); + + deploymentCleanup($repository['path']); +}); + +test('a csomag a commitolt tartalmat viszi, nem a working tree állapotát', function () { + deploymentAllowEnvironment(); + $repository = deploymentTestRepository(); + [$first, , $third] = $repository['commits']; + + // A working tree szándékos elrontása a commit után: ez nem kerülhet a csomagba. + File::put($repository['path'].'/app/Árlista.php', "build('e2e', 'main', $first, $third); + + expect(File::get($package['path'].'/app/Árlista.php'))->not->toContain('NEM KERÜLHET') + // A .gitattributes eol=lf miatt a git archive LF-fel ír, Windowson is. + ->and(File::get($package['path'].'/app/Árlista.php'))->not->toContain("\r\n"); + + deploymentCleanup($repository['path']); +}); + +test('a csomagból kimarad a kizárt és a kézzel kivett fájl, a törlendők külön listára kerülnek', function () { + deploymentAllowEnvironment(); + $repository = deploymentTestRepository(); + [$first, , $third] = $repository['commits']; + + $package = app(DeploymentPackageBuilder::class)->build('e2e', 'main', $first, $third, ['app/Elso.php']); + + expect(File::exists($package['path'].'/app/Atnevezett.php'))->toBeTrue() + ->and(File::exists($package['path'].'/app/Elso.php'))->toBeFalse() + ->and(File::exists($package['path'].'/.env'))->toBeFalse() + ->and(File::exists($package['path'].'/app/Regi.php'))->toBeFalse() + ->and($package['counts']['skipped'])->toBe(1) + ->and($package['counts']['missing'])->toBe(0); + + $torlendo = File::get($package['path'].'/_TORLENDO.txt'); + + expect($torlendo)->toContain('app/Regi.php') + ->and($torlendo)->toContain('app/Atnevezendo.php'); + + deploymentCleanup($repository['path']); +}); + +test('a csomag rögzíti a kirakott verziót', function () { + deploymentAllowEnvironment(); + $repository = deploymentTestRepository(); + [$first, , $third] = $repository['commits']; + + $package = app(DeploymentPackageBuilder::class)->build('e2e', 'main', $first, $third); + + $version = json_decode(File::get($package['path'].'/public/deploy-version.json'), true); + + expect($version['commit'])->toBe($third) + ->and($version['target'])->toBe('e2e') + ->and($version['branch'])->toBe('main'); + + deploymentCleanup($repository['path']); +}); + +test('a limit fölötti fájlszámnál nem készül csomag', function () { + deploymentAllowEnvironment(); + $repository = deploymentTestRepository(); + [$first, , $third] = $repository['commits']; + + Config::set('deployment.max_files', 1); + + expect(fn () => app(DeploymentPackageBuilder::class)->build('e2e', 'main', $first, $third)) + ->toThrow(RuntimeException::class); + + expect(File::exists(config('deployment.output_path')))->toBeFalse(); + + deploymentCleanup($repository['path']); +}); + +test('a csomagoló szerveren akkor sem indul el, ha közvetlenül hívják', function () { + deploymentAllowEnvironment(); + $repository = deploymentTestRepository(); + [$first, , $third] = $repository['commits']; + + Config::set('app.stage', 'PROD'); + + expect(fn () => app(DeploymentPackageBuilder::class)->build('e2e', 'main', $first, $third)) + ->toThrow(RuntimeException::class); + + deploymentCleanup($repository['path']); +}); + +test('a törlő szkript dry runban fut és ellenőrzi az app gyökeret', function () { + deploymentAllowEnvironment(); + $repository = deploymentTestRepository(); + [$first, , $third] = $repository['commits']; + + $package = app(DeploymentPackageBuilder::class)->build('e2e', 'main', $first, $third); + $script = File::get($package['path'].'/_torles.sh'); + + expect($script)->toStartWith('#!/usr/bin/env bash') + ->and($script)->toContain('[[ -f artisan && -d app ]]') + ->and($script)->toContain('"app/Regi.php"') + // Windowson generáljuk, Linuxon fut: CRLF-fel a shebang sor törne el. + ->and($script)->not->toContain("\r\n"); + + deploymentCleanup($repository['path']); +}); diff --git a/tests/Feature/Deployment/DeploymentPackageCreationTest.php b/tests/Feature/Deployment/DeploymentPackageCreationTest.php new file mode 100644 index 0000000..2c6e809 --- /dev/null +++ b/tests/Feature/Deployment/DeploymentPackageCreationTest.php @@ -0,0 +1,70 @@ +actingAs(deploymentDeveloper()); + + $component = Livewire::test(DeploymentPackage::class) + ->call('selectFrom', $first) + ->call('selectTo', $third) + ->call('toggleFile', 'app/Elso.php') + ->call('createPackage'); + + $package = $component->get('lastPackage'); + + // Alapértelmezett célkörnyezet a d2d: azt frissítjük sűrűbben. + expect($package)->not->toBeNull() + ->and($package['name'])->toStartWith('deploy_d2d_') + ->and(File::isDirectory($package['path']))->toBeTrue() + ->and($package['skipped'])->toBe(['app/Elso.php']); + + deploymentCleanup($repository['path']); +}); + +test('a kirakottként megjelölt csomag adja a következő tartomány kezdetét', function () { + deploymentAllowEnvironment(); + $repository = deploymentTestRepository(); + [$first, $second] = $repository['commits']; + + $this->actingAs(deploymentDeveloper()); + + // Az alapértelmezett célkörnyezetre készül, hogy a második oldalbetöltés is ezt töltse elő. + $record = DeploymentPackageRecord::create([ + 'name' => 'deploy_d2d_teszt', + 'target' => 'd2d', + 'branch' => 'main', + 'from_commit' => $first, + 'to_commit' => $second, + 'folder' => $repository['path'], + ]); + + Livewire::test(DeploymentPackage::class) + ->call('markDeployed', $record->id) + ->assertSet('fromCommit', $second); + + expect($record->refresh()->deployed_at)->not->toBeNull(); + + // Új oldalbetöltésnél is a kirakott commitról indul a tartomány. + Livewire::test(DeploymentPackage::class)->assertSet('fromCommit', $second); + + deploymentCleanup($repository['path']); +}); diff --git a/tests/Feature/Deployment/DeploymentPackageDownloadTest.php b/tests/Feature/Deployment/DeploymentPackageDownloadTest.php new file mode 100644 index 0000000..d653c7b --- /dev/null +++ b/tests/Feature/Deployment/DeploymentPackageDownloadTest.php @@ -0,0 +1,70 @@ +actingAs(deploymentDeveloper()); + + Livewire::test(DeploymentPackage::class) + ->call('selectFrom', $first) + ->call('selectTo', $third) + ->call('createPackage'); + + $record = DeploymentPackageRecord::query()->latest('id')->first(); + + expect($record)->not->toBeNull() + ->and($record->target)->toBe('d2d') + ->and($record->to_commit)->toBe($third) + ->and($record->deployed_at)->toBeNull() + ->and(File::exists($record->zip_path))->toBeTrue(); + + Livewire::test(DeploymentPackage::class) + ->call('downloadPackage', $record->id) + ->assertFileDownloaded(); + + deploymentCleanup($repository['path']); +}); + +test('a kimeneti mappán kívüli ZIP útvonal nem tölthető le', function () { + deploymentAllowEnvironment(); + $repository = deploymentTestRepository(); + [$first, , $third] = $repository['commits']; + + $this->actingAs(deploymentDeveloper()); + + // Elrontott (vagy szándékosan átírt) rekord: a fájl létezik, de a csomagoló + // kimeneti mappáján kívül van. + $record = DeploymentPackageRecord::create([ + 'name' => 'deploy_e2e_hamis', + 'target' => 'e2e', + 'branch' => 'main', + 'from_commit' => $first, + 'to_commit' => $third, + 'folder' => $repository['path'], + 'zip_path' => base_path('composer.json'), + ]); + + Livewire::test(DeploymentPackage::class) + ->call('downloadPackage', $record->id) + ->assertNoFileDownloaded(); + + deploymentCleanup($repository['path']); +}); diff --git a/tests/Feature/Deployment/DeploymentPackageGitTest.php b/tests/Feature/Deployment/DeploymentPackageGitTest.php new file mode 100644 index 0000000..0e95f82 --- /dev/null +++ b/tests/Feature/Deployment/DeploymentPackageGitTest.php @@ -0,0 +1,95 @@ +commits('main', 10); + + expect($commits)->toHaveCount(3) + ->and($commits[0]['subject'])->toBe('harmadik commit') + ->and($commits[2]['subject'])->toBe('első commit') + ->and($commits[0]['hash'])->toHaveLength(40) + ->and($commits[0]['file_count'])->toBe(2) + // A törzs a tárgysor nélkül jön, és nem tapad hozzá a --shortstat sora sem. + ->and($commits[1]['body'])->toBe('A törzsben említett árlista részlet.') + ->and($commits[0]['body'])->toBe(''); + + deploymentCleanup($repository['path']); +}); + +test('a diff felismeri az új, módosult, törölt és átnevezett fájlokat', function () { + $repository = deploymentTestRepository(); + [$first, , $third] = $repository['commits']; + + $files = collect(app(GitRepository::class)->changedFiles($first, $third))->keyBy('path'); + + expect($files->get('app/Elso.php')['status'])->toBe('M') + ->and($files->get('app/Regi.php')['status'])->toBe('D') + ->and($files->get('app/Atnevezett.php')['status'])->toBe('R') + ->and($files->get('app/Atnevezett.php')['old_path'])->toBe('app/Atnevezendo.php') + // Ékezetes útvonal: core.quotePath=false nélkül "app/\303\201rlista.php" jönne vissza. + ->and($files->has('app/Árlista.php'))->toBeTrue(); + + deploymentCleanup($repository['path']); +}); + +test('az elemzés kizárja a tiltott fájlokat és külön kezeli a törlendőket', function () { + $repository = deploymentTestRepository(); + [$first, , $third] = $repository['commits']; + + $changedFiles = app(GitRepository::class)->changedFiles($first, $third); + $changeSet = app(ChangeSetAnalyzer::class)->analyze($changedFiles); + + expect($changeSet['copied'])->toContain('app/Árlista.php', 'app/Atnevezett.php', 'app/Elso.php') + ->and($changeSet['copied'])->not->toContain('.env') + ->and($changeSet['counts']['excluded'])->toBe(1) + // A törölt fájl mellett az átnevezés régi útvonala is törlendő a célgépen. + ->and($changeSet['deleted'])->toContain('app/Regi.php', 'app/Atnevezendo.php'); + + $titles = collect($changeSet['warnings'])->pluck('title')->implode(' | '); + + expect($titles)->toContain('Migráció') + ->and($titles)->toContain('Seeder') + ->and($titles)->toContain('Törlendő fájl'); + + deploymentCleanup($repository['path']); +}); + +test('az érvénytelen commit azonosítót nem adja tovább a gitnek', function () { + $repository = deploymentTestRepository(); + + $git = app(GitRepository::class); + + expect($git->resolveCommit('../../etc/passwd'))->toBeNull() + ->and($git->resolveCommit('main; rm -rf /'))->toBeNull() + ->and($git->resolveCommit('HEAD'))->toBeNull() + ->and($git->resolveCommit($repository['commits'][0]))->toBe($repository['commits'][0]); + + deploymentCleanup($repository['path']); +}); + +test('a fordított tartomány felismerhető', function () { + $repository = deploymentTestRepository(); + [$first, , $third] = $repository['commits']; + + $git = app(GitRepository::class); + + expect($git->isAncestor($first, $third))->toBeTrue() + ->and($git->isAncestor($third, $first))->toBeFalse(); + + deploymentCleanup($repository['path']); +}); diff --git a/tests/Feature/Deployment/DeploymentPackagePageTest.php b/tests/Feature/Deployment/DeploymentPackagePageTest.php new file mode 100644 index 0000000..dfdd42b --- /dev/null +++ b/tests/Feature/Deployment/DeploymentPackagePageTest.php @@ -0,0 +1,90 @@ +actingAs(deploymentDeveloper()); + + Livewire::test(DeploymentPackage::class) + ->set('data.search', 'árlista részlet') + ->assertSee('második commit') + ->assertDontSee('harmadik commit'); + + deploymentCleanup($repository['path']); +}); + +test('az oldal kilistázza a commitokat és a kijelölt tartomány fájljait', function () { + deploymentAllowEnvironment(); + $repository = deploymentTestRepository(); + [$first, , $third] = $repository['commits']; + + $this->actingAs(deploymentDeveloper()); + + Livewire::test(DeploymentPackage::class) + ->assertSuccessful() + ->assertSee('harmadik commit') + ->call('selectFrom', $first) + ->call('selectTo', $third) + ->assertSet('fromCommit', $first) + ->assertSet('toCommit', $third) + ->assertSee('app/Atnevezett.php') + ->assertSee('app/Regi.php'); + + deploymentCleanup($repository['path']); +}); + +test('az oldal alapból a d2d környezetet ajánlja és betöltéskor lekérdezi az élő állapotot', function () { + deploymentAllowEnvironment(); + $repository = deploymentTestRepository(); + [, , $third] = $repository['commits']; + + // A beforeEach-ben regisztrált catch-all stub minden URL-re illeszkedik, és a + // Http factory az ELSŐ találatot adja vissza - ezért itt új factory kell, különben + // az üres törzsű alapértelmezett válasz nyerne a konkrét stub helyett. + Http::swap(new Factory); + Http::fake([ + '*deploy-version.json' => Http::response(['target' => 'd2d', 'commit' => $third, 'branch' => 'main']), + ]); + + $this->actingAs(deploymentDeveloper()); + + $component = Livewire::test(DeploymentPackage::class); + + expect($component->get('data')['target'])->toBe('d2d') + // A lekérdezés gomb nélkül, magától megtörténik. + ->and($component->get('liveVersions')['d2d']['ok'])->toBeTrue() + ->and($component->get('liveVersions')['d2d']['data']['commit'])->toBe($third); + + deploymentCleanup($repository['path']); +}); + +test('az oldal nem fogad el kamu commit azonosítót', function () { + deploymentAllowEnvironment(); + $repository = deploymentTestRepository(); + + $this->actingAs(deploymentDeveloper()); + + Livewire::test(DeploymentPackage::class) + ->call('selectFrom', '../../etc/passwd') + ->assertSet('fromCommit', null); + + deploymentCleanup($repository['path']); +}); diff --git a/tests/Feature/Deployment/DeploymentTestHelpers.php b/tests/Feature/Deployment/DeploymentTestHelpers.php new file mode 100644 index 0000000..e9c71aa --- /dev/null +++ b/tests/Feature/Deployment/DeploymentTestHelpers.php @@ -0,0 +1,181 @@ + 'developer'], ['display_name' => 'Developer']); + + $user = User::factory()->create(); + $user->addRole($role); + + FeatureFlag::create([ + 'name' => 'DeploymentPackage', + 'label' => 'Deployment csomagoló', + 'enabled' => $flagEnabled, + 'stages' => null, + 'roles' => ['developer'], + ]); + + app(FeatureFlagRegistrar::class)->registerAll(); + + return $user; +} + +/** + * @param array $arguments + */ +function deploymentGit(string $path, array $arguments): string +{ + $process = new Process(array_merge(['git'], $arguments), $path); + $process->mustRun(); + + return $process->getOutput(); +} + +/** + * A minta-repó útvonala. A PID azért van benne, hogy párhuzamos futásnál (paratest) + * minden processznek saját mintája legyen. + */ +function deploymentTemplatePath(): string +{ + return sys_get_temp_dir().DIRECTORY_SEPARATOR.'deployment-package-minta-'.getmypid(); +} + +/** + * Determinisztikus minta-repó: átnevezés, törlés, ékezetes fájlnév és migráció is van benne. + * + * A 12 git parancs processzenként EGYSZER fut le - Windowson a processzindítás olyan drága, + * hogy tesztenként újraépítve ez önmagában ~4 mp lenne. A tesztek másolatot kapnak. + * + * @return array{path:string, commits:array} + */ +function deploymentRepositoryTemplate(): array +{ + static $template = null; + + // Ha a minta időközben eltűnt (kézi takarítás, megszakadt futás), újraépítjük. + if ($template !== null && is_dir($template['path'])) { + return $template; + } + + $elsoFelepites = $template === null; + $path = deploymentTemplatePath(); + + // Egy korábbi, megszakadt futás azonos PID-ű maradványa nem szennyezheti a mintát. + deploymentDeleteDirectory($path); + + File::makeDirectory($path.'/app', 0777, true); + File::makeDirectory($path.'/database/migrations', 0777, true); + File::makeDirectory($path.'/database/seeders', 0777, true); + + deploymentGit($path, ['init']); + deploymentGit($path, ['config', 'user.email', 'teszt@example.com']); + deploymentGit($path, ['config', 'user.name', 'Teszt']); + deploymentGit($path, ['config', 'commit.gpgsign', 'false']); + + File::put($path.'/app/Elso.php', " deploymentDeleteDirectory(deploymentTemplatePath())); + } + + return $template = ['path' => $path, 'commits' => explode("\n", $log)]; +} + +/** + * A teszt saját, írható másolata a mintáról: a builder és a "working tree elrontása" + * tesztek beleírnak a repóba, ezért nem oszthatnak közös példányt. + * + * @return array{path:string, commits:array} + */ +function deploymentTestRepository(): array +{ + $template = deploymentRepositoryTemplate(); + + $path = sys_get_temp_dir().DIRECTORY_SEPARATOR.'deployment-package-test-'.uniqid(); + + File::copyDirectory($template['path'], $path); + + Config::set('deployment.repo_path', $path); + Config::set('deployment.output_path', $path.DIRECTORY_SEPARATOR.'_kimenet'); + + return ['path' => $path, 'commits' => $template['commits']]; +} + +/** + * Facade nélküli törlés: a shutdown függvény a booteolt alkalmazáson kívül is fut. + */ +function deploymentDeleteDirectory(string $path): void +{ + if (! is_dir($path)) { + return; + } + + $items = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS), + RecursiveIteratorIterator::CHILD_FIRST + ); + + foreach ($items as $item) { + // A .git/objects tartalma Windowson csak olvasható, ezért törlés előtt fel kell oldani. + @chmod($item->getPathname(), 0777); + + $item->isDir() ? @rmdir($item->getPathname()) : @unlink($item->getPathname()); + } + + @rmdir($path); +} + +function deploymentCleanup(string $path): void +{ + deploymentDeleteDirectory($path); +} diff --git a/tests/Feature/DeploymentPackageTest.php b/tests/Feature/DeploymentPackageTest.php deleted file mode 100644 index 1b8c63d..0000000 --- a/tests/Feature/DeploymentPackageTest.php +++ /dev/null @@ -1,668 +0,0 @@ - 'developer'], ['display_name' => 'Developer']); - - $user = User::factory()->create(); - $user->addRole($role); - - FeatureFlag::create([ - 'name' => 'DeploymentPackage', - 'label' => 'Deployment csomagoló', - 'enabled' => $flagEnabled, - 'stages' => null, - 'roles' => ['developer'], - ]); - - app(FeatureFlagRegistrar::class)->registerAll(); - - return $user; -} - -/** - * @param array $arguments - */ -function deploymentGit(string $path, array $arguments): string -{ - $process = new Process(array_merge(['git'], $arguments), $path); - $process->mustRun(); - - return $process->getOutput(); -} - -/** - * A minta-repó útvonala. A PID azért van benne, hogy párhuzamos futásnál (paratest) - * minden processznek saját mintája legyen. - */ -function deploymentTemplatePath(): string -{ - return sys_get_temp_dir().DIRECTORY_SEPARATOR.'deployment-package-minta-'.getmypid(); -} - -/** - * Determinisztikus minta-repó: átnevezés, törlés, ékezetes fájlnév és migráció is van benne. - * - * A 12 git parancs futásonként EGYSZER fut le - Windowson a processzindítás olyan drága, - * hogy tesztenként újraépítve ez önmagában ~4 mp lenne. A tesztek másolatot kapnak. - * - * @return array{path:string, commits:array} - */ -function deploymentRepositoryTemplate(): array -{ - static $template = null; - - if ($template !== null) { - return $template; - } - - $path = deploymentTemplatePath(); - - // Egy korábbi, megszakadt futás azonos PID-ű maradványa nem szennyezheti a mintát. - deploymentDeleteDirectory($path); - - File::makeDirectory($path.'/app', 0777, true); - File::makeDirectory($path.'/database/migrations', 0777, true); - File::makeDirectory($path.'/database/seeders', 0777, true); - - deploymentGit($path, ['init']); - deploymentGit($path, ['config', 'user.email', 'teszt@example.com']); - deploymentGit($path, ['config', 'user.name', 'Teszt']); - deploymentGit($path, ['config', 'commit.gpgsign', 'false']); - - File::put($path.'/app/Elso.php', " $path, 'commits' => explode("\n", $log)]; -} - -/** - * A teszt saját, írható másolata a mintáról: a builder és a "working tree elrontása" - * tesztek beleírnak a repóba, ezért nem oszthatnak közös példányt. - * - * @return array{path:string, commits:array} - */ -function deploymentTestRepository(): array -{ - $template = deploymentRepositoryTemplate(); - - $path = sys_get_temp_dir().DIRECTORY_SEPARATOR.'deployment-package-test-'.uniqid(); - - File::copyDirectory($template['path'], $path); - - Config::set('deployment.repo_path', $path); - Config::set('deployment.output_path', $path.DIRECTORY_SEPARATOR.'_kimenet'); - - return ['path' => $path, 'commits' => $template['commits']]; -} - -/** - * Facade nélküli törlés: az afterAll a booteolt alkalmazáson kívül is fut. - */ -function deploymentDeleteDirectory(string $path): void -{ - if (! is_dir($path)) { - return; - } - - $items = new RecursiveIteratorIterator( - new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS), - RecursiveIteratorIterator::CHILD_FIRST - ); - - foreach ($items as $item) { - // A .git/objects tartalma Windowson csak olvasható, ezért törlés előtt fel kell oldani. - @chmod($item->getPathname(), 0777); - - $item->isDir() ? @rmdir($item->getPathname()) : @unlink($item->getPathname()); - } - - @rmdir($path); -} - -function deploymentCleanup(string $path): void -{ - deploymentDeleteDirectory($path); -} - -afterAll(function () { - deploymentDeleteDirectory(deploymentTemplatePath()); -}); - -test('a kikapcsolt env kapcsoló mindenkitől elveszi a hozzáférést', function () { - deploymentAllowEnvironment(); - Config::set('deployment.enabled', false); - - $developer = deploymentDeveloper(); - - expect(app(DeploymentGuard::class)->isAllowed($developer))->toBeFalse() - ->and(app(DeploymentGuard::class)->denialReason($developer))->toContain('DEPLOYMENT_PACKAGE_ENABLED'); -}); - -test('a nem fejlesztői stage-en akkor sem érhető el, ha a kapcsoló be van kapcsolva', function () { - deploymentAllowEnvironment(); - Config::set('app.stage', 'PROD'); - - $developer = deploymentDeveloper(); - - expect(app(DeploymentGuard::class)->isAllowed($developer))->toBeFalse() - ->and(app(DeploymentGuard::class)->denialReason($developer))->toContain('fejlesztői környezetben'); -}); - -test('a developer szerep nélküli felhasználó nem éri el', function () { - deploymentAllowEnvironment(); - - Role::create(['name' => 'admin', 'display_name' => 'Admin']); - $admin = User::factory()->create(); - $admin->addRole(Role::where('name', 'admin')->first()); - - FeatureFlag::create([ - 'name' => 'DeploymentPackage', - 'label' => 'Deployment csomagoló', - 'enabled' => true, - 'stages' => null, - 'roles' => null, - ]); - app(FeatureFlagRegistrar::class)->registerAll(); - - expect(app(DeploymentGuard::class)->isAllowed($admin))->toBeFalse(); -}); - -test('a kikapcsolt feature flag elrejti a felületet a fejlesztő elől is', function () { - deploymentAllowEnvironment(); - - $developer = deploymentDeveloper(flagEnabled: false); - - expect(app(DeploymentGuard::class)->isAllowed($developer))->toBeFalse() - ->and(DeploymentPackage::canAccess())->toBeFalse(); -}); - -test('minden feltétel teljesülése esetén engedélyezett', function () { - deploymentAllowEnvironment(); - - $developer = deploymentDeveloper(); - - expect(app(DeploymentGuard::class)->isAllowed($developer))->toBeTrue() - ->and(app(DeploymentGuard::class)->denialReason($developer))->toBeNull(); -}); - -test('a jogosulatlan felhasználó 403-at kap az oldal betöltésekor', function () { - deploymentAllowEnvironment(); - - $developer = deploymentDeveloper(flagEnabled: false); - - // Szándékosan valódi HTTP kérés: a Livewire::test() a HttpException-t válasszá - // alakítja (RequestBroker: withoutExceptionHandling([HttpException::class])), így - // ott nem a tényleges belépési pontot mérnénk. - $this->actingAs($developer) - ->get(DeploymentPackage::getUrl()) - ->assertForbidden(); -}); - -test('a commitlista a legfrissebbtől visszafelé, fájlszámmal együtt jön', function () { - $repository = deploymentTestRepository(); - - $commits = app(GitRepository::class)->commits('main', 10); - - expect($commits)->toHaveCount(3) - ->and($commits[0]['subject'])->toBe('harmadik commit') - ->and($commits[2]['subject'])->toBe('első commit') - ->and($commits[0]['hash'])->toHaveLength(40) - ->and($commits[0]['file_count'])->toBe(2) - // A törzs a tárgysor nélkül jön, és nem tapad hozzá a --shortstat sora sem. - ->and($commits[1]['body'])->toBe('A törzsben említett árlista részlet.') - ->and($commits[0]['body'])->toBe(''); - - deploymentCleanup($repository['path']); -}); - -test('a keresés a commit törzsében is talál, nem csak a tárgysorban', function () { - deploymentAllowEnvironment(); - $repository = deploymentTestRepository(); - - $this->actingAs(deploymentDeveloper()); - - Livewire::test(DeploymentPackage::class) - ->set('data.search', 'árlista részlet') - ->assertSee('második commit') - ->assertDontSee('harmadik commit'); - - deploymentCleanup($repository['path']); -}); - -test('a diff felismeri az új, módosult, törölt és átnevezett fájlokat', function () { - $repository = deploymentTestRepository(); - [$first, , $third] = $repository['commits']; - - $files = collect(app(GitRepository::class)->changedFiles($first, $third))->keyBy('path'); - - expect($files->get('app/Elso.php')['status'])->toBe('M') - ->and($files->get('app/Regi.php')['status'])->toBe('D') - ->and($files->get('app/Atnevezett.php')['status'])->toBe('R') - ->and($files->get('app/Atnevezett.php')['old_path'])->toBe('app/Atnevezendo.php') - // Ékezetes útvonal: core.quotePath=false nélkül "app/\303\201rlista.php" jönne vissza. - ->and($files->has('app/Árlista.php'))->toBeTrue(); - - deploymentCleanup($repository['path']); -}); - -test('az elemzés kizárja a tiltott fájlokat és külön kezeli a törlendőket', function () { - $repository = deploymentTestRepository(); - [$first, , $third] = $repository['commits']; - - $changedFiles = app(GitRepository::class)->changedFiles($first, $third); - $changeSet = app(ChangeSetAnalyzer::class)->analyze($changedFiles); - - expect($changeSet['copied'])->toContain('app/Árlista.php', 'app/Atnevezett.php', 'app/Elso.php') - ->and($changeSet['copied'])->not->toContain('.env') - ->and($changeSet['counts']['excluded'])->toBe(1) - // A törölt fájl mellett az átnevezés régi útvonala is törlendő a célgépen. - ->and($changeSet['deleted'])->toContain('app/Regi.php', 'app/Atnevezendo.php'); - - $titles = collect($changeSet['warnings'])->pluck('title')->implode(' | '); - - expect($titles)->toContain('Migráció') - ->and($titles)->toContain('Seeder') - ->and($titles)->toContain('Törlendő fájl'); - - deploymentCleanup($repository['path']); -}); - -test('a teendők kérik a queue worker és a scheduler újraindítását, a cache ürítés után', function () { - deploymentAllowEnvironment(); - $repository = deploymentTestRepository(); - [$first, , $third] = $repository['commits']; - - $package = app(DeploymentPackageBuilder::class)->build('d2d', 'test', $first, $third); - $teendok = File::get($package['path'].'/_TEENDOK.md'); - - expect($teendok)->toContain('docker service update --force d2d-environment_queue') - ->and($teendok)->toContain('docker service update --force d2d-environment_scheduler') - // A 0 replikás scheduler miatt tudni kell, hogy a restart üresjárat lehet. - ->and($teendok)->toContain('0 replikán fut'); - - // A restartnak a cache ürítés UTÁN kell jönnie: a frissen induló worker különben - // a régi, cache-elt konfigot töltené be. - expect(strpos($teendok, 'docker service update'))->toBeGreaterThan(strpos($teendok, 'config:clear')); - - deploymentCleanup($repository['path']); -}); - -test('a queue worker nélküli környezetnél is kiderül, mit kell majd újraindítani', function () { - deploymentAllowEnvironment(); - $repository = deploymentTestRepository(); - [$first, , $third] = $repository['commits']; - - $package = app(DeploymentPackageBuilder::class)->build('e2e', 'main', $first, $third); - $teendok = File::get($package['path'].'/_TEENDOK.md'); - - expect($teendok)->toContain('docker service update --force t2t_scheduler') - ->and($teendok)->toContain('t2t_queue'); - - deploymentCleanup($repository['path']); -}); - -test('a teendők között szerepel a seeder futtatása a konkrét osztálynévvel', function () { - deploymentAllowEnvironment(); - $repository = deploymentTestRepository(); - [$first, , $third] = $repository['commits']; - - $package = app(DeploymentPackageBuilder::class)->build('e2e', 'main', $first, $third); - $teendok = File::get($package['path'].'/_TEENDOK.md'); - - expect($teendok)->toContain('php artisan db:seed --class=TesztSeeder') - // A seeder puszta felmásolása nem futtatja le - ezt ki kell mondani. - ->and($teendok)->toContain('NEM futtatja le') - // A Pennant purge feltételes: Eloquent mentésnél a FeatureFlagObserver elvégzi. - ->and($teendok)->toContain('pennant:purge') - ->and($teendok)->toContain('cache:clear'); - - deploymentCleanup($repository['path']); -}); - -test('az érvénytelen commit azonosítót nem adja tovább a gitnek', function () { - $repository = deploymentTestRepository(); - - $git = app(GitRepository::class); - - expect($git->resolveCommit('../../etc/passwd'))->toBeNull() - ->and($git->resolveCommit('main; rm -rf /'))->toBeNull() - ->and($git->resolveCommit('HEAD'))->toBeNull() - ->and($git->resolveCommit($repository['commits'][0]))->toBe($repository['commits'][0]); - - deploymentCleanup($repository['path']); -}); - -test('a fordított tartomány felismerhető', function () { - $repository = deploymentTestRepository(); - [$first, , $third] = $repository['commits']; - - $git = app(GitRepository::class); - - expect($git->isAncestor($first, $third))->toBeTrue() - ->and($git->isAncestor($third, $first))->toBeFalse(); - - deploymentCleanup($repository['path']); -}); - -test('az oldal kilistázza a commitokat és a kijelölt tartomány fájljait', function () { - deploymentAllowEnvironment(); - $repository = deploymentTestRepository(); - [$first, , $third] = $repository['commits']; - - $this->actingAs(deploymentDeveloper()); - - Livewire::test(DeploymentPackage::class) - ->assertSuccessful() - ->assertSee('harmadik commit') - ->call('selectFrom', $first) - ->call('selectTo', $third) - ->assertSet('fromCommit', $first) - ->assertSet('toCommit', $third) - ->assertSee('app/Atnevezett.php') - ->assertSee('app/Regi.php'); - - deploymentCleanup($repository['path']); -}); - -test('a csomag a commitolt tartalmat viszi, nem a working tree állapotát', function () { - deploymentAllowEnvironment(); - $repository = deploymentTestRepository(); - [$first, , $third] = $repository['commits']; - - // A working tree szándékos elrontása a commit után: ez nem kerülhet a csomagba. - File::put($repository['path'].'/app/Árlista.php', "build('e2e', 'main', $first, $third); - - expect(File::get($package['path'].'/app/Árlista.php'))->not->toContain('NEM KERÜLHET') - // A .gitattributes eol=lf miatt a git archive LF-fel ír, Windowson is. - ->and(File::get($package['path'].'/app/Árlista.php'))->not->toContain("\r\n"); - - deploymentCleanup($repository['path']); -}); - -test('a csomagból kimarad a kizárt és a kézzel kivett fájl, a törlendők külön listára kerülnek', function () { - deploymentAllowEnvironment(); - $repository = deploymentTestRepository(); - [$first, , $third] = $repository['commits']; - - $package = app(DeploymentPackageBuilder::class)->build('e2e', 'main', $first, $third, ['app/Elso.php']); - - expect(File::exists($package['path'].'/app/Atnevezett.php'))->toBeTrue() - ->and(File::exists($package['path'].'/app/Elso.php'))->toBeFalse() - ->and(File::exists($package['path'].'/.env'))->toBeFalse() - ->and(File::exists($package['path'].'/app/Regi.php'))->toBeFalse() - ->and($package['counts']['skipped'])->toBe(1) - ->and($package['counts']['missing'])->toBe(0); - - $torlendo = File::get($package['path'].'/_TORLENDO.txt'); - - expect($torlendo)->toContain('app/Regi.php') - ->and($torlendo)->toContain('app/Atnevezendo.php'); - - deploymentCleanup($repository['path']); -}); - -test('a csomag rögzíti a kirakott verziót', function () { - deploymentAllowEnvironment(); - $repository = deploymentTestRepository(); - [$first, , $third] = $repository['commits']; - - $package = app(DeploymentPackageBuilder::class)->build('e2e', 'main', $first, $third); - - $version = json_decode(File::get($package['path'].'/public/deploy-version.json'), true); - - expect($version['commit'])->toBe($third) - ->and($version['target'])->toBe('e2e') - ->and($version['branch'])->toBe('main'); - - deploymentCleanup($repository['path']); -}); - -test('a limit fölötti fájlszámnál nem készül csomag', function () { - deploymentAllowEnvironment(); - $repository = deploymentTestRepository(); - [$first, , $third] = $repository['commits']; - - Config::set('deployment.max_files', 1); - - expect(fn () => app(DeploymentPackageBuilder::class)->build('e2e', 'main', $first, $third)) - ->toThrow(RuntimeException::class); - - expect(File::exists(config('deployment.output_path')))->toBeFalse(); - - deploymentCleanup($repository['path']); -}); - -test('a csomagoló szerveren akkor sem indul el, ha közvetlenül hívják', function () { - deploymentAllowEnvironment(); - $repository = deploymentTestRepository(); - [$first, , $third] = $repository['commits']; - - Config::set('app.stage', 'PROD'); - - expect(fn () => app(DeploymentPackageBuilder::class)->build('e2e', 'main', $first, $third)) - ->toThrow(RuntimeException::class); - - deploymentCleanup($repository['path']); -}); - -test('a felületről indított csomagolás létrehozza a mappát', function () { - deploymentAllowEnvironment(); - $repository = deploymentTestRepository(); - [$first, , $third] = $repository['commits']; - - $this->actingAs(deploymentDeveloper()); - - $component = Livewire::test(DeploymentPackage::class) - ->call('selectFrom', $first) - ->call('selectTo', $third) - ->call('toggleFile', 'app/Elso.php') - ->call('createPackage'); - - $package = $component->get('lastPackage'); - - // Alapértelmezett célkörnyezet a d2d: azt frissítjük sűrűbben. - expect($package)->not->toBeNull() - ->and($package['name'])->toStartWith('deploy_d2d_') - ->and(File::isDirectory($package['path']))->toBeTrue() - ->and($package['skipped'])->toBe(['app/Elso.php']); - - deploymentCleanup($repository['path']); -}); - -test('a csomagolás naplóbejegyzést és letölthető ZIP-et hagy maga után', function () { - deploymentAllowEnvironment(); - $repository = deploymentTestRepository(); - [$first, , $third] = $repository['commits']; - - $this->actingAs(deploymentDeveloper()); - - Livewire::test(DeploymentPackage::class) - ->call('selectFrom', $first) - ->call('selectTo', $third) - ->call('createPackage'); - - $record = DeploymentPackageRecord::query()->latest('id')->first(); - - expect($record)->not->toBeNull() - ->and($record->target)->toBe('d2d') - ->and($record->to_commit)->toBe($third) - ->and($record->deployed_at)->toBeNull() - ->and(File::exists($record->zip_path))->toBeTrue(); - - Livewire::test(DeploymentPackage::class) - ->call('downloadPackage', $record->id) - ->assertFileDownloaded(); - - deploymentCleanup($repository['path']); -}); - -test('a kirakottként megjelölt csomag adja a következő tartomány kezdetét', function () { - deploymentAllowEnvironment(); - $repository = deploymentTestRepository(); - [$first, $second, $third] = $repository['commits']; - - $this->actingAs(deploymentDeveloper()); - - // Az alapértelmezett célkörnyezetre készül, hogy a második oldalbetöltés is ezt töltse elő. - $record = DeploymentPackageRecord::create([ - 'name' => 'deploy_d2d_teszt', - 'target' => 'd2d', - 'branch' => 'main', - 'from_commit' => $first, - 'to_commit' => $second, - 'folder' => $repository['path'], - ]); - - Livewire::test(DeploymentPackage::class) - ->call('markDeployed', $record->id) - ->assertSet('fromCommit', $second); - - expect($record->refresh()->deployed_at)->not->toBeNull(); - - // Új oldalbetöltésnél is a kirakott commitról indul a tartomány. - Livewire::test(DeploymentPackage::class)->assertSet('fromCommit', $second); - - deploymentCleanup($repository['path']); -}); - -test('a kimeneti mappán kívüli ZIP útvonal nem tölthető le', function () { - deploymentAllowEnvironment(); - $repository = deploymentTestRepository(); - [$first, , $third] = $repository['commits']; - - $this->actingAs(deploymentDeveloper()); - - // Elrontott (vagy szándékosan átírt) rekord: a fájl létezik, de a csomagoló - // kimeneti mappáján kívül van. - $record = DeploymentPackageRecord::create([ - 'name' => 'deploy_e2e_hamis', - 'target' => 'e2e', - 'branch' => 'main', - 'from_commit' => $first, - 'to_commit' => $third, - 'folder' => $repository['path'], - 'zip_path' => base_path('composer.json'), - ]); - - Livewire::test(DeploymentPackage::class) - ->call('downloadPackage', $record->id) - ->assertNoFileDownloaded(); - - deploymentCleanup($repository['path']); -}); - -test('a törlő szkript dry runban fut és ellenőrzi az app gyökeret', function () { - deploymentAllowEnvironment(); - $repository = deploymentTestRepository(); - [$first, , $third] = $repository['commits']; - - $package = app(DeploymentPackageBuilder::class)->build('e2e', 'main', $first, $third); - $script = File::get($package['path'].'/_torles.sh'); - - expect($script)->toStartWith('#!/usr/bin/env bash') - ->and($script)->toContain('[[ -f artisan && -d app ]]') - ->and($script)->toContain('"app/Regi.php"') - // Windowson generáljuk, Linuxon fut: CRLF-fel a shebang sor törne el. - ->and($script)->not->toContain("\r\n"); - - deploymentCleanup($repository['path']); -}); - -test('az oldal alapból a d2d környezetet ajánlja és betöltéskor lekérdezi az élő állapotot', function () { - deploymentAllowEnvironment(); - $repository = deploymentTestRepository(); - [, , $third] = $repository['commits']; - - // A beforeEach-ben regisztrált catch-all stub minden URL-re illeszkedik, és a - // Http factory az ELSŐ találatot adja vissza - ezért itt új factory kell, különben - // az üres törzsű alapértelmezett válasz nyerne a konkrét stub helyett. - Http::swap(new Factory); - Http::fake([ - '*deploy-version.json' => Http::response(['target' => 'd2d', 'commit' => $third, 'branch' => 'main']), - ]); - - $this->actingAs(deploymentDeveloper()); - - $component = Livewire::test(DeploymentPackage::class); - - expect($component->get('data')['target'])->toBe('d2d') - // A lekérdezés gomb nélkül, magától megtörténik. - ->and($component->get('liveVersions')['d2d']['ok'])->toBeTrue() - ->and($component->get('liveVersions')['d2d']['data']['commit'])->toBe($third); - - deploymentCleanup($repository['path']); -}); - -test('az oldal nem fogad el kamu commit azonosítót', function () { - deploymentAllowEnvironment(); - $repository = deploymentTestRepository(); - - $this->actingAs(deploymentDeveloper()); - - Livewire::test(DeploymentPackage::class) - ->call('selectFrom', '../../etc/passwd') - ->assertSet('fromCommit', null); - - deploymentCleanup($repository['path']); -});