92 lines
3.6 KiB
PHP
92 lines
3.6 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use App\Livewire\Services\Index as Services;
|
|
use App\Models\AuditEvent;
|
|
use App\Models\Server;
|
|
use App\Models\User;
|
|
use App\Services\FleetService;
|
|
use App\Support\Confirm\ConfirmToken;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Livewire\Livewire;
|
|
use Mockery;
|
|
use Tests\TestCase;
|
|
|
|
/**
|
|
* A systemd action that FAILS must leave a persistent, readable trail: the ephemeral toast is
|
|
* truncated, so the full systemctl reason is written to the audit log (a red service.<op>.failed
|
|
* entry with the complete output in meta) where the operator can re-read it.
|
|
*/
|
|
class ServiceActionAuditTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
protected function tearDown(): void
|
|
{
|
|
Mockery::close();
|
|
parent::tearDown();
|
|
}
|
|
|
|
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 confirmed(string $event, array $params, int $serverId): string
|
|
{
|
|
$token = ConfirmToken::issue($event, $params, '', null, $serverId);
|
|
ConfirmToken::confirm($token);
|
|
|
|
return $token;
|
|
}
|
|
|
|
public function test_failed_start_is_persisted_to_the_audit_log_with_the_full_reason(): void
|
|
{
|
|
$this->actingAs(User::factory()->create(['must_change_password' => false]));
|
|
$server = $this->activeServer();
|
|
|
|
$reason = "monit.service: Failed with result 'exit-code'.\n"
|
|
.'monit.service - Pro-active monitoring utility for unix systems: a very long detailed '
|
|
.'reason that would be truncated in a 90-char toast but must survive in full here.';
|
|
|
|
$fleet = Mockery::mock(FleetService::class);
|
|
$fleet->shouldReceive('serviceAction')->once()->andReturn(['ok' => false, 'output' => $reason]);
|
|
$fleet->shouldReceive('systemd')->andReturn(['services' => []]);
|
|
app()->instance(FleetService::class, $fleet);
|
|
|
|
$token = $this->confirmed('serviceConfirmed', ['op' => 'start', 'name' => 'monit.service'], $server->id);
|
|
|
|
Livewire::test(Services::class)->call('applyService', $token);
|
|
|
|
$event = AuditEvent::where('action', 'service.start.failed')->first();
|
|
$this->assertNotNull($event, 'a service.start.failed audit event should be written');
|
|
$this->assertTrue($event->is_error);
|
|
$this->assertSame($server->id, $event->server_id);
|
|
// The FULL reason is retained (not truncated), so it is re-readable later.
|
|
$this->assertStringContainsString('must survive in full here.', $event->meta['output']);
|
|
$this->assertStringContainsString('Failed with result', $event->meta['output']);
|
|
}
|
|
|
|
public function test_successful_action_writes_no_failure_event(): void
|
|
{
|
|
$this->actingAs(User::factory()->create(['must_change_password' => false]));
|
|
$server = $this->activeServer();
|
|
|
|
$fleet = Mockery::mock(FleetService::class);
|
|
$fleet->shouldReceive('serviceAction')->once()->andReturn(['ok' => true, 'output' => '']);
|
|
$fleet->shouldReceive('systemd')->andReturn(['services' => []]);
|
|
app()->instance(FleetService::class, $fleet);
|
|
|
|
$token = $this->confirmed('serviceConfirmed', ['op' => 'restart', 'name' => 'nginx.service'], $server->id);
|
|
|
|
Livewire::test(Services::class)->call('applyService', $token);
|
|
|
|
$this->assertSame(0, AuditEvent::where('action', 'like', 'service.%.failed')->count());
|
|
}
|
|
}
|