457 lines
16 KiB
PHP
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']);
|
|
});
|
|
});
|