252 lines
9.0 KiB
PHP
252 lines
9.0 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use App\Livewire\Docker\Index;
|
|
use App\Livewire\Modals\ContainerLogs;
|
|
use App\Models\HostCredential;
|
|
use App\Models\Server;
|
|
use App\Models\User;
|
|
use App\Services\DockerService;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Livewire\Livewire;
|
|
use Mockery;
|
|
use Tests\TestCase;
|
|
|
|
class DockerComponentTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
protected function tearDown(): void
|
|
{
|
|
Mockery::close();
|
|
parent::tearDown();
|
|
}
|
|
|
|
private function admin(): User
|
|
{
|
|
return User::factory()->create(['must_change_password' => false]);
|
|
}
|
|
|
|
private function operator(): User
|
|
{
|
|
return User::factory()->operator()->create(['must_change_password' => false]);
|
|
}
|
|
|
|
private function viewer(): User
|
|
{
|
|
return User::factory()->viewer()->create(['must_change_password' => false]);
|
|
}
|
|
|
|
private function activeServer(): Server
|
|
{
|
|
$server = Server::create(['name' => 'box', 'ip' => '10.0.0.2', 'ssh_port' => 22, 'status' => 'online']);
|
|
$server->credential()->create(['username' => 'root', 'auth_type' => 'password', 'secret' => 'x']);
|
|
session(['active_server_id' => $server->id]);
|
|
|
|
return $server;
|
|
}
|
|
|
|
private function stubDocker(array $containers = []): DockerService
|
|
{
|
|
$docker = Mockery::mock(DockerService::class);
|
|
$docker->shouldReceive('available')->andReturn(true);
|
|
$docker->shouldReceive('containers')->andReturn($containers);
|
|
app()->instance(DockerService::class, $docker);
|
|
|
|
return $docker;
|
|
}
|
|
|
|
private function hostCred(): HostCredential
|
|
{
|
|
return HostCredential::create([
|
|
'host' => 'host.docker.internal', 'port' => 22,
|
|
'username' => 'root', 'auth_type' => 'password', 'secret' => 'x',
|
|
]);
|
|
}
|
|
|
|
public function test_defaults_to_the_clusev_host_when_a_host_login_is_configured(): void
|
|
{
|
|
// The host is where the operator's own containers live, so it is the default target.
|
|
$this->actingAs($this->viewer());
|
|
$this->hostCred();
|
|
$this->activeServer(); // a fleet server also exists, but the host wins the default
|
|
$this->stubDocker([['id' => 'h1', 'name' => 'clusev-app-1', 'image' => 'clusev-app:prod', 'state' => 'running', 'status' => 'Up', 'ports' => '']]);
|
|
|
|
Livewire::test(Index::class)
|
|
->call('load')
|
|
->assertOk()
|
|
->assertSet('connected', true)
|
|
->assertSee('clusev-app-1')
|
|
->assertSee(__('docker.host_target'));
|
|
}
|
|
|
|
public function test_switching_to_host_without_a_login_shows_the_unconfigured_state(): void
|
|
{
|
|
$this->actingAs($this->viewer());
|
|
$this->activeServer();
|
|
$this->stubDocker();
|
|
|
|
Livewire::test(Index::class)
|
|
->call('setTarget', 'host')
|
|
->assertOk()
|
|
->assertSet('hostNotConfigured', true)
|
|
->assertSet('connected', false)
|
|
->assertSee(__('docker.host_unconfigured_title'));
|
|
}
|
|
|
|
public function test_can_switch_from_the_host_to_the_active_fleet_server(): void
|
|
{
|
|
$this->actingAs($this->viewer());
|
|
$this->hostCred();
|
|
$this->activeServer();
|
|
$this->stubDocker([['id' => 's1', 'name' => 'web', 'image' => 'nginx', 'state' => 'running', 'status' => 'Up', 'ports' => '80/tcp']]);
|
|
|
|
Livewire::test(Index::class)
|
|
->call('setTarget', 'server')
|
|
->assertOk()
|
|
->assertSet('connected', true)
|
|
->assertSee('web');
|
|
}
|
|
|
|
public function test_operator_can_action_a_host_container_and_it_is_audited(): void
|
|
{
|
|
$this->actingAs($this->operator());
|
|
$this->hostCred(); // default target = host
|
|
$docker = $this->stubDocker();
|
|
$docker->shouldReceive('containerAction')->once()
|
|
->with(Mockery::type(Server::class), 'clusev-redis-1', 'restart')
|
|
->andReturn(['ok' => true, 'output' => '']);
|
|
|
|
Livewire::test(Index::class)
|
|
->call('action', 'clusev-redis-1', 'restart')
|
|
->assertOk();
|
|
|
|
// Host actions carry a null server_id (transient host server); the target label names it.
|
|
$this->assertDatabaseHas('audit_events', ['action' => 'docker.action', 'server_id' => null]);
|
|
}
|
|
|
|
public function test_logs_modal_loads_a_host_container_tail(): void
|
|
{
|
|
$this->actingAs($this->admin());
|
|
$this->hostCred();
|
|
$docker = Mockery::mock(DockerService::class);
|
|
$docker->shouldReceive('logs')->once()->with(Mockery::type(Server::class), 'clusev-app-1', 200)->andReturn('host log line');
|
|
app()->instance(DockerService::class, $docker);
|
|
|
|
Livewire::test(ContainerLogs::class, ['serverId' => 0, 'ref' => 'clusev-app-1'])
|
|
->call('load')
|
|
->assertOk()
|
|
->assertSet('logs', 'host log line');
|
|
}
|
|
|
|
public function test_viewer_can_browse_the_container_list(): void
|
|
{
|
|
$this->actingAs($this->viewer());
|
|
$this->activeServer();
|
|
$this->stubDocker([['id' => 'abc', 'name' => 'web', 'image' => 'nginx', 'state' => 'running', 'status' => 'Up', 'ports' => '80/tcp']]);
|
|
|
|
Livewire::test(Index::class)
|
|
->call('load')
|
|
->assertOk()
|
|
->assertSet('connected', true)
|
|
->assertSee('web');
|
|
}
|
|
|
|
public function test_shows_a_clean_not_installed_state_when_docker_is_absent(): void
|
|
{
|
|
// A host without a container runtime (e.g. a native postfix/dovecot mail server) must NOT
|
|
// render the raw "sh: 1: docker: not found" shell error — that reads as a broken feature.
|
|
// available() is probed first; false => a clean, honest "not installed" panel.
|
|
$this->actingAs($this->viewer());
|
|
$this->activeServer();
|
|
$docker = Mockery::mock(DockerService::class);
|
|
$docker->shouldReceive('available')->once()->andReturn(false);
|
|
$docker->shouldReceive('containers')->never(); // never even attempt ps when docker is absent
|
|
app()->instance(DockerService::class, $docker);
|
|
|
|
Livewire::test(Index::class)
|
|
->call('load')
|
|
->assertOk()
|
|
->assertSet('connected', false)
|
|
->assertSet('notInstalled', true)
|
|
->assertSet('error', null)
|
|
->assertSee(__('docker.not_installed_title'))
|
|
->assertDontSee('not found');
|
|
}
|
|
|
|
public function test_viewer_cannot_run_a_container_action(): void
|
|
{
|
|
$this->actingAs($this->viewer());
|
|
$this->activeServer();
|
|
$docker = $this->stubDocker();
|
|
$docker->shouldReceive('containerAction')->never(); // guard fires first
|
|
|
|
Livewire::test(Index::class)
|
|
->call('action', 'web', 'stop')
|
|
->assertForbidden();
|
|
}
|
|
|
|
public function test_operator_can_run_a_container_action_and_it_is_audited(): void
|
|
{
|
|
$this->actingAs($this->operator());
|
|
$server = $this->activeServer();
|
|
$docker = $this->stubDocker();
|
|
$docker->shouldReceive('containerAction')->once()
|
|
->with(Mockery::type(Server::class), 'web', 'restart')
|
|
->andReturn(['ok' => true, 'output' => 'web']);
|
|
|
|
Livewire::test(Index::class)
|
|
->call('action', 'web', 'restart')
|
|
->assertOk();
|
|
|
|
$this->assertDatabaseHas('audit_events', ['action' => 'docker.action', 'server_id' => $server->id]);
|
|
}
|
|
|
|
public function test_admin_can_run_a_container_action(): void
|
|
{
|
|
$this->actingAs($this->admin());
|
|
$this->activeServer();
|
|
$docker = $this->stubDocker();
|
|
$docker->shouldReceive('containerAction')->once()->andReturn(['ok' => true, 'output' => '']);
|
|
|
|
Livewire::test(Index::class)->call('action', 'db', 'start')->assertOk();
|
|
}
|
|
|
|
public function test_viewer_cannot_open_logs(): void
|
|
{
|
|
$this->actingAs($this->viewer());
|
|
$this->activeServer();
|
|
$this->stubDocker();
|
|
|
|
Livewire::test(Index::class)->call('viewLogs', 'web')->assertForbidden();
|
|
}
|
|
|
|
public function test_viewer_cannot_read_logs_in_the_modal(): void
|
|
{
|
|
$this->actingAs($this->viewer());
|
|
$server = $this->activeServer();
|
|
$docker = Mockery::mock(DockerService::class);
|
|
$docker->shouldReceive('logs')->never(); // guard fires before the read
|
|
app()->instance(DockerService::class, $docker);
|
|
|
|
Livewire::test(ContainerLogs::class, ['serverId' => $server->id, 'ref' => 'web'])
|
|
->call('load')
|
|
->assertForbidden();
|
|
}
|
|
|
|
public function test_logs_modal_loads_a_container_tail(): void
|
|
{
|
|
$this->actingAs($this->admin());
|
|
$server = $this->activeServer();
|
|
$docker = Mockery::mock(DockerService::class);
|
|
$docker->shouldReceive('logs')->once()->with(Mockery::type(Server::class), 'web', 200)->andReturn('log line 1');
|
|
app()->instance(DockerService::class, $docker);
|
|
|
|
Livewire::test(ContainerLogs::class, ['serverId' => $server->id, 'ref' => 'web'])
|
|
->call('load')
|
|
->assertOk()
|
|
->assertSet('logs', 'log line 1');
|
|
}
|
|
}
|