Files
coolify/tests/Feature/BackupNowAvailabilityTest.php
T
Andras BacsaiandClaude Opus 5.5 8fcff6ebd1 fix(ui): consolidate table row actions into one menu pattern
Rows with two or more actions use a shared "..." menu (x-table.row-actions);
rows with one action keep a single icon button. Every actions column has an
"Actions" header. Removing a member and revoking an invitation now ask for
confirmation. Row and card icons are neutral in dark mode.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 16:14:46 +01:00

137 lines
5.9 KiB
PHP

<?php
use App\Jobs\DatabaseBackupJob;
use App\Livewire\Project\Database\BackupEdit;
use App\Livewire\Project\Service\VolumeBackup\Index;
use App\Models\Environment;
use App\Models\InstanceSettings;
use App\Models\Project;
use App\Models\ScheduledDatabaseBackup;
use App\Models\Server;
use App\Models\Service;
use App\Models\ServiceDatabase;
use App\Models\StandaloneDocker;
use App\Models\StandalonePostgresql;
use App\Models\Team;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Queue;
use Livewire\Livewire;
uses(RefreshDatabase::class);
beforeEach(function () {
InstanceSettings::forceCreate(['id' => 0]);
$team = Team::factory()->create();
$user = User::factory()->create();
$user->teams()->attach($team, ['role' => 'owner']);
$this->actingAs($user);
session(['currentTeam' => $team]);
$server = Server::factory()->create(['team_id' => $team->id]);
$destination = StandaloneDocker::where('server_id', $server->id)->firstOrFail();
$project = Project::factory()->create(['team_id' => $team->id]);
$environment = Environment::factory()->create(['project_id' => $project->id]);
$resourceAttributes = [
'environment_id' => $environment->id,
'destination_id' => $destination->id,
'destination_type' => $destination->getMorphClass(),
];
$this->service = Service::factory()->create(['server_id' => $server->id, ...$resourceAttributes]);
$this->databases = [
'standalone' => StandalonePostgresql::create([
...$resourceAttributes,
'name' => 'postgres',
'postgres_password' => 'password',
]),
'service' => ServiceDatabase::create([
'service_id' => $this->service->id,
'name' => 'postgres',
'image' => 'postgres:16-alpine',
'custom_type' => 'postgresql',
]),
];
$this->backups = collect($this->databases)->map(fn ($database) => ScheduledDatabaseBackup::create([
'team_id' => $team->id,
'frequency' => 'daily',
'database_id' => $database->id,
'database_type' => $database->getMorphClass(),
]));
Queue::fake();
});
dataset('database backup controls', [
'standalone settings' => [BackupEdit::class, 'standalone'],
'service settings' => [BackupEdit::class, 'service'],
'service list' => [Index::class, 'service'],
]);
it('only enables and queues database backups for running targets', function (string $componentClass, string $type, string $status, bool $running) {
$database = $this->databases[$type];
$database->update(['status' => $status]);
$backup = $this->backups[$type]->fresh();
$parameters = $componentClass === Index::class
? ['service' => $this->service]
: ['backup' => $backup, ...($componentClass === BackupEdit::class ? ['availableS3Storages' => collect()] : [])];
$component = Livewire::test($componentClass, $parameters);
$dom = new DOMDocument;
@$dom->loadHTML($component->html());
$buttons = (new DOMXPath($dom))->query('//button');
$backupButtons = [];
foreach ($buttons as $button) {
if (str_starts_with($button->getAttribute('wire:click'), 'backupNow') || str_starts_with($button->getAttribute('wire:click.stop'), 'backupNow')) {
$backupButtons[] = $button;
}
}
expect($backupButtons)->toHaveCount(1);
expect($backupButtons[0]->hasAttribute('disabled'))->toBe(! $running);
$component->call('backupNow', ...($componentClass === Index::class ? ['database', $backup->uuid] : []));
if ($running) {
Queue::assertPushed(DatabaseBackupJob::class);
} else {
$component->assertDispatched('error')->assertNotDispatched('success')->assertNoRedirect();
Queue::assertNotPushed(DatabaseBackupJob::class);
}
})->with('database backup controls')->with([
['running:healthy', true],
['running:unhealthy', true],
['exited:unhealthy', false],
['restarting:unhealthy', false],
]);
it('checks current database status before queuing from a stale page', function (string $componentClass, string $type) {
$database = $this->databases[$type];
$database->update(['status' => 'running:healthy']);
$backup = $this->backups[$type]->fresh();
$parameters = $componentClass === Index::class
? ['service' => $this->service]
: ['backup' => $backup, ...($componentClass === BackupEdit::class ? ['availableS3Storages' => collect()] : [])];
$component = Livewire::test($componentClass, $parameters);
$database->update(['status' => 'exited:unhealthy']);
$component->call('backupNow', ...($componentClass === Index::class ? ['database', $backup->uuid] : []))
->assertDispatched('error')->assertNotDispatched('success')->assertNoRedirect();
Queue::assertNotPushed(DatabaseBackupJob::class);
})->with('database backup controls');
it('refreshes service backup buttons when the service status changes', function (string $componentClass, string $event) {
$database = $this->databases['service'];
$database->update(['status' => 'exited:unhealthy']);
$parameters = $componentClass === Index::class
? ['service' => $this->service]
: ['backup' => $this->backups['service']->fresh(), 'availableS3Storages' => collect()];
$component = Livewire::test($componentClass, $parameters);
$disabledButton = '/<button(?=[^>]*\sdisabled\b)(?=[^>]*wire:click(\.stop)?="backupNow)[^>]*>/s';
$database->update(['status' => 'running:healthy']);
$component->dispatch('echo-private:team.'.currentTeam()->id.','.$event);
expect($component->html())->not->toMatch($disabledButton);
$database->update(['status' => 'exited:unhealthy']);
$component->dispatch('echo-private:team.'.currentTeam()->id.','.$event);
expect($component->html())->toMatch($disabledButton);
})->with([
'service list' => Index::class,
'service settings' => BackupEdit::class,
])->with(['ServiceChecked', 'ServiceStatusChanged']);