'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(); } /** * Determinisztikus teszt-repó: átnevezés, törlés, ékezetes fájlnév és migráció is van benne. * * @return array{path:string, commits:array} */ function deploymentTestRepository(): array { $path = sys_get_temp_dir().DIRECTORY_SEPARATOR.'deployment-package-test-'.uniqid(); 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)]; } function deploymentCleanup(string $path): void { // A .git/objects tartalma Windowson csak olvasható, ezért törlés előtt fel kell oldani. foreach (File::allFiles($path, true) as $file) { @chmod($file->getPathname(), 0777); } try { File::deleteDirectory($path); } catch (Throwable) { // A takarítás nem futtathat el tesztet - a temp mappa maradványa nem befolyásol semmit. } } 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']); });