fix(backups): require valid S3 storage selection
Preserve S3 backups when a single valid storage is available, require explicit selection when multiple storages exist, and disable S3 when none are available. Make backup action controls responsive on narrow screens.
This commit is contained in:
parent
b93a91cc00
commit
22b31f5671
8 changed files with 157 additions and 50 deletions
|
|
@ -3,10 +3,12 @@
|
||||||
namespace App\Livewire\Project\Database;
|
namespace App\Livewire\Project\Database;
|
||||||
|
|
||||||
use App\Jobs\DatabaseBackupJob;
|
use App\Jobs\DatabaseBackupJob;
|
||||||
|
use App\Models\S3Storage;
|
||||||
use App\Models\ScheduledDatabaseBackup;
|
use App\Models\ScheduledDatabaseBackup;
|
||||||
use App\Models\ServiceDatabase;
|
use App\Models\ServiceDatabase;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
use Livewire\Attributes\Locked;
|
use Livewire\Attributes\Locked;
|
||||||
use Livewire\Attributes\Validate;
|
use Livewire\Attributes\Validate;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
|
|
@ -18,7 +20,7 @@ class BackupEdit extends Component
|
||||||
public ScheduledDatabaseBackup $backup;
|
public ScheduledDatabaseBackup $backup;
|
||||||
|
|
||||||
#[Locked]
|
#[Locked]
|
||||||
public $s3s;
|
public $availableS3Storages;
|
||||||
|
|
||||||
#[Locked]
|
#[Locked]
|
||||||
public $parameters;
|
public $parameters;
|
||||||
|
|
@ -69,7 +71,7 @@ class BackupEdit extends Component
|
||||||
public bool $disableLocalBackup = false;
|
public bool $disableLocalBackup = false;
|
||||||
|
|
||||||
#[Validate(['nullable', 'integer'])]
|
#[Validate(['nullable', 'integer'])]
|
||||||
public ?int $s3StorageId = 1;
|
public ?int $s3StorageId = null;
|
||||||
|
|
||||||
#[Validate(['nullable', 'string'])]
|
#[Validate(['nullable', 'string'])]
|
||||||
public ?string $databasesToBackup = null;
|
public ?string $databasesToBackup = null;
|
||||||
|
|
@ -222,10 +224,18 @@ private function customValidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
// S3 backup cannot be enabled without a valid S3 storage owned by the team
|
// S3 backup cannot be enabled without a valid S3 storage owned by the team
|
||||||
$availableS3Ids = collect($this->s3s)->pluck('id');
|
$availableS3Ids = $this->availableS3StorageIds();
|
||||||
if ($this->backup->save_s3 && ! $availableS3Ids->contains($this->backup->s3_storage_id)) {
|
if ($this->backup->save_s3 && ! $availableS3Ids->contains($this->backup->s3_storage_id)) {
|
||||||
$this->backup->save_s3 = $this->saveS3 = false;
|
if ($availableS3Ids->isEmpty()) {
|
||||||
$this->backup->s3_storage_id = $this->s3StorageId = null;
|
$this->backup->s3_storage_id = $this->s3StorageId = null;
|
||||||
|
$this->backup->save_s3 = $this->saveS3 = false;
|
||||||
|
} elseif ($this->backup->s3_storage_id === null && $availableS3Ids->count() === 1) {
|
||||||
|
$this->backup->s3_storage_id = $this->s3StorageId = $availableS3Ids->first();
|
||||||
|
} else {
|
||||||
|
$this->backup->s3_storage_id = $this->s3StorageId = null;
|
||||||
|
|
||||||
|
throw new Exception('Please select a valid S3 storage to enable S3 backups.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate that disable_local_backup can only be true when S3 backup is enabled
|
// Validate that disable_local_backup can only be true when S3 backup is enabled
|
||||||
|
|
@ -240,6 +250,23 @@ private function customValidate()
|
||||||
$this->validate();
|
$this->validate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function availableS3StorageIds(): Collection
|
||||||
|
{
|
||||||
|
$storages = collect($this->availableS3Storages);
|
||||||
|
$storageIds = $storages->pluck('id')->filter()->all();
|
||||||
|
$teamIds = $storages->pluck('team_id')->reject(fn ($teamId) => $teamId === null)->unique()->values()->all();
|
||||||
|
|
||||||
|
if (empty($storageIds) || empty($teamIds)) {
|
||||||
|
return collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
return S3Storage::query()
|
||||||
|
->whereKey($storageIds)
|
||||||
|
->whereIn('team_id', $teamIds)
|
||||||
|
->where('is_usable', true)
|
||||||
|
->pluck('id');
|
||||||
|
}
|
||||||
|
|
||||||
public function submit()
|
public function submit()
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -129,7 +129,11 @@
|
||||||
}
|
}
|
||||||
}"
|
}"
|
||||||
@keydown.escape.window="if (modalOpen) { modalOpen = false; resetModal(); }" :class="{ 'z-40': modalOpen }"
|
@keydown.escape.window="if (modalOpen) { modalOpen = false; resetModal(); }" :class="{ 'z-40': modalOpen }"
|
||||||
class="relative w-auto h-auto">
|
@class([
|
||||||
|
'relative h-auto',
|
||||||
|
'w-full' => $buttonFullWidth,
|
||||||
|
'w-auto' => ! $buttonFullWidth,
|
||||||
|
])>
|
||||||
@if (isset($trigger))
|
@if (isset($trigger))
|
||||||
<div @click="modalOpen=true">
|
<div @click="modalOpen=true">
|
||||||
{{ $trigger }}
|
{{ $trigger }}
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,37 @@
|
||||||
<form wire:submit="submit">
|
<form wire:submit="submit">
|
||||||
<div class="flex gap-2 pb-2">
|
<div class="flex flex-col gap-3 pb-4 sm:flex-row sm:items-start sm:justify-between">
|
||||||
<h2>Scheduled Backup</h2>
|
<h2>Scheduled Backup</h2>
|
||||||
<x-forms.button type="submit">
|
<div class="grid grid-cols-1 gap-2 min-[420px]:grid-cols-2 sm:flex sm:flex-wrap sm:justify-end">
|
||||||
Save
|
<x-forms.button type="submit" class="w-full sm:w-auto">
|
||||||
</x-forms.button>
|
Save
|
||||||
@if (str($status)->startsWith('running'))
|
</x-forms.button>
|
||||||
<x-forms.button wire:click='backupNow'>Backup Now</x-forms.button>
|
@if (str($status)->startsWith('running'))
|
||||||
@endif
|
<x-forms.button wire:click='backupNow' class="w-full sm:w-auto">Backup Now</x-forms.button>
|
||||||
@if ($backup->database_id !== 0)
|
@endif
|
||||||
<x-modal-confirmation title="Confirm Backup Schedule Deletion?" buttonTitle="Delete Backups and Schedule"
|
@if ($backup->database_id !== 0)
|
||||||
isErrorButton submitAction="delete" :checkboxes="$checkboxes" :actions="[
|
<div class="min-[420px]:col-span-2 sm:col-span-1">
|
||||||
'The selected backup schedule will be deleted.',
|
<x-modal-confirmation title="Confirm Backup Schedule Deletion?"
|
||||||
'Scheduled backups for this database will be stopped (if this is the only backup schedule for this database).',
|
buttonTitle="Delete Backups and Schedule" buttonFullWidth isErrorButton submitAction="delete"
|
||||||
]"
|
:checkboxes="$checkboxes" :actions="[
|
||||||
confirmationText="{{ $backup->database->name }}"
|
'The selected backup schedule will be deleted.',
|
||||||
confirmationLabel="Please confirm the execution of the actions by entering the Database Name of the scheduled backups below"
|
'Scheduled backups for this database will be stopped (if this is the only backup schedule for this database).',
|
||||||
shortConfirmationLabel="Database Name" />
|
]"
|
||||||
@endif
|
confirmationText="{{ $backup->database->name }}"
|
||||||
|
confirmationLabel="Please confirm the execution of the actions by entering the Database Name of the scheduled backups below"
|
||||||
|
shortConfirmationLabel="Database Name" />
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-64 pb-2">
|
<div class="w-full max-w-md pb-2">
|
||||||
<x-forms.checkbox instantSave label="Backup Enabled" id="backupEnabled" />
|
<x-forms.checkbox instantSave label="Backup Enabled" id="backupEnabled" />
|
||||||
@if ($s3s->count() > 0)
|
@if ($availableS3Storages->count() > 0)
|
||||||
<x-forms.checkbox instantSave label="S3 Enabled" id="saveS3" />
|
<x-forms.checkbox instantSave label="S3 Enabled" id="saveS3" />
|
||||||
@else
|
@else
|
||||||
<x-forms.checkbox instantSave helper="No validated S3 storage available." label="S3 Enabled" id="saveS3"
|
<x-forms.checkbox instantSave helper="No validated S3 storage available." label="S3 Enabled" id="saveS3"
|
||||||
disabled />
|
disabled />
|
||||||
@endif
|
@endif
|
||||||
@if ($backup->save_s3)
|
@if ($saveS3)
|
||||||
<x-forms.checkbox instantSave label="Disable Local Backup" id="disableLocalBackup"
|
<x-forms.checkbox instantSave label="Disable Local Backup" id="disableLocalBackup"
|
||||||
helper="When enabled, backup files will be deleted from local storage immediately after uploading to S3. This requires S3 backup to be enabled." />
|
helper="When enabled, backup files will be deleted from local storage immediately after uploading to S3. This requires S3 backup to be enabled." />
|
||||||
@else
|
@else
|
||||||
|
|
@ -34,11 +39,11 @@
|
||||||
helper="When enabled, backup files will be deleted from local storage immediately after uploading to S3. This requires S3 backup to be enabled." />
|
helper="When enabled, backup files will be deleted from local storage immediately after uploading to S3. This requires S3 backup to be enabled." />
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
@if ($backup->save_s3)
|
@if ($saveS3)
|
||||||
<div class="pb-6">
|
<div class="w-full max-w-md pb-6">
|
||||||
<x-forms.select id="s3StorageId" label="S3 Storage" required>
|
<x-forms.select id="s3StorageId" label="S3 Storage" required>
|
||||||
<option value="default" disabled>Select a S3 storage</option>
|
<option value="default" disabled>Select a S3 storage</option>
|
||||||
@foreach ($s3s as $s3)
|
@foreach ($availableS3Storages as $s3)
|
||||||
<option value="{{ $s3->id }}">{{ $s3->name }}</option>
|
<option value="{{ $s3->id }}">{{ $s3->name }}</option>
|
||||||
@endforeach
|
@endforeach
|
||||||
</x-forms.select>
|
</x-forms.select>
|
||||||
|
|
@ -80,7 +85,7 @@
|
||||||
@endif
|
@endif
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2">
|
<div class="grid grid-cols-1 gap-2 md:grid-cols-3">
|
||||||
<x-forms.input label="Frequency" id="frequency" required />
|
<x-forms.input label="Frequency" id="frequency" required />
|
||||||
<x-forms.input label="Timezone" id="timezone" disabled
|
<x-forms.input label="Timezone" id="timezone" disabled
|
||||||
helper="The timezone of the server where the backup is scheduled to run (if not set, the instance timezone will be used)" required />
|
helper="The timezone of the server where the backup is scheduled to run (if not set, the instance timezone will be used)" required />
|
||||||
|
|
@ -98,7 +103,7 @@
|
||||||
<div class="flex gap-6 flex-col">
|
<div class="flex gap-6 flex-col">
|
||||||
<div>
|
<div>
|
||||||
<h4 class="mb-3 font-medium">Local Backup Retention</h4>
|
<h4 class="mb-3 font-medium">Local Backup Retention</h4>
|
||||||
<div class="flex gap-2">
|
<div class="grid grid-cols-1 gap-2 md:grid-cols-3">
|
||||||
<x-forms.input label="Number of backups to keep" id="databaseBackupRetentionAmountLocally"
|
<x-forms.input label="Number of backups to keep" id="databaseBackupRetentionAmountLocally"
|
||||||
type="number" min="0"
|
type="number" min="0"
|
||||||
helper="Keeps only the specified number of most recent backups on the server. Set to 0 for unlimited backups." required />
|
helper="Keeps only the specified number of most recent backups on the server. Set to 0 for unlimited backups." required />
|
||||||
|
|
@ -111,10 +116,10 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if ($backup->save_s3)
|
@if ($saveS3)
|
||||||
<div>
|
<div>
|
||||||
<h4 class="mb-3 font-medium">S3 Storage Retention</h4>
|
<h4 class="mb-3 font-medium">S3 Storage Retention</h4>
|
||||||
<div class="flex gap-2">
|
<div class="grid grid-cols-1 gap-2 md:grid-cols-3">
|
||||||
<x-forms.input label="Number of backups to keep" id="databaseBackupRetentionAmountS3"
|
<x-forms.input label="Number of backups to keep" id="databaseBackupRetentionAmountS3"
|
||||||
type="number" min="0"
|
type="number" min="0"
|
||||||
helper="Keeps only the specified number of most recent backups on S3 storage. Set to 0 for unlimited backups." required />
|
helper="Keeps only the specified number of most recent backups on S3 storage. Set to 0 for unlimited backups." required />
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<div wire:init='refreshBackupExecutions'>
|
<div wire:init='refreshBackupExecutions'>
|
||||||
@isset($backup)
|
@isset($backup)
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex flex-col gap-3 py-4 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<h3 class="py-4">Executions <span class="text-xs">({{ $executions_count }})</span></h3>
|
<h3 class="py-0">Executions <span class="text-xs">({{ $executions_count }})</span></h3>
|
||||||
@if ($executions_count > 0)
|
@if ($executions_count > 0)
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<x-forms.button disabled="{{ !$showPrev }}" wire:click="previousPage('{{ $defaultTake }}')">
|
<x-forms.button disabled="{{ !$showPrev }}" wire:click="previousPage('{{ $defaultTake }}')">
|
||||||
|
|
@ -21,13 +21,15 @@
|
||||||
</x-forms.button>
|
</x-forms.button>
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
<x-forms.button wire:click='cleanupFailed'>Cleanup Failed Backups</x-forms.button>
|
<div class="grid grid-cols-1 gap-2 min-[420px]:grid-cols-2 sm:flex sm:flex-wrap sm:justify-end">
|
||||||
<x-modal-confirmation title="Cleanup Deleted Backup Entries?" buttonTitle="Cleanup Deleted" isErrorButton
|
<x-forms.button wire:click='cleanupFailed' class="w-full sm:w-auto">Cleanup Failed Backups</x-forms.button>
|
||||||
submitAction="cleanupDeleted()"
|
<x-modal-confirmation title="Cleanup Deleted Backup Entries?" buttonTitle="Cleanup Deleted" isErrorButton
|
||||||
:actions="['This will permanently delete all backup execution entries that are marked as deleted from local storage.', 'This only removes database entries, not actual backup files.']"
|
buttonFullWidth submitAction="cleanupDeleted()"
|
||||||
confirmationText="cleanup deleted backups"
|
:actions="['This will permanently delete all backup execution entries that are marked as deleted from local storage.', 'This only removes database entries, not actual backup files.']"
|
||||||
confirmationLabel="Please confirm by typing 'cleanup deleted backups' below"
|
confirmationText="cleanup deleted backups"
|
||||||
shortConfirmationLabel="Confirmation" />
|
confirmationLabel="Please confirm by typing 'cleanup deleted backups' below"
|
||||||
|
shortConfirmationLabel="Confirmation" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div @if (!$skip) wire:poll.5000ms="refreshBackupExecutions" @endif
|
<div @if (!$skip) wire:poll.5000ms="refreshBackupExecutions" @endif
|
||||||
class="flex flex-col gap-4">
|
class="flex flex-col gap-4">
|
||||||
|
|
@ -87,7 +89,7 @@ class="flex flex-col gap-4">
|
||||||
<div class="text-gray-600 dark:text-gray-400 text-sm">
|
<div class="text-gray-600 dark:text-gray-400 text-sm">
|
||||||
Location: {{ data_get($execution, 'filename', 'N/A') }}
|
Location: {{ data_get($execution, 'filename', 'N/A') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-3 mt-2">
|
<div class="flex flex-col gap-2 mt-2 sm:flex-row sm:flex-wrap sm:items-center sm:gap-3">
|
||||||
<div class="text-gray-600 dark:text-gray-400 text-sm">
|
<div class="text-gray-600 dark:text-gray-400 text-sm">
|
||||||
Backup Availability:
|
Backup Availability:
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -154,9 +156,9 @@ class="flex flex-col gap-4">
|
||||||
<pre class="whitespace-pre-wrap text-sm">{{ data_get($execution, 'message') }}</pre>
|
<pre class="whitespace-pre-wrap text-sm">{{ data_get($execution, 'message') }}</pre>
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
<div class="flex gap-2 mt-4">
|
<div class="flex flex-col gap-2 mt-4 min-[420px]:flex-row min-[420px]:flex-wrap">
|
||||||
@if (data_get($execution, 'status') === 'success')
|
@if (data_get($execution, 'status') === 'success')
|
||||||
<x-forms.button class="dark:hover:bg-coolgray-400"
|
<x-forms.button class="w-full dark:hover:bg-coolgray-400 min-[420px]:w-auto"
|
||||||
x-on:click="download_file('{{ data_get($execution, 'id') }}')">Download</x-forms.button>
|
x-on:click="download_file('{{ data_get($execution, 'id') }}')">Download</x-forms.button>
|
||||||
@endif
|
@endif
|
||||||
@php
|
@php
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
<livewire:project.shared.configuration-checker :resource="$database" />
|
<livewire:project.shared.configuration-checker :resource="$database" />
|
||||||
<livewire:project.database.heading :database="$database" />
|
<livewire:project.database.heading :database="$database" />
|
||||||
<div>
|
<div>
|
||||||
<livewire:project.database.backup-edit :backup="$backup" :s3s="$s3s" :status="data_get($database, 'status')" />
|
<livewire:project.database.backup-edit :backup="$backup" :available-s3-storages="$s3s" :status="data_get($database, 'status')" />
|
||||||
<livewire:project.database.backup-executions :backup="$backup" />
|
<livewire:project.database.backup-executions :backup="$backup" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -216,7 +216,7 @@ class="px-3 py-1 rounded-md text-xs font-medium tracking-wide shadow-xs bg-gray-
|
||||||
@if ($type === 'service-database' && $selectedBackup)
|
@if ($type === 'service-database' && $selectedBackup)
|
||||||
<div class="pt-10">
|
<div class="pt-10">
|
||||||
<livewire:project.database.backup-edit wire:key="{{ $selectedBackup->id }}" :backup="$selectedBackup"
|
<livewire:project.database.backup-edit wire:key="{{ $selectedBackup->id }}" :backup="$selectedBackup"
|
||||||
:s3s="$s3s" :status="data_get($database, 'status')" />
|
:available-s3-storages="$s3s" :status="data_get($database, 'status')" />
|
||||||
<livewire:project.database.backup-executions wire:key="{{ $selectedBackup->uuid }}" :backup="$selectedBackup"
|
<livewire:project.database.backup-executions wire:key="{{ $selectedBackup->uuid }}" :backup="$selectedBackup"
|
||||||
:database="$database" />
|
:database="$database" />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@
|
||||||
<x-forms.input type="password" label="Password" readonly id="postgres_password" />
|
<x-forms.input type="password" label="Password" readonly id="postgres_password" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<livewire:project.database.backup-edit :backup="$backup" :s3s="$s3s" :status="data_get($database, 'status')" />
|
<livewire:project.database.backup-edit :backup="$backup" :available-s3-storages="$s3s" :status="data_get($database, 'status')" />
|
||||||
<div class="py-4">
|
<div class="py-4">
|
||||||
<livewire:project.database.backup-executions :backup="$backup" />
|
<livewire:project.database.backup-executions :backup="$backup" />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
use App\Models\Environment;
|
use App\Models\Environment;
|
||||||
use App\Models\InstanceSettings;
|
use App\Models\InstanceSettings;
|
||||||
use App\Models\Project;
|
use App\Models\Project;
|
||||||
|
use App\Models\S3Storage;
|
||||||
use App\Models\ScheduledDatabaseBackup;
|
use App\Models\ScheduledDatabaseBackup;
|
||||||
use App\Models\Server;
|
use App\Models\Server;
|
||||||
use App\Models\StandaloneDocker;
|
use App\Models\StandaloneDocker;
|
||||||
|
|
@ -43,6 +44,20 @@ function createBackupForEditValidationTest(Team $team, array $overrides = []): S
|
||||||
], $overrides));
|
], $overrides));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createS3StorageForBackupEditValidationTest(Team|int $team, string $name = 'Backup Edit S3'): S3Storage
|
||||||
|
{
|
||||||
|
return S3Storage::create([
|
||||||
|
'name' => $name,
|
||||||
|
'region' => 'us-east-1',
|
||||||
|
'key' => 'test-key',
|
||||||
|
'secret' => 'test-secret',
|
||||||
|
'bucket' => 'test-bucket',
|
||||||
|
'endpoint' => 'https://s3.example.com',
|
||||||
|
'is_usable' => true,
|
||||||
|
'team_id' => $team instanceof Team ? $team->id : $team,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
if (InstanceSettings::find(0) === null) {
|
if (InstanceSettings::find(0) === null) {
|
||||||
$settings = new InstanceSettings;
|
$settings = new InstanceSettings;
|
||||||
|
|
@ -60,7 +75,7 @@ function createBackupForEditValidationTest(Team $team, array $overrides = []): S
|
||||||
it('disables S3 backup when saved without a selected S3 storage', function () {
|
it('disables S3 backup when saved without a selected S3 storage', function () {
|
||||||
$backup = createBackupForEditValidationTest($this->team);
|
$backup = createBackupForEditValidationTest($this->team);
|
||||||
|
|
||||||
Livewire::test(BackupEdit::class, ['backup' => $backup->fresh(), 's3s' => $this->team->s3s])
|
Livewire::test(BackupEdit::class, ['backup' => $backup->fresh(), 'availableS3Storages' => $this->team->s3s])
|
||||||
->call('submit')
|
->call('submit')
|
||||||
->assertDispatched('success');
|
->assertDispatched('success');
|
||||||
|
|
||||||
|
|
@ -74,7 +89,7 @@ function createBackupForEditValidationTest(Team $team, array $overrides = []): S
|
||||||
'disable_local_backup' => true,
|
'disable_local_backup' => true,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
Livewire::test(BackupEdit::class, ['backup' => $backup->fresh(), 's3s' => $this->team->s3s])
|
Livewire::test(BackupEdit::class, ['backup' => $backup->fresh(), 'availableS3Storages' => $this->team->s3s])
|
||||||
->call('submit')
|
->call('submit')
|
||||||
->assertDispatched('success');
|
->assertDispatched('success');
|
||||||
|
|
||||||
|
|
@ -83,3 +98,57 @@ function createBackupForEditValidationTest(Team $team, array $overrides = []): S
|
||||||
expect($backup->s3_storage_id)->toBeNull();
|
expect($backup->s3_storage_id)->toBeNull();
|
||||||
expect($backup->disable_local_backup)->toBeFalsy();
|
expect($backup->disable_local_backup)->toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('keeps S3 enabled by selecting the only available team storage when none is selected yet', function () {
|
||||||
|
createS3StorageForBackupEditValidationTest(Team::factory()->create());
|
||||||
|
$s3 = createS3StorageForBackupEditValidationTest($this->team);
|
||||||
|
$backup = createBackupForEditValidationTest($this->team, [
|
||||||
|
'save_s3' => false,
|
||||||
|
's3_storage_id' => null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
Livewire::test(BackupEdit::class, ['backup' => $backup->fresh(), 'availableS3Storages' => $this->team->s3s])
|
||||||
|
->set('saveS3', true)
|
||||||
|
->call('instantSave')
|
||||||
|
->assertDispatched('success');
|
||||||
|
|
||||||
|
$backup->refresh();
|
||||||
|
expect($backup->save_s3)->toBeTruthy();
|
||||||
|
expect($backup->s3_storage_id)->toBe($s3->id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('requires an explicit S3 selection when multiple storages are available', function () {
|
||||||
|
createS3StorageForBackupEditValidationTest($this->team, 'First S3');
|
||||||
|
createS3StorageForBackupEditValidationTest($this->team, 'Second S3');
|
||||||
|
$backup = createBackupForEditValidationTest($this->team, [
|
||||||
|
'save_s3' => false,
|
||||||
|
's3_storage_id' => null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
Livewire::test(BackupEdit::class, ['backup' => $backup->fresh(), 'availableS3Storages' => $this->team->s3s])
|
||||||
|
->set('saveS3', true)
|
||||||
|
->call('instantSave')
|
||||||
|
->assertDispatched('error');
|
||||||
|
|
||||||
|
$backup->refresh();
|
||||||
|
expect($backup->save_s3)->toBeFalsy();
|
||||||
|
expect($backup->s3_storage_id)->toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts the S3 storage scope passed to the component', function () {
|
||||||
|
$s3 = createS3StorageForBackupEditValidationTest(0);
|
||||||
|
$backup = createBackupForEditValidationTest($this->team, [
|
||||||
|
'save_s3' => false,
|
||||||
|
's3_storage_id' => null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
Livewire::test(BackupEdit::class, ['backup' => $backup->fresh(), 'availableS3Storages' => collect([$s3])])
|
||||||
|
->set('saveS3', true)
|
||||||
|
->set('s3StorageId', $s3->id)
|
||||||
|
->call('instantSave')
|
||||||
|
->assertDispatched('success');
|
||||||
|
|
||||||
|
$backup->refresh();
|
||||||
|
expect($backup->save_s3)->toBeTruthy();
|
||||||
|
expect($backup->s3_storage_id)->toBe($s3->id);
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue