mirror of
https://github.com/coollabsio/coolify.git
synced 2026-10-29 10:04:36 +08:00
Co-authored-by: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Co-authored-by: Aditya Tripathi <aditya@climactic.co> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
133 lines
5.3 KiB
PHP
133 lines
5.3 KiB
PHP
<?php
|
||
|
||
use App\Livewire\Server\DockerImages;
|
||
use App\Models\InstanceSettings;
|
||
use App\Models\PrivateKey;
|
||
use App\Models\Server;
|
||
use App\Models\Team;
|
||
use App\Models\User;
|
||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||
use Illuminate\Support\Facades\Process;
|
||
use Livewire\Livewire;
|
||
|
||
uses(RefreshDatabase::class);
|
||
|
||
beforeEach(function () {
|
||
$this->team = Team::factory()->create();
|
||
$this->user = User::factory()->create();
|
||
$this->team->members()->attach($this->user->id, ['role' => 'owner']);
|
||
$privateKey = PrivateKey::factory()->create(['team_id' => $this->team->id]);
|
||
$this->server = Server::factory()->create(['team_id' => $this->team->id, 'private_key_id' => $privateKey->id]);
|
||
$this->server->settings->update(['is_reachable' => true, 'is_usable' => true]);
|
||
$this->withoutVite();
|
||
$this->actingAs($this->user);
|
||
session(['currentTeam' => $this->team]);
|
||
InstanceSettings::unguarded(fn () => InstanceSettings::updateOrCreate(['id' => 0], []));
|
||
});
|
||
|
||
$nginxId = 'sha256:'.str_repeat('a', 64);
|
||
$danglingId = 'sha256:'.str_repeat('b', 64);
|
||
|
||
$fakeDocker = function (string $nginxId, string $danglingId) {
|
||
fakeRemoteProcesses([
|
||
'*docker image ls*' => Process::result(output: implode("\n", [
|
||
json_encode(['Repository' => 'nginx', 'Tag' => '1.27', 'ID' => $nginxId, 'Size' => '52.5MB', 'CreatedSince' => '3 weeks ago']),
|
||
json_encode(['Repository' => '<none>', 'Tag' => '<none>', 'ID' => $danglingId, 'Size' => '11MB', 'CreatedSince' => '2 days ago']),
|
||
])),
|
||
'*docker ps -a*' => Process::result(output: "{$nginxId}#/web\n"),
|
||
'*docker system df*' => Process::result(output: json_encode([
|
||
'Type' => 'Images', 'TotalCount' => '2', 'Active' => '1', 'Size' => '63.5MB', 'Reclaimable' => '11MB (17%)',
|
||
])),
|
||
'*docker image rm*' => Process::result(output: 'Deleted: '.$danglingId),
|
||
]);
|
||
};
|
||
|
||
it('renders the images page for a server', function () use ($fakeDocker, $nginxId, $danglingId) {
|
||
$fakeDocker($nginxId, $danglingId);
|
||
|
||
$this->get(route('server.docker-images', ['server_uuid' => $this->server->uuid]))
|
||
->assertOk()
|
||
->assertSee('Images');
|
||
});
|
||
|
||
it('lists images and marks the ones no container uses', function () use ($fakeDocker, $nginxId, $danglingId) {
|
||
$fakeDocker($nginxId, $danglingId);
|
||
|
||
Livewire::test(DockerImages::class, ['server_uuid' => $this->server->uuid])
|
||
->call('load')
|
||
->assertSet('loaded', true)
|
||
->assertSet('images.0.repository', 'nginx')
|
||
->assertSet('images.0.reference', 'nginx:1.27')
|
||
->assertSet('images.0.short_id', str_repeat('a', 12))
|
||
->assertSet('images.0.size', '52.5MB')
|
||
->assertSet('images.0.containers', ['web'])
|
||
->assertSet('images.1.reference', $danglingId)
|
||
->assertSet('images.1.dangling', true)
|
||
->assertSet('images.1.containers', [])
|
||
->assertSet('usage.size', '63.5MB')
|
||
->assertSet('usage.reclaimable', '11MB (17%)')
|
||
->assertSee('Unused')
|
||
->assertSee('web');
|
||
});
|
||
|
||
it('deletes an unused image', function () use ($fakeDocker, $nginxId, $danglingId) {
|
||
$fakeDocker($nginxId, $danglingId);
|
||
|
||
Livewire::test(DockerImages::class, ['server_uuid' => $this->server->uuid])
|
||
->call('load')
|
||
->call('delete', $danglingId)
|
||
->assertDispatched('success');
|
||
|
||
Process::assertRan(fn ($process) => str_contains(remoteProcessScript($process), "docker image rm '{$danglingId}'"));
|
||
});
|
||
|
||
it('refuses to delete an image that a container is using', function () use ($fakeDocker, $nginxId, $danglingId) {
|
||
$fakeDocker($nginxId, $danglingId);
|
||
|
||
Livewire::test(DockerImages::class, ['server_uuid' => $this->server->uuid])
|
||
->call('load')
|
||
->call('delete', 'nginx:1.27')
|
||
->assertDispatched('error');
|
||
|
||
Process::assertNotRan(fn ($process) => str_contains(remoteProcessScript($process), 'docker image rm'));
|
||
});
|
||
|
||
it('filters images by the search term', function () use ($fakeDocker, $nginxId, $danglingId) {
|
||
$fakeDocker($nginxId, $danglingId);
|
||
|
||
Livewire::test(DockerImages::class, ['server_uuid' => $this->server->uuid])
|
||
->call('load')
|
||
->set('search', 'nginx')
|
||
->assertSee('1.27')
|
||
->assertDontSee(str_repeat('b', 12));
|
||
});
|
||
|
||
it('paginates images and returns to the first page when the search changes', function () {
|
||
fakeRemoteProcesses([
|
||
'*docker image ls*' => Process::result(output: collect(range(1, 12))
|
||
->map(fn (int $i) => json_encode([
|
||
'Repository' => sprintf('app-%02d', $i), 'Tag' => 'latest', 'ID' => 'sha256:'.str_repeat(dechex($i), 64),
|
||
'Size' => '1MB', 'CreatedSince' => '1 day ago',
|
||
]))
|
||
->implode("\n")),
|
||
'*docker ps -a*' => Process::result(output: ''),
|
||
'*docker system df*' => Process::result(output: ''),
|
||
]);
|
||
|
||
Livewire::test(DockerImages::class, ['server_uuid' => $this->server->uuid])
|
||
->call('load')
|
||
->assertSee('app-10')
|
||
->assertDontSee('app-11')
|
||
->assertSee('1–10 of 12')
|
||
->call('nextPage')
|
||
->assertSee('app-12')
|
||
->assertDontSee('app-01')
|
||
->assertSee('11–12 of 12')
|
||
->set('search', 'app-0')
|
||
->assertSee('app-01')
|
||
->assertSee('1–9 of 9')
|
||
->set('perPage', 5)
|
||
->assertSee('1–5 of 9')
|
||
->assertDontSee('app-06');
|
||
});
|