mirror of
https://github.com/coollabsio/coolify.git
synced 2026-08-24 02:24:11 -05:00
Hide source path from volume creation UI (#7591)
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
APP_ENV=testing
|
||||
APP_KEY=base64:8VEfVNVkXQ9mH2L33WBWNMF4eQ0BWD5CTzB8mIxcl+k=
|
||||
APP_DEBUG=true
|
||||
APP_MAINTENANCE_DRIVER=file
|
||||
|
||||
DB_CONNECTION=testing
|
||||
|
||||
|
||||
@@ -77,6 +77,7 @@ class Storage extends Component
|
||||
$this->activeTab = $this->resolveDefaultTab();
|
||||
$this->fileStorage = collect();
|
||||
$this->loadFileStorageForActiveTab();
|
||||
$this->name = $this->generateDefaultVolumeName();
|
||||
}
|
||||
|
||||
public function refreshStoragesFromEvent()
|
||||
@@ -201,9 +202,7 @@ class Storage extends Component
|
||||
$this->validate([
|
||||
'name' => ValidationPatterns::volumeNameRules(),
|
||||
'mount_path' => 'required|string',
|
||||
'host_path' => $this->isSwarm
|
||||
? ['required', 'string', 'regex:'.ValidationPatterns::DIRECTORY_PATH_PATTERN]
|
||||
: ['nullable', 'string', 'regex:'.ValidationPatterns::DIRECTORY_PATH_PATTERN],
|
||||
'host_path' => ['nullable', 'string', 'regex:'.ValidationPatterns::DIRECTORY_PATH_PATTERN],
|
||||
], array_merge(ValidationPatterns::volumeNameMessages(), [
|
||||
'host_path.regex' => 'Host path must start with / and only contain safe path characters.',
|
||||
]));
|
||||
@@ -340,7 +339,7 @@ class Storage extends Component
|
||||
|
||||
public function clearForm()
|
||||
{
|
||||
$this->name = '';
|
||||
$this->name = $this->generateDefaultVolumeName();
|
||||
$this->mount_path = '';
|
||||
$this->host_path = null;
|
||||
$this->file_storage_path = '';
|
||||
@@ -373,6 +372,13 @@ class Storage extends Component
|
||||
throw new \Exception('No valid resource type for file mount storage type!');
|
||||
}
|
||||
|
||||
private function generateDefaultVolumeName(): string
|
||||
{
|
||||
$name = str($this->resource->name)->slug()->value();
|
||||
|
||||
return ($name ?: 'volume').'-data';
|
||||
}
|
||||
|
||||
public function fileStoragePreviewPath(): string
|
||||
{
|
||||
$path = str($this->file_storage_path)->trim();
|
||||
|
||||
@@ -107,6 +107,25 @@ class All extends Component
|
||||
$this->submit($storageId);
|
||||
}
|
||||
|
||||
public function clearHostPath(int $storageId): void
|
||||
{
|
||||
$this->authorize('update', $this->resource);
|
||||
|
||||
$storage = $this->findStorageOrFail($storageId);
|
||||
if ($storage->shouldBeReadOnlyInUI()) {
|
||||
$this->dispatch('error', 'This volume is read-only.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$storage->host_path = null;
|
||||
$storage->save();
|
||||
$this->forms[$storageId]['hostPath'] = null;
|
||||
|
||||
$this->dispatch('configurationChanged');
|
||||
$this->dispatch('success', 'Source path removed. Use a directory mount for host directory bindings.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Livewire listbox onChange cannot pass args; PR suffix fields call this via updatedForms.
|
||||
*/
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class DuskServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register Dusk's browser macros.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
\Laravel\Dusk\Browser::macro('loginWithRootUser', function () {
|
||||
return $this->visit('/login')
|
||||
->type('email', 'test@example.com')
|
||||
->type('password', 'password')
|
||||
->press('Login');
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -64,7 +64,6 @@
|
||||
"driftingly/rector-laravel": "^2.5.0",
|
||||
"fakerphp/faker": "^1.24.1",
|
||||
"laravel/boost": "^2.4.8",
|
||||
"laravel/dusk": "^8.6.0",
|
||||
"laravel/pint": "^1.30.4",
|
||||
"mockery/mockery": "^1.6.12",
|
||||
"nunomaduro/collision": "^8.9.5",
|
||||
|
||||
Generated
+1
-141
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "2d511da9e5e82eade5aa7e5094c888ae",
|
||||
"content-hash": "13e5d201c34a64cdf53e80a21304c9d5",
|
||||
"packages": [
|
||||
{
|
||||
"name": "aws/aws-crt-php",
|
||||
@@ -13698,80 +13698,6 @@
|
||||
},
|
||||
"time": "2026-05-19T20:09:50+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laravel/dusk",
|
||||
"version": "v8.6.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/laravel/dusk.git",
|
||||
"reference": "e7fd48762c6a82ad2cd311db07587aa2a97ce143"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/laravel/dusk/zipball/e7fd48762c6a82ad2cd311db07587aa2a97ce143",
|
||||
"reference": "e7fd48762c6a82ad2cd311db07587aa2a97ce143",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-json": "*",
|
||||
"ext-zip": "*",
|
||||
"guzzlehttp/guzzle": "^7.5",
|
||||
"illuminate/console": "^10.0|^11.0|^12.0|^13.0",
|
||||
"illuminate/support": "^10.0|^11.0|^12.0|^13.0",
|
||||
"php": "^8.1",
|
||||
"php-webdriver/webdriver": "^1.15.2",
|
||||
"symfony/console": "^6.2|^7.0|^8.0",
|
||||
"symfony/finder": "^6.2|^7.0|^8.0",
|
||||
"symfony/process": "^6.2|^7.0|^8.0",
|
||||
"vlucas/phpdotenv": "^5.2"
|
||||
},
|
||||
"require-dev": {
|
||||
"laravel/framework": "^10.0|^11.0|^12.0|^13.0",
|
||||
"mockery/mockery": "^1.6",
|
||||
"orchestra/testbench-core": "^8.19|^9.17|^10.8|^11.0",
|
||||
"phpstan/phpstan": "^1.10",
|
||||
"phpunit/phpunit": "^10.1|^11.0|^12.0.1",
|
||||
"psy/psysh": "^0.11.12|^0.12",
|
||||
"symfony/yaml": "^6.2|^7.0|^8.0"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-pcntl": "Used to gracefully terminate Dusk when tests are running."
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"Laravel\\Dusk\\DuskServiceProvider"
|
||||
]
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Laravel\\Dusk\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Taylor Otwell",
|
||||
"email": "taylor@laravel.com"
|
||||
}
|
||||
],
|
||||
"description": "Laravel Dusk provides simple end-to-end testing and browser automation.",
|
||||
"keywords": [
|
||||
"laravel",
|
||||
"testing",
|
||||
"webdriver"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/laravel/dusk/issues",
|
||||
"source": "https://github.com/laravel/dusk/tree/v8.6.0"
|
||||
},
|
||||
"time": "2026-04-15T14:50:40+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laravel/pint",
|
||||
"version": "v1.30.4",
|
||||
@@ -14817,72 +14743,6 @@
|
||||
},
|
||||
"time": "2022-02-21T01:04:05+00:00"
|
||||
},
|
||||
{
|
||||
"name": "php-webdriver/webdriver",
|
||||
"version": "1.16.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/php-webdriver/php-webdriver.git",
|
||||
"reference": "ac0662863aa120b4f645869f584013e4c4dba46a"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/php-webdriver/php-webdriver/zipball/ac0662863aa120b4f645869f584013e4c4dba46a",
|
||||
"reference": "ac0662863aa120b4f645869f584013e4c4dba46a",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-curl": "*",
|
||||
"ext-json": "*",
|
||||
"ext-zip": "*",
|
||||
"php": "^7.3 || ^8.0",
|
||||
"symfony/polyfill-mbstring": "^1.12",
|
||||
"symfony/process": "^5.0 || ^6.0 || ^7.0 || ^8.0"
|
||||
},
|
||||
"replace": {
|
||||
"facebook/webdriver": "*"
|
||||
},
|
||||
"require-dev": {
|
||||
"ergebnis/composer-normalize": "^2.20.0",
|
||||
"ondram/ci-detector": "^4.0",
|
||||
"php-coveralls/php-coveralls": "^2.4",
|
||||
"php-mock/php-mock-phpunit": "^2.0",
|
||||
"php-parallel-lint/php-parallel-lint": "^1.2",
|
||||
"phpunit/phpunit": "^9.3",
|
||||
"squizlabs/php_codesniffer": "^3.5",
|
||||
"symfony/var-dumper": "^5.0 || ^6.0 || ^7.0 || ^8.0"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-simplexml": "For Firefox profile creation"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"files": [
|
||||
"lib/Exception/TimeoutException.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Facebook\\WebDriver\\": "lib/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"description": "A PHP client for Selenium WebDriver. Previously facebook/webdriver.",
|
||||
"homepage": "https://github.com/php-webdriver/php-webdriver",
|
||||
"keywords": [
|
||||
"Chromedriver",
|
||||
"geckodriver",
|
||||
"php",
|
||||
"selenium",
|
||||
"webdriver"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/php-webdriver/php-webdriver/issues",
|
||||
"source": "https://github.com/php-webdriver/php-webdriver/tree/1.16.0"
|
||||
},
|
||||
"time": "2025-12-28T23:57:40+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpstan/phpstan",
|
||||
"version": "2.2.8",
|
||||
|
||||
+2
-2
@@ -193,8 +193,8 @@ return [
|
||||
*/
|
||||
|
||||
'maintenance' => [
|
||||
'driver' => 'cache',
|
||||
'store' => 'redis',
|
||||
'driver' => env('APP_MAINTENANCE_DRIVER', 'cache'),
|
||||
'store' => env('APP_MAINTENANCE_STORE', 'redis'),
|
||||
],
|
||||
|
||||
/*
|
||||
|
||||
@@ -116,25 +116,9 @@
|
||||
<p class="text-[13px] leading-5 text-neutral-500 dark:text-fg-dim">
|
||||
Mount a Docker volume inside the container.
|
||||
</p>
|
||||
@if ($isSwarm)
|
||||
<div class="text-warning">Swarm Mode detected: You need to set a shared
|
||||
volume
|
||||
(EFS/NFS/etc) on all the worker nodes if you would like to use a
|
||||
persistent
|
||||
volumes.</div>
|
||||
@endif
|
||||
<div class="flex flex-col gap-4">
|
||||
<x-forms.input canGate="update" :canResource="$resource" placeholder="pv-name"
|
||||
id="name" label="Name" required helper="Volume name." />
|
||||
@if ($isSwarm)
|
||||
<x-forms.input canGate="update" :canResource="$resource"
|
||||
placeholder="/root" id="host_path" label="Source Path" required
|
||||
helper="Directory on the host system." />
|
||||
@else
|
||||
<x-forms.input canGate="update" :canResource="$resource"
|
||||
placeholder="/root" id="host_path" label="Source Path"
|
||||
helper="Directory on the host system." />
|
||||
@endif
|
||||
<x-forms.input canGate="update" :canResource="$resource"
|
||||
placeholder="/tmp/root" id="mount_path" label="Destination Path"
|
||||
required helper="Directory inside the container." />
|
||||
|
||||
@@ -154,7 +154,24 @@
|
||||
|
||||
<div class="volumes-col-source min-w-0">
|
||||
<span class="volumes-mobile-label volumes-field-label">Source Path</span>
|
||||
<x-forms.input id="forms.{{ $id }}.hostPath" placeholder="Host path (optional)" />
|
||||
@if (filled($form['hostPath']))
|
||||
<div class="flex items-center gap-1.5">
|
||||
<div class="min-w-0 flex-1">
|
||||
<x-forms.input id="forms.{{ $id }}.hostPath" />
|
||||
</div>
|
||||
<x-modal-confirmation title="Remove Source Path?" isErrorButton
|
||||
canGate="update" :canResource="$resource"
|
||||
buttonTitle="Remove" submitAction="clearHostPath({{ $id }})"
|
||||
:actions="[
|
||||
'Are you sure you want to remove the source path?',
|
||||
'The next deployment will use a named Docker volume instead.',
|
||||
'Data from the existing host directory will not be copied to the named volume.',
|
||||
'Use a Directory Mount when you need to mount a host directory.',
|
||||
]" />
|
||||
</div>
|
||||
@else
|
||||
<span class="data-table-cell-dash">-</span>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="volumes-cell-dest min-w-0">
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Browser;
|
||||
|
||||
use Laravel\Dusk\Browser;
|
||||
use Tests\DuskTestCase;
|
||||
use Throwable;
|
||||
|
||||
class LoginTest extends DuskTestCase
|
||||
{
|
||||
/**
|
||||
* A basic test for the login page.
|
||||
* Login with the test user and assert that the user is redirected to the dashboard.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function test_login()
|
||||
{
|
||||
$this->browse(callback: function (Browser $browser) {
|
||||
$browser->loginWithRootUser()
|
||||
->assertPathIs('/')
|
||||
->assertSee('Dashboard');
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Browser;
|
||||
|
||||
use Laravel\Dusk\Browser;
|
||||
use Tests\DuskTestCase;
|
||||
use Throwable;
|
||||
|
||||
class ProjectAddNewTest extends DuskTestCase
|
||||
{
|
||||
/**
|
||||
* A basic test for the projects page.
|
||||
* Login with the test user and assert that the user is redirected to the projects page.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function test_login()
|
||||
{
|
||||
$this->browse(function (Browser $browser) {
|
||||
$browser->loginWithRootUser()
|
||||
->visit('/projects')
|
||||
->pressAndWaitFor('+ Add', 1)
|
||||
->assertSee('New Project')
|
||||
->screenshot('project-add-new-1')
|
||||
->type('name', 'Test Project')
|
||||
->screenshot('project-add-new-2')
|
||||
->press('Continue')
|
||||
->assertSee('Test Project.')
|
||||
->screenshot('project-add-new-3');
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Browser;
|
||||
|
||||
use Laravel\Dusk\Browser;
|
||||
use Tests\DuskTestCase;
|
||||
use Throwable;
|
||||
|
||||
class ProjectSearchTest extends DuskTestCase
|
||||
{
|
||||
/**
|
||||
* A basic test for the projects page.
|
||||
* Login with the test user and assert that the user is redirected to the projects page.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function test_login()
|
||||
{
|
||||
$this->browse(function (Browser $browser) {
|
||||
$browser->loginWithRootUser()
|
||||
->visit('/projects')
|
||||
->type('[x-model="search"]', 'joi43j4oi32j4o2')
|
||||
->assertSee('No project found with the search term "joi43j4oi32j4o2".')
|
||||
->screenshot('project-search-not-found');
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Browser;
|
||||
|
||||
use Laravel\Dusk\Browser;
|
||||
use Tests\DuskTestCase;
|
||||
use Throwable;
|
||||
|
||||
class ProjectTest extends DuskTestCase
|
||||
{
|
||||
/**
|
||||
* A basic test for the projects page.
|
||||
* Login with the test user and assert that the user is redirected to the projects page.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function test_login()
|
||||
{
|
||||
$this->browse(function (Browser $browser) {
|
||||
$browser->loginWithRootUser()
|
||||
->visit('/projects')
|
||||
->assertSee('Projects');
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
*
|
||||
!.gitignore
|
||||
@@ -1,2 +0,0 @@
|
||||
*
|
||||
!.gitignore
|
||||
@@ -1,57 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tests;
|
||||
|
||||
use Facebook\WebDriver\Chrome\ChromeOptions;
|
||||
use Facebook\WebDriver\Remote\DesiredCapabilities;
|
||||
use Facebook\WebDriver\Remote\RemoteWebDriver;
|
||||
use Illuminate\Support\Collection;
|
||||
use Laravel\Dusk\TestCase as BaseTestCase;
|
||||
|
||||
abstract class DuskTestCase extends BaseTestCase
|
||||
{
|
||||
use CreatesApplication;
|
||||
|
||||
/**
|
||||
* Prepare for Dusk test execution.
|
||||
*
|
||||
* @beforeClass
|
||||
*/
|
||||
public static function prepare(): void
|
||||
{
|
||||
if (! static::runningInSail()) {
|
||||
static::startChromeDriver();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the RemoteWebDriver instance.
|
||||
*/
|
||||
protected function driver(): RemoteWebDriver
|
||||
{
|
||||
$options = (new ChromeOptions)->addArguments(collect([
|
||||
$this->shouldStartMaximized() ? '--start-maximized' : '--window-size=1920,1080',
|
||||
])->unless($this->hasHeadlessDisabled(), function (Collection $items) {
|
||||
return $items->merge([
|
||||
'--disable-gpu',
|
||||
'--headless=new',
|
||||
]);
|
||||
})->all());
|
||||
|
||||
return RemoteWebDriver::create(
|
||||
'http://localhost:4444',
|
||||
DesiredCapabilities::chrome()->setCapability(
|
||||
ChromeOptions::CAPABILITY,
|
||||
$options
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the browser window should start maximized.
|
||||
*/
|
||||
protected function baseUrl()
|
||||
{
|
||||
return 'http://localhost:8000';
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,7 @@ it('keeps storage backup schedule tables horizontally scrollable on mobile', fun
|
||||
->and($css)->toMatch('/\.backup-table-grid\s*\{[^}]*min-width:\s*50rem;/');
|
||||
});
|
||||
|
||||
use App\Livewire\Project\Service\Storage;
|
||||
use App\Livewire\Project\Service\VolumeBackup\Create as CreateServiceVolumeBackup;
|
||||
use App\Livewire\Project\Shared\Storages\All;
|
||||
use App\Models\Application;
|
||||
@@ -206,6 +207,62 @@ it('renders volumes as a data table with shared column headers', function () {
|
||||
->toMatch('/\.application-settings-form label\s*\{[^}]*font-size:\s*13px/s');
|
||||
});
|
||||
|
||||
it('keeps bind mount source paths out of the add volume form', function () {
|
||||
$storageView = file_get_contents(resource_path('views/livewire/project/service/storage.blade.php'));
|
||||
$volumesView = file_get_contents(resource_path('views/livewire/project/shared/storages/all.blade.php'));
|
||||
|
||||
expect($storageView)
|
||||
->not->toContain('id="host_path"')
|
||||
->not->toContain('Swarm Mode detected')
|
||||
->and($volumesView)
|
||||
->toMatch('/<x-modal-confirmation title="Remove Source Path\?"[^>]*canGate="update"[^>]*:canResource="\$resource"/')
|
||||
->toContain('The next deployment will use a named Docker volume instead.')
|
||||
->toContain('Data from the existing host directory will not be copied to the named volume.');
|
||||
});
|
||||
|
||||
it('creates named volumes without a host path in swarm mode', function () {
|
||||
[$application] = createApplicationWithVolume();
|
||||
$application->persistentStorages()->delete();
|
||||
|
||||
Livewire::test(Storage::class, ['resource' => $application])
|
||||
->set('isSwarm', true)
|
||||
->set('name', 'storage-app-data')
|
||||
->set('mount_path', '/data')
|
||||
->call('submitPersistentVolume')
|
||||
->assertHasNoErrors();
|
||||
|
||||
expect($application->persistentStorages()->first())
|
||||
->name->toBe($application->uuid.'-storage-app-data')
|
||||
->host_path->toBeNull();
|
||||
});
|
||||
|
||||
it('uses a resource based default name for new volumes', function () {
|
||||
[$application] = createApplicationWithVolume(['name' => 'Storage App']);
|
||||
|
||||
Livewire::test(Storage::class, ['resource' => $application])
|
||||
->assertSet('name', 'storage-app-data');
|
||||
});
|
||||
|
||||
it('uses a valid fallback default volume name when the resource name has no slug characters', function () {
|
||||
[$application] = createApplicationWithVolume(['name' => '---']);
|
||||
|
||||
Livewire::test(Storage::class, ['resource' => $application])
|
||||
->assertSet('name', 'volume-data');
|
||||
});
|
||||
|
||||
it('removes existing bind mount source paths from the volume table', function () {
|
||||
[$application, $volume] = createApplicationWithVolume(volumeAttributes: [
|
||||
'host_path' => '/srv/storage',
|
||||
]);
|
||||
|
||||
Livewire::test(All::class, ['resource' => $application])
|
||||
->assertSet("forms.{$volume->id}.hostPath", '/srv/storage')
|
||||
->call('clearHostPath', $volume->id)
|
||||
->assertHasNoErrors();
|
||||
|
||||
expect($volume->refresh()->host_path)->toBeNull();
|
||||
});
|
||||
|
||||
it('creates and exposes volume backups for service storage', function () {
|
||||
$service = Service::factory()->create([
|
||||
'environment_id' => $this->environment->id,
|
||||
|
||||
Reference in New Issue
Block a user