- a topbar menüpont a most bemásolt git ikont használja; a fájl ico_gitpng néven érkezett, átnevezve ico_git.png-re, hogy illeszkedjen az ico_*.png konvencióhoz, amiből a nav az útvonalat építi - a git log a törzset (%b) is elkéri, és a keresés a tárgysor mellett ebben is keres: a hash-re ritkán keres ember, az érdemi leírás viszont gyakran a törzsben van - a --shortstat sora a törzs után érkezik, ezért a parse leválasztja róla, különben a "N files changed" szöveg a törzsbe és a keresésbe is beszivárogna Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
308 lines
11 KiB
PHP
308 lines
11 KiB
PHP
<?php
|
|
|
|
use App\Filament\Pages\DeploymentPackage;
|
|
use App\Models\FeatureFlag;
|
|
use App\Models\Role;
|
|
use App\Models\User;
|
|
use App\Services\Deployment\ChangeSetAnalyzer;
|
|
use App\Services\Deployment\DeploymentGuard;
|
|
use App\Services\Deployment\GitRepository;
|
|
use App\Services\FeatureFlagRegistrar;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\Config;
|
|
use Illuminate\Support\Facades\File;
|
|
use Livewire\Livewire;
|
|
use Symfony\Component\HttpKernel\Exception\HttpException;
|
|
use Symfony\Component\Process\Process;
|
|
use Tests\TestCase;
|
|
|
|
uses(TestCase::class, RefreshDatabase::class);
|
|
|
|
/**
|
|
* Fejlesztői gépet szimuláló környezet: az .env kapcsoló és a stage whitelist rendben.
|
|
*/
|
|
function deploymentAllowEnvironment(): void
|
|
{
|
|
Config::set('deployment.enabled', true);
|
|
Config::set('app.stage', 'local');
|
|
}
|
|
|
|
function deploymentDeveloper(bool $flagEnabled = true): User
|
|
{
|
|
$role = Role::firstOrCreate(['name' => '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<int, string> $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<int, string>}
|
|
*/
|
|
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);
|
|
|
|
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', "<?php\n// elso\n");
|
|
File::put($path.'/app/Regi.php', "<?php\n// regi\n");
|
|
File::put($path.'/app/Atnevezendo.php', "<?php\n// atnevezendo\n");
|
|
deploymentGit($path, ['add', '-A']);
|
|
deploymentGit($path, ['commit', '-m', 'első commit']);
|
|
deploymentGit($path, ['branch', '-M', 'main']);
|
|
|
|
File::put($path.'/app/Elso.php', "<?php\n// elso modositva\n");
|
|
File::put($path.'/app/Árlista.php', "<?php\n// ekezetes\n");
|
|
File::put($path.'/database/migrations/2026_01_01_000000_teszt.php', "<?php\n// migracio\n");
|
|
File::put($path.'/.env', "APP_KEY=titok\n");
|
|
deploymentGit($path, ['add', '-A', '-f']);
|
|
deploymentGit($path, ['commit', '-m', "második commit\n\nA törzsben említett árlista részlet."]);
|
|
|
|
deploymentGit($path, ['mv', 'app/Atnevezendo.php', 'app/Atnevezett.php']);
|
|
deploymentGit($path, ['rm', 'app/Regi.php']);
|
|
deploymentGit($path, ['commit', '-m', 'harmadik commit']);
|
|
|
|
$log = trim(deploymentGit($path, ['log', '--reverse', '--pretty=format:%H']));
|
|
|
|
Config::set('deployment.repo_path', $path);
|
|
|
|
return ['path' => $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);
|
|
$this->actingAs($developer);
|
|
|
|
expect(fn () => Livewire::test(DeploymentPackage::class))->toThrow(HttpException::class);
|
|
});
|
|
|
|
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('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']);
|
|
});
|
|
|
|
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 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']);
|
|
});
|