mirror of
https://github.com/coollabsio/coolify.git
synced 2026-09-28 02:06:37 -04:00
feat(backups): unify service backup management (#11574)
This commit is contained in:
@@ -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'),
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user