feat(backups): unify service backup management (#11574)

This commit is contained in:
Andras Bacsai
2026-08-31 22:51:30 +02:00
committed by GitHub
parent ff8b019296
commit e4146a6314
26 changed files with 1066 additions and 140 deletions
+3 -3
View File
@@ -28,11 +28,11 @@ it('uses the upload reicon for import backup in database configuration nav', fun
->toContain("'icon' => 'upload'");
});
it('uses the upload reicon for import backup in service database sidebar', function () {
$contents = file_get_contents(resource_path('views/components/service-database/sidebar.blade.php'));
it('uses the upload reicon for import backup in service navigation', function () {
$contents = file_get_contents(resource_path('views/components/service/configuration-sidebar.blade.php'));
expect($contents)
->toContain("'label' => 'Import Backup'")
->toContain("'icon' => 'upload'")
->not->toMatch("/'label' => 'Import Backup',\s*'route' => 'project\.service\.database\.import',\s*'icon' => 'storages'/s");
->toContain("'route' => 'project.service.import-backup'");
});
@@ -69,6 +69,20 @@ it('declares deploy authorization on the service container removal confirmation'
);
});
it('declares update authorization on service backup mutation controls', function () {
$importBackupView = file_get_contents(resource_path('views/livewire/project/service/import-backup.blade.php'));
$volumeBackupView = file_get_contents(resource_path('views/livewire/project/service/volume-backup/index.blade.php'));
expect($importBackupView)->toMatch(
'/<x-forms\.listbox(?=[^>]*id="selectedDatabaseUuid")(?=[^>]*canGate="update")(?=[^>]*:canResource="\$service")[^>]*>/'
);
expect($volumeBackupView)
->toMatch('/<x-modal-input(?=[^>]*:title="\'Edit backup schedule\'")(?=[^>]*canGate="update")(?=[^>]*:canResource="\$service")[^>]*>/')
->toMatch('/<x-forms\.button(?=[^>]*wire:click\.stop="backupNow\(\'database\',[^"]+")(?=[^>]*canGate="update")(?=[^>]*:canResource="\$service")[^>]*>/')
->toMatch('/<x-forms\.button(?=[^>]*wire:click\.stop="backupNow\(\'storage\',[^"]+")(?=[^>]*canGate="update")(?=[^>]*:canResource="\$service")[^>]*>/');
});
it('keeps mutable Livewire components behind authorization checks', function (string $path, array $requiredNeedles) {
$source = file_get_contents(base_path($path));
@@ -1,5 +1,7 @@
<?php
use App\Livewire\Project\Service\DatabaseBackups;
it('moves service and database page navigation into their sidebars', function () {
$serviceHeading = file_get_contents(resource_path('views/livewire/project/service/heading.blade.php'));
$databaseHeading = file_get_contents(resource_path('views/livewire/project/database/heading.blade.php'));
@@ -58,7 +60,7 @@ it('groups database and service navigation by user workflow', function () {
expect($serviceSidebar)
->toContain("'Settings' => ['General', 'Domains', 'Environment Variables', 'Persistent Storage']")
->toContain("'Observe & troubleshoot' => ['Runtime Logs', 'Terminal']")
->toContain("'Automation' => ['Scheduled Tasks', 'Webhooks', 'Backups']")
->toContain("'Automation' => ['Scheduled Tasks', 'Webhooks', 'Backups', 'Import Backup']")
->toContain("'Operations' => ['Resource Operations', 'Tags', 'Danger Zone']");
}
});
@@ -110,7 +112,7 @@ it('combines service database and storage backups in one section', function () {
->toContain('class="data-table w-full overflow-x-auto"')
->toContain('backup-table-grid service-backup-table-grid')
->not->toContain('<span class="text-right">Executions</span>')
->toContain('class="data-table-row backup-table-grid text-[13px]')
->toMatch('/class="(?=[^"]*\\bdata-table-row\\b)(?=[^"]*\\bbackup-table-grid\\b)(?=[^"]*text-\\[13px\\])[^\"]*"/')
->toContain('class="listbox-option justify-start! gap-2.5!"')
->toContain('x-data="{ dropdownOpen: false }"')
->toContain('class="listbox-panel left-0! right-auto! z-[90]! w-52! min-w-52! sm:left-auto! sm:right-0!"')
@@ -119,15 +121,147 @@ it('combines service database and storage backups in one section', function () {
expect($styles)->toContain('.service-backup-table-grid');
});
it('links compose database backups to the unified service backups page', function () {
it('keeps backup navigation out of compose database settings', function () {
$sidebar = file_get_contents(resource_path('views/components/service-database/sidebar.blade.php'));
expect($sidebar)
->toContain("'route' => 'project.service.volume-backups.index'")
->toContain("'parameters' => \$serviceParameters")
->not->toContain("'label' => 'Backups'")
->not->toContain("'label' => 'Import Backup'")
->not->toContain("'route' => 'project.service.database.backups'");
});
it('links service backup details back to the unified service backups page', function () {
$sidebar = file_get_contents(resource_path('views/components/backup-sidebar.blade.php'));
expect($sidebar)->toContain("'back' => 'project.service.volume-backups.index'");
});
it('declares the database backup mount return type', function () {
$returnType = (new ReflectionMethod(DatabaseBackups::class, 'mount'))->getReturnType();
expect($returnType)->not->toBeNull()
->and($returnType->getName())->toBe('mixed');
});
it('keeps service navigation visible on backup detail pages and uses section tabs', function () {
$databaseBackup = file_get_contents(resource_path('views/livewire/project/service/database-backups.blade.php'));
$storageBackup = file_get_contents(resource_path('views/livewire/project/service/volume-backup/show.blade.php'));
foreach ([$databaseBackup, $storageBackup] as $view) {
expect($view)
->toContain('<x-service.configuration-sidebar :service="$service"')
->toContain('<x-backup-tabs')
->not->toContain('<x-backup-sidebar');
}
expect($databaseBackup)->toContain('context="service"')
->and($storageBackup)->toContain('context="service-volume"');
});
it('opens service backup schedules in place without navigating', function () {
$index = file_get_contents(resource_path('views/livewire/project/service/volume-backup/index.blade.php'));
expect($index)
->toContain('wire:click="openSchedule(')
->toContain('wireOpen="scheduleModalOpen"')
->toContain('<x-backup-tabs')
->not->toContain("route('project.service.backups.schedule.show'");
});
it('shows service backup executions below schedules without a separate view tab', function () {
$index = file_get_contents(resource_path('views/livewire/project/service/volume-backup/index.blade.php'));
$executions = file_get_contents(resource_path('views/livewire/project/service/backup-executions.blade.php'));
expect($index)
->toContain('<livewire:project.service.backup-executions :service="$service"')
->not->toContain('aria-label="Backup views"')
->not->toContain("route('project.service.backups.executions'");
expect($executions)
->toContain('title="Executions"')
->toContain('flush>');
});
it('wraps backup modal tabs and shows schedule loading feedback', function () {
$tabs = file_get_contents(resource_path('views/components/backup-tabs.blade.php'));
$index = file_get_contents(resource_path('views/livewire/project/service/volume-backup/index.blade.php'));
expect($tabs)
->toContain('flex-wrap')
->not->toContain('overflow-x-auto');
expect($index)
->toContain('<x-table.loading target="openSchedule" text="Loading schedule..."')
->not->toContain('Loading schedule\n');
});
it('loads every backup editor section when the modal opens and switches tabs locally', function () {
$tabs = file_get_contents(resource_path('views/components/backup-tabs.blade.php'));
$index = file_get_contents(resource_path('views/livewire/project/service/volume-backup/index.blade.php'));
expect($index)
->toContain("x-data=\"{ activeSection: 'general' }\"")
->toContain("@foreach (['general', 's3', 'retention', 'danger'] as \$modalSection)")
->toContain("x-show=\"activeSection === '{{ \$modalSection }}'\"");
expect($tabs)
->toContain("@click=\"activeSection = '{{ \$item['key'] }}'\"")
->not->toContain('wire:click="selectScheduleSection');
});
it('adds a back up now action to every service backup schedule row', function () {
$index = file_get_contents(resource_path('views/livewire/project/service/volume-backup/index.blade.php'));
$styles = file_get_contents(resource_path('css/app.css'));
expect($index)
->toContain('<span class="text-right">Actions</span>')
->toContain('<div class="min-w-[59rem]">')
->toContain("wire:click.stop=\"backupNow('database',")
->toContain("wire:click.stop=\"backupNow('storage',")
->toContain('<x-forms.button')
->toContain('Back up now</x-forms.button>')
->not->toContain('class="icon-button shrink-0"')
->not->toContain('class="contents cursor-pointer"');
expect($styles)
->toContain('.service-backup-table-grid')
->toContain('width: 100%;')
->toContain('.data-table-row.service-backup-table-grid {')
->not->toContain('.data-table-header.service-backup-table-grid > :last-child')
->not->toContain('.data-table-row.service-backup-table-grid > :last-child');
});
it('refreshes backup executions from backup broadcasts on the current team channel', function () {
$serviceExecutions = file_get_contents(app_path('Livewire/Project/Service/BackupExecutions.php'));
$serviceBackups = file_get_contents(app_path('Livewire/Project/Service/VolumeBackup/Index.php'));
$databaseExecutions = file_get_contents(app_path('Livewire/Project/Database/BackupExecutions.php'));
$databaseBackupJob = file_get_contents(app_path('Jobs/DatabaseBackupJob.php'));
expect($serviceExecutions)
->toContain('echo-private:team.{$teamId},BackupCreated')
->toContain("=> '\$refresh'")
->and($serviceBackups)
->toContain('echo-private:team.{$teamId},BackupCreated')
->and($databaseExecutions)
->toContain('$teamId = currentTeam()->id')
->not->toContain('$userId = Auth::id()')
->and(strpos($databaseBackupJob, "'finished_at' => Carbon::now()->toImmutable()"))
->toBeLessThan(strrpos($databaseBackupJob, 'BackupCreated::dispatch($this->team->id)'));
});
it('offers downloads from the service backup executions list', function () {
$component = file_get_contents(app_path('Livewire/Project/Service/BackupExecutions.php'));
$view = file_get_contents(resource_path('views/livewire/project/service/backup-executions.blade.php'));
expect($component)
->toContain("route('download.backup'")
->toContain("route('download.volume-backup'")
->and($view)
->toContain('<span class="text-right">Actions</span>')
->toContain('aria-label="Download backup"')
->toContain('@click.stop');
});
it('uses a distinct backup icon across resource sidebars', function () {
$sidebars = [
resource_path('views/components/application/configuration-sidebar.blade.php'),
+260 -47
View File
@@ -1,11 +1,16 @@
<?php
use App\Jobs\DatabaseBackupJob;
use App\Jobs\VolumeBackupJob;
use App\Livewire\Project\Database\Import as DatabaseImport;
use App\Livewire\Project\Service\Heading;
use App\Livewire\Project\Service\VolumeBackup\Index as ServiceVolumeBackupIndex;
use App\Models\Environment;
use App\Models\InstanceSettings;
use App\Models\LocalPersistentVolume;
use App\Models\Project;
use App\Models\ScheduledDatabaseBackup;
use App\Models\ScheduledDatabaseBackupExecution;
use App\Models\Server;
use App\Models\Service;
use App\Models\ServiceApplication;
@@ -16,6 +21,7 @@ use App\Models\User;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Once;
use Livewire\Livewire;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
@@ -113,6 +119,14 @@ test('does not open service database backups route from another team', function
]));
})->throws(NotFoundHttpException::class);
test('does not open service import backup route from another team', function () {
$this->get(route('project.service.import-backup', [
'project_uuid' => $this->projectA->uuid,
'environment_uuid' => $this->environmentA->uuid,
'service_uuid' => $this->otherService->uuid,
]))->assertForbidden();
});
test('does not resolve service database import component from another team', function () {
$component = app(DatabaseImport::class);
$component->parameters = [
@@ -141,7 +155,7 @@ test('owner can still hydrate service heading with own service', function () {
->assertOk();
});
test('service database backup schedules use dedicated general retention and executions urls', function () {
test('legacy service database backup detail urls redirect to unified backup views', function () {
$backup = ScheduledDatabaseBackup::create([
'team_id' => $this->teamA->id,
'frequency' => 'daily',
@@ -156,51 +170,254 @@ test('service database backup schedules use dedicated general retention and exec
'stack_service_uuid' => $this->ownServiceDatabase->uuid,
]);
$generalUrl = $listUrl.'/'.$backup->uuid;
$parameters = [
'project_uuid' => $this->projectA->uuid,
'environment_uuid' => $this->environmentA->uuid,
'service_uuid' => $this->ownService->uuid,
];
$this->get($listUrl)
->assertOk()
->assertSee('href="'.$generalUrl.'"', false);
$this->get($generalUrl)->assertRedirect(route('project.service.volume-backups.index', $parameters));
$this->get($generalUrl.'/s3')->assertRedirect(route('project.service.volume-backups.index', $parameters));
$this->get($generalUrl.'/retention')->assertRedirect(route('project.service.volume-backups.index', $parameters));
$this->get($generalUrl.'/danger')->assertRedirect(route('project.service.volume-backups.index', $parameters));
$this->get($generalUrl.'/executions')->assertRedirect(route('project.service.volume-backups.index', $parameters));
});
$this->get($generalUrl)
->assertOk()
->assertSee('Frequency')
->assertDontSee('S3 Enabled')
->assertDontSee('Number of backups to keep')
->assertDontSee('Cleanup Failed Backups')
->assertDontSee('Delete Backups and Schedule');
test('legacy service database backup list redirects to unified service backups', function () {
$legacyUrl = route('project.service.database.backups', [
'project_uuid' => $this->projectA->uuid,
'environment_uuid' => $this->environmentA->uuid,
'service_uuid' => $this->ownService->uuid,
'stack_service_uuid' => $this->ownServiceDatabase->uuid,
]);
$centralBackupsUrl = route('project.service.volume-backups.index', [
'project_uuid' => $this->projectA->uuid,
'environment_uuid' => $this->environmentA->uuid,
'service_uuid' => $this->ownService->uuid,
]);
$this->get($generalUrl.'/s3')
->assertOk()
->assertSee('S3 Storage')
->assertDontSee('S3 Storage Retention')
->assertDontSee('Local Backup Retention')
->assertDontSee('Frequency')
->assertDontSee('Cleanup Failed Backups');
$this->get($legacyUrl)->assertRedirect($centralBackupsUrl);
});
$this->get($generalUrl.'/retention')
->assertOk()
->assertSee('Local Backup Retention')
->assertSee('S3 Storage Retention')
->assertSee('Number of backups to keep')
->assertDontSee('Frequency')
->assertDontSee('Cleanup Failed Backups');
test('service backup schedules open in place from the unified view', function () {
$backup = ScheduledDatabaseBackup::create([
'team_id' => $this->teamA->id,
'frequency' => 'daily',
'database_id' => $this->ownServiceDatabase->id,
'database_type' => $this->ownServiceDatabase->getMorphClass(),
]);
$this->get($generalUrl.'/executions')
$this->get(route('project.service.volume-backups.index', [
'project_uuid' => $this->projectA->uuid,
'environment_uuid' => $this->environmentA->uuid,
'service_uuid' => $this->ownService->uuid,
]))
->assertOk()
->assertSee('<h2 class="py-0">Executions</h2>', false)
->assertDontSee('Executions <span', false)
->assertSee('Cleanup Failed Backups')
->assertDontSee('Frequency')
->assertDontSee('Number of backups to keep');
->assertSee("wire:click=\"openSchedule('{$backup->uuid}')\"", false);
});
$this->get($generalUrl.'/danger')
test('service database backup schedules open in the Livewire component', function () {
Queue::fake();
$backup = ScheduledDatabaseBackup::create([
'team_id' => $this->teamA->id,
'frequency' => 'daily',
'database_id' => $this->ownServiceDatabase->id,
'database_type' => $this->ownServiceDatabase->getMorphClass(),
]);
Livewire::test(ServiceVolumeBackupIndex::class, ['service' => $this->ownService])
->call('openSchedule', $backup->uuid)
->assertSet('scheduleModalOpen', true)
->assertSet('selectedDatabaseBackup.uuid', $backup->uuid)
->assertSet('selectedVolumeBackup', null);
});
test('service database backups can be queued from the Livewire component', function () {
Queue::fake();
$backup = ScheduledDatabaseBackup::create([
'team_id' => $this->teamA->id,
'frequency' => 'daily',
'database_id' => $this->ownServiceDatabase->id,
'database_type' => $this->ownServiceDatabase->getMorphClass(),
]);
Livewire::test(ServiceVolumeBackupIndex::class, ['service' => $this->ownService])
->call('backupNow', 'database', $backup->uuid)
->assertDispatched('success', 'Backup queued.');
Queue::assertPushed(DatabaseBackupJob::class, fn (DatabaseBackupJob $job): bool => $job->backup->is($backup));
});
test('service storage backups can be queued from the Livewire component', function () {
Queue::fake();
$volume = LocalPersistentVolume::create([
'name' => 'service-data',
'mount_path' => '/data',
'resource_id' => $this->ownServiceDatabase->id,
'resource_type' => $this->ownServiceDatabase->getMorphClass(),
]);
$backup = $volume->scheduledBackups()->create([
'team_id' => $this->teamA->id,
'frequency' => 'daily',
]);
Livewire::test(ServiceVolumeBackupIndex::class, ['service' => $this->ownService])
->call('backupNow', 'storage', $backup->uuid)
->assertDispatched('success', 'Backup queued.');
Queue::assertPushed(VolumeBackupJob::class, fn (VolumeBackupJob $job): bool => $job->backup->is($backup));
});
test('service backup executions combine database execution history', function () {
$backup = ScheduledDatabaseBackup::create([
'team_id' => $this->teamA->id,
'frequency' => 'daily',
'database_id' => $this->ownServiceDatabase->id,
'database_type' => $this->ownServiceDatabase->getMorphClass(),
]);
$execution = ScheduledDatabaseBackupExecution::create([
'scheduled_database_backup_id' => $backup->id,
'status' => 'success',
'database_name' => 'coolify',
'size' => 2048,
'finished_at' => now(),
]);
$this->get(route('project.service.volume-backups.index', [
'project_uuid' => $this->projectA->uuid,
'environment_uuid' => $this->environmentA->uuid,
'service_uuid' => $this->ownService->uuid,
]))
->assertOk()
->assertSee('Danger Zone')
->assertSee('Delete Scheduled Backup')
->assertSee('Delete Backups and Schedule')
->assertDontSee('Frequency')
->assertDontSee('Number of backups to keep')
->assertDontSee('Cleanup Failed Backups');
->assertSee('own-db')
->assertSee('Success')
->assertSee('2 KB');
$this->get(route('project.service.volume-backups.index', [
'project_uuid' => $this->projectA->uuid,
'environment_uuid' => $this->environmentA->uuid,
'service_uuid' => $this->ownService->uuid,
]))->assertSee("wire:click=\"openExecution('{$execution->uuid}')\"", false);
});
test('service import backup page selects from compatible databases', function () {
$secondDatabase = ServiceDatabase::create([
'service_id' => $this->ownService->id,
'name' => 'analytics-db',
'image' => 'mysql:8',
'custom_type' => 'mysql',
]);
$importUrl = route('project.service.import-backup', [
'project_uuid' => $this->projectA->uuid,
'environment_uuid' => $this->environmentA->uuid,
'service_uuid' => $this->ownService->uuid,
]);
$this->get($importUrl)
->assertOk()
->assertSee('Import Backup')
->assertSee('own-db')
->assertSee('analytics-db');
$this->get($importUrl.'/'.$secondDatabase->uuid)
->assertOk()
->assertSee('analytics-db')
->assertSee('Start the database first');
});
test('service import backup redirects when exactly one compatible database exists', function () {
$this->get(route('project.service.import-backup', [
'project_uuid' => $this->projectA->uuid,
'environment_uuid' => $this->environmentA->uuid,
'service_uuid' => $this->ownService->uuid,
]))->assertRedirectToRoute('project.service.import-backup.database', [
'project_uuid' => $this->projectA->uuid,
'environment_uuid' => $this->environmentA->uuid,
'service_uuid' => $this->ownService->uuid,
'stack_service_uuid' => $this->ownServiceDatabase->uuid,
]);
});
test('service import backup excludes unsupported databases', function () {
$unsupportedDatabase = ServiceDatabase::create([
'service_id' => $this->ownService->id,
'name' => 'cache-db',
'image' => 'redis:7-alpine',
'custom_type' => 'redis',
]);
$this->get(route('project.service.import-backup', [
'project_uuid' => $this->projectA->uuid,
'environment_uuid' => $this->environmentA->uuid,
'service_uuid' => $this->ownService->uuid,
]))
->assertRedirectToRoute('project.service.import-backup.database', [
'project_uuid' => $this->projectA->uuid,
'environment_uuid' => $this->environmentA->uuid,
'service_uuid' => $this->ownService->uuid,
'stack_service_uuid' => $this->ownServiceDatabase->uuid,
]);
$this->get(route('project.service.import-backup.database', [
'project_uuid' => $this->projectA->uuid,
'environment_uuid' => $this->environmentA->uuid,
'service_uuid' => $this->ownService->uuid,
'stack_service_uuid' => $unsupportedDatabase->uuid,
]))->assertNotFound();
});
test('service import backup opens the selected compatible database', function () {
$secondDatabase = ServiceDatabase::create([
'service_id' => $this->ownService->id,
'name' => 'analytics-db',
'image' => 'mysql:8',
'custom_type' => 'mysql',
]);
$this->get(route('project.service.import-backup.database', [
'project_uuid' => $this->projectA->uuid,
'environment_uuid' => $this->environmentA->uuid,
'service_uuid' => $this->ownService->uuid,
'stack_service_uuid' => $secondDatabase->uuid,
]))
->assertOk()
->assertSee('analytics-db')
->assertSee('Start the database first');
});
test('service import backup requires update authorization for the service and selected database', function () {
$member = User::factory()->create();
$member->teams()->attach($this->teamA, ['role' => 'member']);
$this->actingAs($member);
$parameters = [
'project_uuid' => $this->projectA->uuid,
'environment_uuid' => $this->environmentA->uuid,
'service_uuid' => $this->ownService->uuid,
];
$this->get(route('project.service.import-backup', $parameters))->assertForbidden();
$this->get(route('project.service.import-backup.database', [
...$parameters,
'stack_service_uuid' => $this->ownServiceDatabase->uuid,
]))->assertForbidden();
});
test('legacy service database import redirects to the service import page with its database selected', function () {
$legacyUrl = route('project.service.database.import', [
'project_uuid' => $this->projectA->uuid,
'environment_uuid' => $this->environmentA->uuid,
'service_uuid' => $this->ownService->uuid,
'stack_service_uuid' => $this->ownServiceDatabase->uuid,
]);
$selectedImportUrl = route('project.service.import-backup.database', [
'project_uuid' => $this->projectA->uuid,
'environment_uuid' => $this->environmentA->uuid,
'service_uuid' => $this->ownService->uuid,
'stack_service_uuid' => $this->ownServiceDatabase->uuid,
]);
$this->get($legacyUrl)->assertRedirect($selectedImportUrl);
});
test('service storage backups page includes schedules from all compose databases', function () {
@@ -211,15 +428,15 @@ test('service storage backups page includes schedules from all compose databases
'custom_type' => 'postgresql',
]);
foreach ([$this->ownServiceDatabase, $secondDatabase] as $database) {
ScheduledDatabaseBackup::create([
$backups = collect([$this->ownServiceDatabase, $secondDatabase])->map(function (ServiceDatabase $database) {
return ScheduledDatabaseBackup::create([
'team_id' => $this->teamA->id,
'description' => $database->name.' backup',
'frequency' => 'daily',
'database_id' => $database->id,
'database_type' => $database->getMorphClass(),
]);
}
});
$this->get(route('project.service.volume-backups.index', [
'project_uuid' => $this->projectA->uuid,
@@ -230,10 +447,6 @@ test('service storage backups page includes schedules from all compose databases
->assertSee('>Database</span>', false)
->assertSee('own-db')
->assertSee('analytics-db')
->assertSee(route('project.service.database.backups', [
'project_uuid' => $this->projectA->uuid,
'environment_uuid' => $this->environmentA->uuid,
'service_uuid' => $this->ownService->uuid,
'stack_service_uuid' => $this->ownServiceDatabase->uuid,
]), false);
->assertSee("wire:click=\"openSchedule('{$backups->first()->uuid}')\"", false)
->assertSee("wire:click=\"backupNow('database', '{$backups->first()->uuid}')\"", false);
});
+10
View File
@@ -7,6 +7,7 @@ use App\Jobs\VolumeBackupRecoveryJob;
use App\Livewire\Project\Application\Backup\Create as CreateScheduledVolumeBackup;
use App\Livewire\Project\Service\FileStorage;
use App\Livewire\Project\Service\VolumeBackup\Create as CreateServiceVolumeBackup;
use App\Livewire\Project\Service\VolumeBackup\Index as ServiceVolumeBackupIndex;
use App\Livewire\Project\Shared\Storages\Show;
use App\Livewire\Project\Shared\Storages\VolumeBackups;
use App\Models\Application;
@@ -26,6 +27,7 @@ use App\Models\ServiceDatabase;
use App\Models\StandaloneDocker;
use App\Models\Team;
use App\Models\User;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\RedirectResponse;
use Illuminate\Queue\Middleware\WithoutOverlapping;
@@ -44,6 +46,14 @@ use Symfony\Component\HttpKernel\Exception\HttpException;
uses(RefreshDatabase::class);
it('types service backup S3 storage state as a nullable Eloquent collection', function () {
$property = new ReflectionProperty(ServiceVolumeBackupIndex::class, 's3s');
expect($property->getType()?->getName())->toBe(Collection::class)
->and($property->getType()?->allowsNull())->toBeTrue()
->and($property->getDefaultValue())->toBeNull();
});
it('provides the volume backup domain classes and relationship', function () {
expect(class_exists(ScheduledVolumeBackup::class))->toBeTrue()
->and(class_exists(ScheduledVolumeBackupExecution::class))->toBeTrue()