Add comprehensive test coverage and update notes
This commit is contained in:
462
tests/Feature/SystemUpdateControllerBranchesTest.php
Normal file
462
tests/Feature/SystemUpdateControllerBranchesTest.php
Normal file
@@ -0,0 +1,462 @@
|
||||
<?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();
|
||||
|
||||
$artisanPath = base_path('artisan');
|
||||
$originalArtisan = file_get_contents($artisanPath);
|
||||
file_put_contents($artisanPath, "#!/usr/bin/env php\n<?php exit(1);\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->assertStatus(500);
|
||||
$response->assertJsonFragment(['message' => 'Migrations failed.']);
|
||||
} finally {
|
||||
file_put_contents($artisanPath, $originalArtisan);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
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');
|
||||
|
||||
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();
|
||||
|
||||
$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(['message' => 'Update finished.']);
|
||||
$response->assertJsonStructure(['used_rsync']);
|
||||
} finally {
|
||||
file_put_contents($artisanPath, $originalArtisan);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user