Files
speedBB/tests/Feature/SystemUpdateControllerBranchesTest.php
tracer bf278667bc
All checks were successful
CI/CD Pipeline / test (push) Successful in 3s
CI/CD Pipeline / deploy (push) Successful in 23s
Add git_update.sh and adjust update/test hooks
2026-02-10 18:38:51 +01:00

457 lines
16 KiB
PHP

<?php
use App\Models\Role;
use App\Models\User;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
use Laravel\Sanctum\Sanctum;
function makeAdminForSystemUpdate(): User
{
$admin = User::factory()->create();
$role = Role::firstOrCreate(['name' => 'ROLE_ADMIN'], ['color' => '#111111']);
$admin->roles()->attach($role);
return $admin;
}
function withFakeBin(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();
} 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('uses token auth header and tarball template', function (): void {
putenv('GITEA_OWNER=acme');
putenv('GITEA_REPO=speedbb');
putenv('GITEA_API_BASE=https://git.example.test/api/v1');
putenv('GITEA_TGZ_URL_TEMPLATE=https://git.example.test/tarball/{{TAG}}-{{VERSION}}.tgz');
putenv('GITEA_TOKEN=secrettoken');
$tarballUrl = 'https://git.example.test/tarball/v1.2.3-1.2.3.tgz';
Http::fake([
'https://git.example.test/api/v1/repos/acme/speedbb/releases/latest' => Http::response([
'tag_name' => 'v1.2.3',
'tarball_url' => '',
], 200),
$tarballUrl => Http::response('archive-bytes', 200),
]);
File::shouldReceive('ensureDirectoryExists')->andReturnTrue();
File::shouldReceive('put')->andReturnTrue();
File::shouldReceive('directories')->andReturn(['/tmp/extract/speedbb']);
File::shouldReceive('copyDirectory')->andReturnTrue();
$artisanPath = base_path('artisan');
$originalArtisan = file_get_contents($artisanPath);
file_put_contents($artisanPath, "#!/usr/bin/env php\n<?php exit(0);\n");
chmod($artisanPath, 0755);
withFakeBin([
'tar' => "#!/bin/sh\nexit 0\n",
'composer' => "#!/bin/sh\nexit 0\n",
'npm' => "#!/bin/sh\nexit 0\n",
], function () use ($artisanPath, $originalArtisan): void {
try {
Sanctum::actingAs(makeAdminForSystemUpdate());
$response = $this->postJson('/api/system/update');
$response->assertOk();
$response->assertJsonFragment(['tag' => 'v1.2.3']);
} finally {
file_put_contents($artisanPath, $originalArtisan);
}
});
Http::assertSent(function ($request) use ($tarballUrl) {
if ($request->url() === $tarballUrl) {
return true;
}
return $request->hasHeader('Authorization', 'token secrettoken');
});
});
it('returns update failed on unexpected exception', function (): void {
putenv('GITEA_OWNER=acme');
putenv('GITEA_REPO=speedbb');
putenv('GITEA_API_BASE=https://git.example.test/api/v1');
Http::fake(function () {
throw new RuntimeException('boom');
});
Sanctum::actingAs(makeAdminForSystemUpdate());
$response = $this->postJson('/api/system/update');
$response->assertStatus(500);
$response->assertJsonFragment(['message' => 'Update failed.']);
});
it('handles release check failures', function (): void {
putenv('GITEA_OWNER=acme');
putenv('GITEA_REPO=speedbb');
putenv('GITEA_API_BASE=https://git.example.test/api/v1');
Http::fake([
'https://git.example.test/api/v1/repos/acme/speedbb/releases/latest' => Http::response([], 500),
]);
Sanctum::actingAs(makeAdminForSystemUpdate());
$response = $this->postJson('/api/system/update');
$response->assertStatus(500);
$response->assertJsonFragment(['message' => 'Release check failed: 500']);
});
it('handles missing tag in release response', function (): void {
putenv('GITEA_OWNER=acme');
putenv('GITEA_REPO=speedbb');
putenv('GITEA_API_BASE=https://git.example.test/api/v1');
Http::fake([
'https://git.example.test/api/v1/repos/acme/speedbb/releases/latest' => Http::response([
'tag_name' => '',
], 200),
]);
Sanctum::actingAs(makeAdminForSystemUpdate());
$response = $this->postJson('/api/system/update');
$response->assertStatus(500);
$response->assertJsonFragment(['message' => 'Release tag not found.']);
});
it('handles missing tarball url', function (): void {
putenv('GITEA_OWNER=acme');
putenv('GITEA_REPO=speedbb');
putenv('GITEA_API_BASE=https://git.example.test/api/v1');
putenv('GITEA_TGZ_URL_TEMPLATE=');
Http::fake([
'https://git.example.test/api/v1/repos/acme/speedbb/releases/latest' => Http::response([
'tag_name' => 'v1.2.3',
'tarball_url' => '',
], 200),
]);
Sanctum::actingAs(makeAdminForSystemUpdate());
$response = $this->postJson('/api/system/update');
$response->assertStatus(500);
$response->assertJsonFragment(['message' => 'No tarball URL available.']);
});
it('handles tarball download failure', function (): void {
putenv('GITEA_OWNER=acme');
putenv('GITEA_REPO=speedbb');
putenv('GITEA_API_BASE=https://git.example.test/api/v1');
Http::fake([
'https://git.example.test/api/v1/repos/acme/speedbb/releases/latest' => Http::response([
'tag_name' => 'v1.2.3',
'tarball_url' => 'https://git.example.test/archive.tgz',
], 200),
'https://git.example.test/archive.tgz' => Http::response('fail', 500),
]);
File::shouldReceive('ensureDirectoryExists')->andReturnTrue();
Sanctum::actingAs(makeAdminForSystemUpdate());
$response = $this->postJson('/api/system/update');
$response->assertStatus(500);
$response->assertJsonFragment(['message' => 'Download failed: 500']);
});
it('handles extract failure', function (): void {
putenv('GITEA_OWNER=acme');
putenv('GITEA_REPO=speedbb');
putenv('GITEA_API_BASE=https://git.example.test/api/v1');
Http::fake([
'https://git.example.test/api/v1/repos/acme/speedbb/releases/latest' => Http::response([
'tag_name' => 'v1.2.3',
'tarball_url' => 'https://git.example.test/archive.tgz',
], 200),
'https://git.example.test/archive.tgz' => Http::response('archive-bytes', 200),
]);
File::shouldReceive('ensureDirectoryExists')->andReturnTrue();
File::shouldReceive('put')->andReturnTrue();
withFakeBin([
'tar' => "#!/bin/sh\nexit 1\n",
'composer' => "#!/bin/sh\nexit 0\n",
'npm' => "#!/bin/sh\nexit 0\n",
], function (): void {
Sanctum::actingAs(makeAdminForSystemUpdate());
$response = $this->postJson('/api/system/update');
$response->assertStatus(500);
$response->assertJsonFragment(['message' => 'Failed to extract archive.']);
});
});
it('handles missing extracted folder', function (): void {
putenv('GITEA_OWNER=acme');
putenv('GITEA_REPO=speedbb');
putenv('GITEA_API_BASE=https://git.example.test/api/v1');
Http::fake([
'https://git.example.test/api/v1/repos/acme/speedbb/releases/latest' => Http::response([
'tag_name' => 'v1.2.3',
'tarball_url' => 'https://git.example.test/archive.tgz',
], 200),
'https://git.example.test/archive.tgz' => Http::response('archive-bytes', 200),
]);
File::shouldReceive('ensureDirectoryExists')->andReturnTrue();
File::shouldReceive('put')->andReturnTrue();
File::shouldReceive('directories')->andReturn([]);
withFakeBin([
'tar' => "#!/bin/sh\nexit 0\n",
'composer' => "#!/bin/sh\nexit 0\n",
'npm' => "#!/bin/sh\nexit 0\n",
], function (): void {
Sanctum::actingAs(makeAdminForSystemUpdate());
$response = $this->postJson('/api/system/update');
$response->assertStatus(500);
$response->assertJsonFragment(['message' => 'No extracted folder found.']);
});
});
it('handles rsync failure when available', function (): void {
putenv('GITEA_OWNER=acme');
putenv('GITEA_REPO=speedbb');
putenv('GITEA_API_BASE=https://git.example.test/api/v1');
Http::fake([
'https://git.example.test/api/v1/repos/acme/speedbb/releases/latest' => Http::response([
'tag_name' => 'v1.2.3',
'tarball_url' => 'https://git.example.test/archive.tgz',
], 200),
'https://git.example.test/archive.tgz' => Http::response('archive-bytes', 200),
]);
File::shouldReceive('ensureDirectoryExists')->andReturnTrue();
File::shouldReceive('put')->andReturnTrue();
File::shouldReceive('directories')->andReturn(['/tmp/extract/speedbb']);
withFakeBin([
'tar' => "#!/bin/sh\nexit 0\n",
'rsync' => "#!/bin/sh\nexit 1\n",
'composer' => "#!/bin/sh\nexit 0\n",
'npm' => "#!/bin/sh\nexit 0\n",
], function (): void {
Sanctum::actingAs(makeAdminForSystemUpdate());
$response = $this->postJson('/api/system/update');
$response->assertStatus(500);
$response->assertJsonFragment(['message' => 'rsync failed.']);
});
});
it('handles composer install failure after copyDirectory', function (): void {
putenv('GITEA_OWNER=acme');
putenv('GITEA_REPO=speedbb');
putenv('GITEA_API_BASE=https://git.example.test/api/v1');
Http::fake([
'https://git.example.test/api/v1/repos/acme/speedbb/releases/latest' => Http::response([
'tag_name' => 'v1.2.3',
'tarball_url' => 'https://git.example.test/archive.tgz',
], 200),
'https://git.example.test/archive.tgz' => Http::response('archive-bytes', 200),
]);
File::shouldReceive('ensureDirectoryExists')->andReturnTrue();
File::shouldReceive('put')->andReturnTrue();
File::shouldReceive('directories')->andReturn(['/tmp/extract/speedbb']);
File::shouldReceive('copyDirectory')->andReturnTrue();
withFakeBin([
'tar' => "#!/bin/sh\nexit 0\n",
'composer' => "#!/bin/sh\nexit 1\n",
'npm' => "#!/bin/sh\nexit 0\n",
], function (): void {
Sanctum::actingAs(makeAdminForSystemUpdate());
$response = $this->postJson('/api/system/update');
$response->assertStatus(500);
$response->assertJsonFragment(['message' => 'Composer install failed.']);
});
});
it('handles npm install failure', function (): void {
putenv('GITEA_OWNER=acme');
putenv('GITEA_REPO=speedbb');
putenv('GITEA_API_BASE=https://git.example.test/api/v1');
Http::fake([
'https://git.example.test/api/v1/repos/acme/speedbb/releases/latest' => Http::response([
'tag_name' => 'v1.2.3',
'tarball_url' => 'https://git.example.test/archive.tgz',
], 200),
'https://git.example.test/archive.tgz' => Http::response('archive-bytes', 200),
]);
File::shouldReceive('ensureDirectoryExists')->andReturnTrue();
File::shouldReceive('put')->andReturnTrue();
File::shouldReceive('directories')->andReturn(['/tmp/extract/speedbb']);
File::shouldReceive('copyDirectory')->andReturnTrue();
withFakeBin([
'tar' => "#!/bin/sh\nexit 0\n",
'composer' => "#!/bin/sh\nexit 0\n",
'npm' => "#!/bin/sh\nif [ \"$1\" = \"install\" ]; then exit 1; fi\nexit 0\n",
], function (): void {
Sanctum::actingAs(makeAdminForSystemUpdate());
$response = $this->postJson('/api/system/update');
$response->assertStatus(500);
$response->assertJsonFragment(['message' => 'npm install failed.']);
});
});
it('handles npm build failure', function (): void {
putenv('GITEA_OWNER=acme');
putenv('GITEA_REPO=speedbb');
putenv('GITEA_API_BASE=https://git.example.test/api/v1');
Http::fake([
'https://git.example.test/api/v1/repos/acme/speedbb/releases/latest' => Http::response([
'tag_name' => 'v1.2.3',
'tarball_url' => 'https://git.example.test/archive.tgz',
], 200),
'https://git.example.test/archive.tgz' => Http::response('archive-bytes', 200),
]);
File::shouldReceive('ensureDirectoryExists')->andReturnTrue();
File::shouldReceive('put')->andReturnTrue();
File::shouldReceive('directories')->andReturn(['/tmp/extract/speedbb']);
File::shouldReceive('copyDirectory')->andReturnTrue();
withFakeBin([
'tar' => "#!/bin/sh\nexit 0\n",
'composer' => "#!/bin/sh\nexit 0\n",
'npm' => "#!/bin/sh\nif [ \"$1\" = \"run\" ] && [ \"$2\" = \"build\" ]; then exit 1; fi\nexit 0\n",
], function (): void {
Sanctum::actingAs(makeAdminForSystemUpdate());
$response = $this->postJson('/api/system/update');
$response->assertStatus(500);
$response->assertJsonFragment(['message' => 'npm run build failed.']);
});
});
it('handles migration failure', function (): void {
putenv('GITEA_OWNER=acme');
putenv('GITEA_REPO=speedbb');
putenv('GITEA_API_BASE=https://git.example.test/api/v1');
Http::fake([
'https://git.example.test/api/v1/repos/acme/speedbb/releases/latest' => Http::response([
'tag_name' => 'v1.2.3',
'tarball_url' => 'https://git.example.test/archive.tgz',
], 200),
'https://git.example.test/archive.tgz' => Http::response('archive-bytes', 200),
]);
File::shouldReceive('ensureDirectoryExists')->andReturnTrue();
File::shouldReceive('put')->andReturnTrue();
File::shouldReceive('directories')->andReturn(['/tmp/extract/speedbb']);
File::shouldReceive('copyDirectory')->andReturnTrue();
putenv('SYSTEM_UPDATE_PHP_BINARY=/nope');
$_ENV['SYSTEM_UPDATE_PHP_BINARY'] = '/nope';
$_SERVER['SYSTEM_UPDATE_PHP_BINARY'] = '/nope';
withFakeBin([
'tar' => "#!/bin/sh\nexit 0\n",
'composer' => "#!/bin/sh\nexit 0\n",
'npm' => "#!/bin/sh\nexit 0\n",
], function (): void {
Sanctum::actingAs(makeAdminForSystemUpdate());
$response = $this->postJson('/api/system/update');
$response->assertStatus(500);
$response->assertJsonFragment(['message' => 'Migrations failed.']);
});
});
it('handles fallback copyDirectory update success', function (): void {
putenv('GITEA_OWNER=acme');
putenv('GITEA_REPO=speedbb');
putenv('GITEA_API_BASE=https://git.example.test/api/v1');
putenv('SYSTEM_UPDATE_PHP_BINARY=php');
$_ENV['SYSTEM_UPDATE_PHP_BINARY'] = 'php';
$_SERVER['SYSTEM_UPDATE_PHP_BINARY'] = 'php';
Http::fake([
'https://git.example.test/api/v1/repos/acme/speedbb/releases/latest' => Http::response([
'tag_name' => 'v1.2.3',
'tarball_url' => 'https://git.example.test/archive.tgz',
], 200),
'https://git.example.test/archive.tgz' => Http::response('archive-bytes', 200),
]);
File::shouldReceive('ensureDirectoryExists')->andReturnTrue();
File::shouldReceive('put')->andReturnTrue();
File::shouldReceive('directories')->andReturn(['/tmp/extract/speedbb']);
File::shouldReceive('copyDirectory')->andReturnTrue();
putenv('SYSTEM_UPDATE_PHP_BINARY=php');
$_ENV['SYSTEM_UPDATE_PHP_BINARY'] = 'php';
$_SERVER['SYSTEM_UPDATE_PHP_BINARY'] = 'php';
withFakeBin([
'tar' => "#!/bin/sh\nexit 0\n",
'composer' => "#!/bin/sh\nexit 0\n",
'npm' => "#!/bin/sh\nexit 0\n",
'php' => "#!/bin/sh\nexit 0\n",
], function (): void {
Sanctum::actingAs(makeAdminForSystemUpdate());
$response = $this->postJson('/api/system/update');
$response->assertOk();
$response->assertJsonFragment(['message' => 'Update finished.']);
$response->assertJsonStructure(['used_rsync']);
});
});