Add comprehensive test coverage and update notes
Some checks failed
CI/CD Pipeline / test (push) Successful in 3s
CI/CD Pipeline / deploy (push) Failing after 15s

This commit is contained in:
2026-02-08 19:04:12 +01:00
parent 160430e128
commit 88e4a70f88
43 changed files with 6114 additions and 520 deletions

View File

@@ -0,0 +1,225 @@
<?php
use App\Http\Controllers\SystemStatusController;
use App\Models\Role;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
function withFakeBinForStatus(array $scripts, callable $callback): void
{
$dir = storage_path('app/test-bin-' . Str::random(6));
if (!is_dir($dir)) {
mkdir($dir, 0777, true);
}
foreach ($scripts as $name => $body) {
$path = $dir . DIRECTORY_SEPARATOR . $name;
file_put_contents($path, $body);
chmod($path, 0755);
}
$originalPath = getenv('PATH') ?: '';
putenv("PATH={$dir}");
$_ENV['PATH'] = $dir;
$_SERVER['PATH'] = $dir;
try {
$callback($dir);
} finally {
putenv("PATH={$originalPath}");
$_ENV['PATH'] = $originalPath;
$_SERVER['PATH'] = $originalPath;
if (is_dir($dir)) {
$items = scandir($dir);
if (is_array($items)) {
foreach ($items as $item) {
if ($item === '.' || $item === '..') {
continue;
}
$path = $dir . DIRECTORY_SEPARATOR . $item;
if (is_file($path)) {
unlink($path);
}
}
}
rmdir($dir);
}
}
}
it('returns system status for admins', function (): void {
withFakeBinForStatus([
'php' => "#!/bin/sh\nif [ \"$1\" = \"-r\" ]; then echo \"8.4.0\"; exit 0; fi\necho \"php\"\n",
'composer' => "#!/bin/sh\necho \"composer 2.0.0\"\n",
'node' => "#!/bin/sh\necho \"v20.0.0\"\n",
'npm' => "#!/bin/sh\necho \"9.0.0\"\n",
'tar' => "#!/bin/sh\necho \"tar 1.2.3\"\n",
'rsync' => "#!/bin/sh\necho \"rsync 3.2.0\"\n",
], function (string $dir): void {
$admin = User::factory()->create();
$role = Role::firstOrCreate(['name' => 'ROLE_ADMIN'], ['color' => '#111111']);
$admin->roles()->attach($role);
$request = Request::create('/api/system/status', 'GET');
$request->setUserResolver(fn () => $admin);
$controller = new SystemStatusController();
$response = $controller->__invoke($request);
expect($response->getStatusCode())->toBe(200);
$payload = $response->getData(true);
expect($payload)->toHaveKeys([
'php',
'php_default',
'composer',
'composer_version',
'node',
'node_version',
'npm',
'npm_version',
'tar',
'tar_version',
'rsync',
'rsync_version',
'proc_functions',
'storage_writable',
'updates_writable',
]);
});
});
it('covers binary resolution edge cases', function (): void {
withFakeBinForStatus([
'sh' => "#!/bin/sh\nexit 0\n",
], function (string $dir): void {
$controller = new SystemStatusController();
$refBinary = new ReflectionMethod($controller, 'resolveBinary');
$refBinary->setAccessible(true);
expect($refBinary->invoke($controller, 'php'))->toBeNull();
});
});
it('returns php version when available', function (): void {
withFakeBinForStatus([
'phpfake' => "#!/bin/sh\nif [ \"$1\" = \"-r\" ]; then echo \"8.4.1\"; exit 0; fi\nexit 0\n",
], function (string $dir): void {
$controller = new SystemStatusController();
$refPhp = new ReflectionMethod($controller, 'resolvePhpVersion');
$refPhp->setAccessible(true);
$path = $dir . '/phpfake';
expect($refPhp->invoke($controller, $path))->toBe('8.4.1');
});
});
it('returns null php version when command fails', function (): void {
withFakeBinForStatus([
'phpfail' => "#!/bin/sh\nexit 1\n",
], function (string $dir): void {
$controller = new SystemStatusController();
$refPhp = new ReflectionMethod($controller, 'resolvePhpVersion');
$refPhp->setAccessible(true);
$path = $dir . '/phpfail';
expect($refPhp->invoke($controller, $path))->toBeNull();
});
});
it('returns binary version when regex matches', function (): void {
withFakeBinForStatus([
'tool' => "#!/bin/sh\necho \"tool v1.2.3\"\n",
], function (string $dir): void {
$controller = new SystemStatusController();
$refVer = new ReflectionMethod($controller, 'resolveBinaryVersion');
$refVer->setAccessible(true);
$path = $dir . '/tool';
expect($refVer->invoke($controller, $path, ['--version']))->toBe('1.2.3');
});
});
it('returns null when binary version output is empty', function (): void {
withFakeBinForStatus([
'empty' => "#!/bin/sh\nexit 0\n",
], function (string $dir): void {
$controller = new SystemStatusController();
$refVer = new ReflectionMethod($controller, 'resolveBinaryVersion');
$refVer->setAccessible(true);
$path = $dir . '/empty';
expect($refVer->invoke($controller, $path, ['--version']))->toBeNull();
});
});
it('returns null when binary version output has no version', function (): void {
withFakeBinForStatus([
'noversion' => "#!/bin/sh\necho \"tool version unknown\"\n",
], function (string $dir): void {
$controller = new SystemStatusController();
$refVer = new ReflectionMethod($controller, 'resolveBinaryVersion');
$refVer->setAccessible(true);
$path = $dir . '/noversion';
expect($refVer->invoke($controller, $path, ['--version']))->toBeNull();
});
});
it('returns null when binary version command fails', function (): void {
withFakeBinForStatus([
'fail' => "#!/bin/sh\nexit 1\n",
], function (string $dir): void {
$controller = new SystemStatusController();
$refVer = new ReflectionMethod($controller, 'resolveBinaryVersion');
$refVer->setAccessible(true);
$path = $dir . '/fail';
expect($refVer->invoke($controller, $path, ['--version']))->toBeNull();
});
});
it('returns empty array when readJson cannot read file', function (): void {
$controller = new SystemStatusController();
$ref = new ReflectionMethod($controller, 'readJson');
$ref->setAccessible(true);
$path = sys_get_temp_dir() . '/missing.json';
$result = $ref->invoke($controller, $path);
expect($result)->toBe([]);
});
it('returns empty array when readJson invalid', function (): void {
$controller = new SystemStatusController();
$ref = new ReflectionMethod($controller, 'readJson');
$ref->setAccessible(true);
$path = sys_get_temp_dir() . '/invalid.json';
file_put_contents($path, 'not-json');
$result = $ref->invoke($controller, $path);
unlink($path);
expect($result)->toBe([]);
});
it('returns empty array when readJson cannot read contents', function (): void {
$controller = new SystemStatusController();
$ref = new ReflectionMethod($controller, 'readJson');
$ref->setAccessible(true);
$path = storage_path('app/unreadable.json');
file_put_contents($path, '{"a":1}');
chmod($path, 0000);
$prev = set_error_handler(static fn () => true);
$result = $ref->invoke($controller, $path);
restore_error_handler();
chmod($path, 0644);
unlink($path);
expect($result)->toBe([]);
});