fix(database): refresh SSL/status state and harden clone writes
Handle database status updates more reliably by listening for `ServiceChecked` and using explicit `refresh()` handlers in Livewire database components. Also switch guarded clone/create paths to `forceFill`/`forceCreate` in helper flows to avoid missing persisted attributes during app/service cloning. Update log/terminal font stacks to Geist (with bundled variable fonts) and add coverage for SSL status refresh, persistent volume UUID cloning, and log font styling.
This commit is contained in:
parent
1efed14169
commit
2692496726
24 changed files with 333 additions and 33 deletions
|
|
@ -57,7 +57,8 @@ public function getListeners()
|
|||
|
||||
return [
|
||||
"echo-private:team.{$teamId},DatabaseProxyStopped" => 'databaseProxyStopped',
|
||||
"echo-private:user.{$userId},DatabaseStatusChanged" => '$refresh',
|
||||
"echo-private:user.{$userId},DatabaseStatusChanged" => 'refresh',
|
||||
"echo-private:team.{$teamId},ServiceChecked" => 'refresh',
|
||||
];
|
||||
}
|
||||
|
||||
|
|
@ -299,4 +300,10 @@ public function regenerateSslCertificate()
|
|||
handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function refresh(): void
|
||||
{
|
||||
$this->database->refresh();
|
||||
$this->syncData();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,7 +59,8 @@ public function getListeners()
|
|||
|
||||
return [
|
||||
"echo-private:team.{$teamId},DatabaseProxyStopped" => 'databaseProxyStopped',
|
||||
"echo-private:user.{$userId},DatabaseStatusChanged" => '$refresh',
|
||||
"echo-private:user.{$userId},DatabaseStatusChanged" => 'refresh',
|
||||
"echo-private:team.{$teamId},ServiceChecked" => 'refresh',
|
||||
];
|
||||
}
|
||||
|
||||
|
|
@ -304,4 +305,10 @@ public function regenerateSslCertificate()
|
|||
handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function refresh(): void
|
||||
{
|
||||
$this->database->refresh();
|
||||
$this->syncData();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,9 +61,11 @@ class General extends Component
|
|||
public function getListeners()
|
||||
{
|
||||
$userId = Auth::id();
|
||||
$teamId = Auth::user()->currentTeam()->id;
|
||||
|
||||
return [
|
||||
"echo-private:user.{$userId},DatabaseStatusChanged" => '$refresh',
|
||||
"echo-private:user.{$userId},DatabaseStatusChanged" => 'refresh',
|
||||
"echo-private:team.{$teamId},ServiceChecked" => 'refresh',
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -61,9 +61,11 @@ class General extends Component
|
|||
public function getListeners()
|
||||
{
|
||||
$userId = Auth::id();
|
||||
$teamId = Auth::user()->currentTeam()->id;
|
||||
|
||||
return [
|
||||
"echo-private:user.{$userId},DatabaseStatusChanged" => '$refresh',
|
||||
"echo-private:user.{$userId},DatabaseStatusChanged" => 'refresh',
|
||||
"echo-private:team.{$teamId},ServiceChecked" => 'refresh',
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -63,9 +63,11 @@ class General extends Component
|
|||
public function getListeners()
|
||||
{
|
||||
$userId = Auth::id();
|
||||
$teamId = Auth::user()->currentTeam()->id;
|
||||
|
||||
return [
|
||||
"echo-private:user.{$userId},DatabaseStatusChanged" => '$refresh',
|
||||
"echo-private:user.{$userId},DatabaseStatusChanged" => 'refresh',
|
||||
"echo-private:team.{$teamId},ServiceChecked" => 'refresh',
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -71,9 +71,11 @@ class General extends Component
|
|||
public function getListeners()
|
||||
{
|
||||
$userId = Auth::id();
|
||||
$teamId = Auth::user()->currentTeam()->id;
|
||||
|
||||
return [
|
||||
"echo-private:user.{$userId},DatabaseStatusChanged" => '$refresh',
|
||||
"echo-private:user.{$userId},DatabaseStatusChanged" => 'refresh',
|
||||
"echo-private:team.{$teamId},ServiceChecked" => 'refresh',
|
||||
'save_init_script',
|
||||
'delete_init_script',
|
||||
];
|
||||
|
|
@ -488,4 +490,10 @@ public function submit()
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function refresh(): void
|
||||
{
|
||||
$this->database->refresh();
|
||||
$this->syncData();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,9 +59,11 @@ class General extends Component
|
|||
public function getListeners()
|
||||
{
|
||||
$userId = Auth::id();
|
||||
$teamId = Auth::user()->currentTeam()->id;
|
||||
|
||||
return [
|
||||
"echo-private:user.{$userId},DatabaseStatusChanged" => '$refresh',
|
||||
"echo-private:user.{$userId},DatabaseStatusChanged" => 'refresh',
|
||||
"echo-private:team.{$teamId},ServiceChecked" => 'refresh',
|
||||
'envsUpdated' => 'refresh',
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -237,10 +237,11 @@ function clone_application(Application $source, $destination, array $overrides =
|
|||
'id',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
])->fill([
|
||||
])->forceFill([
|
||||
'application_id' => $newApplication->id,
|
||||
]);
|
||||
$newApplicationSettings->save();
|
||||
$newApplication->setRelation('settings', $newApplicationSettings->fresh());
|
||||
}
|
||||
|
||||
// Clone tags
|
||||
|
|
@ -256,7 +257,7 @@ function clone_application(Application $source, $destination, array $overrides =
|
|||
'id',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
])->fill([
|
||||
])->forceFill([
|
||||
'uuid' => (string) new Cuid2,
|
||||
'application_id' => $newApplication->id,
|
||||
'team_id' => currentTeam()->id,
|
||||
|
|
@ -271,7 +272,7 @@ function clone_application(Application $source, $destination, array $overrides =
|
|||
'id',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
])->fill([
|
||||
])->forceFill([
|
||||
'uuid' => (string) new Cuid2,
|
||||
'application_id' => $newApplication->id,
|
||||
'status' => 'exited',
|
||||
|
|
@ -303,7 +304,7 @@ function clone_application(Application $source, $destination, array $overrides =
|
|||
'created_at',
|
||||
'updated_at',
|
||||
'uuid',
|
||||
])->fill([
|
||||
])->forceFill([
|
||||
'name' => $newName,
|
||||
'resource_id' => $newApplication->id,
|
||||
]);
|
||||
|
|
@ -339,7 +340,7 @@ function clone_application(Application $source, $destination, array $overrides =
|
|||
'id',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
])->fill([
|
||||
])->forceFill([
|
||||
'resource_id' => $newApplication->id,
|
||||
]);
|
||||
$newStorage->save();
|
||||
|
|
@ -353,7 +354,7 @@ function clone_application(Application $source, $destination, array $overrides =
|
|||
'id',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
])->fill([
|
||||
])->forceFill([
|
||||
'resourceable_id' => $newApplication->id,
|
||||
'resourceable_type' => $newApplication->getMorphClass(),
|
||||
'is_preview' => false,
|
||||
|
|
@ -370,7 +371,7 @@ function clone_application(Application $source, $destination, array $overrides =
|
|||
'id',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
])->fill([
|
||||
])->forceFill([
|
||||
'resourceable_id' => $newApplication->id,
|
||||
'resourceable_type' => $newApplication->getMorphClass(),
|
||||
'is_preview' => true,
|
||||
|
|
|
|||
|
|
@ -1919,7 +1919,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
|
|||
// Create new serviceApplication or serviceDatabase
|
||||
if ($isDatabase) {
|
||||
if ($isNew) {
|
||||
$savedService = ServiceDatabase::create([
|
||||
$savedService = ServiceDatabase::forceCreate([
|
||||
'name' => $serviceName,
|
||||
'image' => $image,
|
||||
'service_id' => $resource->id,
|
||||
|
|
@ -1930,7 +1930,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
|
|||
'service_id' => $resource->id,
|
||||
])->first();
|
||||
if (is_null($savedService)) {
|
||||
$savedService = ServiceDatabase::create([
|
||||
$savedService = ServiceDatabase::forceCreate([
|
||||
'name' => $serviceName,
|
||||
'image' => $image,
|
||||
'service_id' => $resource->id,
|
||||
|
|
@ -1939,7 +1939,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
|
|||
}
|
||||
} else {
|
||||
if ($isNew) {
|
||||
$savedService = ServiceApplication::create([
|
||||
$savedService = ServiceApplication::forceCreate([
|
||||
'name' => $serviceName,
|
||||
'image' => $image,
|
||||
'service_id' => $resource->id,
|
||||
|
|
@ -1950,7 +1950,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
|
|||
'service_id' => $resource->id,
|
||||
])->first();
|
||||
if (is_null($savedService)) {
|
||||
$savedService = ServiceApplication::create([
|
||||
$savedService = ServiceApplication::forceCreate([
|
||||
'name' => $serviceName,
|
||||
'image' => $image,
|
||||
'service_id' => $resource->id,
|
||||
|
|
|
|||
30
openapi.json
30
openapi.json
|
|
@ -4331,6 +4331,11 @@
|
|||
"database_backup_retention_max_storage_s3": {
|
||||
"type": "integer",
|
||||
"description": "Max storage (MB) for S3 backups"
|
||||
},
|
||||
"timeout": {
|
||||
"type": "integer",
|
||||
"description": "Backup job timeout in seconds (min: 60, max: 36000)",
|
||||
"default": 3600
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
|
|
@ -4896,6 +4901,11 @@
|
|||
"database_backup_retention_max_storage_s3": {
|
||||
"type": "integer",
|
||||
"description": "Max storage of the backup in S3"
|
||||
},
|
||||
"timeout": {
|
||||
"type": "integer",
|
||||
"description": "Backup job timeout in seconds (min: 60, max: 36000)",
|
||||
"default": 3600
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
|
|
@ -10451,6 +10461,26 @@
|
|||
"none"
|
||||
],
|
||||
"description": "The proxy type."
|
||||
},
|
||||
"concurrent_builds": {
|
||||
"type": "integer",
|
||||
"description": "Number of concurrent builds."
|
||||
},
|
||||
"dynamic_timeout": {
|
||||
"type": "integer",
|
||||
"description": "Deployment timeout in seconds."
|
||||
},
|
||||
"deployment_queue_limit": {
|
||||
"type": "integer",
|
||||
"description": "Maximum number of queued deployments."
|
||||
},
|
||||
"server_disk_usage_notification_threshold": {
|
||||
"type": "integer",
|
||||
"description": "Server disk usage notification threshold (%)."
|
||||
},
|
||||
"server_disk_usage_check_frequency": {
|
||||
"type": "string",
|
||||
"description": "Cron expression for disk usage check frequency."
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
|
|
|
|||
23
openapi.yaml
23
openapi.yaml
|
|
@ -2734,6 +2734,10 @@ paths:
|
|||
database_backup_retention_max_storage_s3:
|
||||
type: integer
|
||||
description: 'Max storage (MB) for S3 backups'
|
||||
timeout:
|
||||
type: integer
|
||||
description: 'Backup job timeout in seconds (min: 60, max: 36000)'
|
||||
default: 3600
|
||||
type: object
|
||||
responses:
|
||||
'201':
|
||||
|
|
@ -3125,6 +3129,10 @@ paths:
|
|||
database_backup_retention_max_storage_s3:
|
||||
type: integer
|
||||
description: 'Max storage of the backup in S3'
|
||||
timeout:
|
||||
type: integer
|
||||
description: 'Backup job timeout in seconds (min: 60, max: 36000)'
|
||||
default: 3600
|
||||
type: object
|
||||
responses:
|
||||
'200':
|
||||
|
|
@ -6669,6 +6677,21 @@ paths:
|
|||
type: string
|
||||
enum: [traefik, caddy, none]
|
||||
description: 'The proxy type.'
|
||||
concurrent_builds:
|
||||
type: integer
|
||||
description: 'Number of concurrent builds.'
|
||||
dynamic_timeout:
|
||||
type: integer
|
||||
description: 'Deployment timeout in seconds.'
|
||||
deployment_queue_limit:
|
||||
type: integer
|
||||
description: 'Maximum number of queued deployments.'
|
||||
server_disk_usage_notification_threshold:
|
||||
type: integer
|
||||
description: 'Server disk usage notification threshold (%).'
|
||||
server_disk_usage_check_frequency:
|
||||
type: string
|
||||
description: 'Cron expression for disk usage check frequency.'
|
||||
type: object
|
||||
responses:
|
||||
'201':
|
||||
|
|
|
|||
|
|
@ -14,7 +14,9 @@
|
|||
@custom-variant dark (&:where(.dark, .dark *));
|
||||
|
||||
@theme {
|
||||
--font-sans: Inter, sans-serif;
|
||||
--font-sans: 'Geist Sans', Inter, sans-serif;
|
||||
--font-geist-sans: 'Geist Sans', Inter, sans-serif;
|
||||
--font-logs: 'Geist Mono', 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
||||
|
||||
--color-base: #101010;
|
||||
--color-warning: #fcd452;
|
||||
|
|
@ -96,7 +98,7 @@ body {
|
|||
}
|
||||
|
||||
body {
|
||||
@apply min-h-screen text-sm antialiased scrollbar overflow-x-hidden;
|
||||
@apply min-h-screen text-sm font-sans antialiased scrollbar overflow-x-hidden;
|
||||
}
|
||||
|
||||
.coolify-monaco-editor {
|
||||
|
|
|
|||
|
|
@ -70,3 +70,18 @@ @font-face {
|
|||
src: url('../fonts/inter-v13-cyrillic_cyrillic-ext_greek_greek-ext_latin_latin-ext_vietnamese-regular.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-display: swap;
|
||||
font-family: 'Geist Mono';
|
||||
font-style: normal;
|
||||
font-weight: 100 900;
|
||||
src: url('../fonts/geist-mono-variable.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-display: swap;
|
||||
font-family: 'Geist Sans';
|
||||
font-style: normal;
|
||||
font-weight: 100 900;
|
||||
src: url('../fonts/geist-sans-variable.woff2') format('woff2');
|
||||
}
|
||||
|
|
|
|||
BIN
resources/fonts/geist-mono-variable.woff2
Normal file
BIN
resources/fonts/geist-mono-variable.woff2
Normal file
Binary file not shown.
BIN
resources/fonts/geist-sans-variable.woff2
Normal file
BIN
resources/fonts/geist-sans-variable.woff2
Normal file
Binary file not shown.
|
|
@ -186,7 +186,7 @@ export function initializeTerminalComponent() {
|
|||
this.term = new Terminal({
|
||||
cols: 80,
|
||||
rows: 30,
|
||||
fontFamily: '"Fira Code", courier-new, courier, monospace, "Powerline Extra Symbols"',
|
||||
fontFamily: '"Geist Mono", "SFMono-Regular", Menlo, Monaco, Consolas, "Liberation Mono", monospace, "Powerline Extra Symbols"',
|
||||
cursorBlink: true,
|
||||
rendererType: 'canvas',
|
||||
convertEol: true,
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@
|
|||
'flex-1 min-h-0' => $fullHeight,
|
||||
'max-h-96' => !$fullHeight,
|
||||
])>
|
||||
<pre class="font-mono whitespace-pre-wrap" @if ($isPollingActive) wire:poll.1000ms="polling" @endif>{{ RunRemoteProcess::decodeOutput($activity) }}</pre>
|
||||
<pre class="font-logs whitespace-pre-wrap" @if ($isPollingActive) wire:poll.1000ms="polling" @endif>{{ RunRemoteProcess::decodeOutput($activity) }}</pre>
|
||||
</div>
|
||||
@else
|
||||
@if ($showWaiting)
|
||||
|
|
|
|||
|
|
@ -330,7 +330,7 @@ class="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-
|
|||
<div id="logsContainer"
|
||||
class="flex flex-col overflow-y-auto p-2 px-4 min-h-4 scrollbar"
|
||||
:class="fullscreen ? 'flex-1' : 'max-h-[30rem]'">
|
||||
<div id="logs" class="flex flex-col font-mono">
|
||||
<div id="logs" class="flex flex-col font-logs">
|
||||
<div x-show="searchQuery.trim() && matchCount === 0"
|
||||
class="text-gray-500 dark:text-gray-400 py-2">
|
||||
No matches found.
|
||||
|
|
@ -356,7 +356,7 @@ class="shrink-0 text-gray-500">{{ $line['timestamp'] }}</span>
|
|||
])>{{ $lineContent }}</span>
|
||||
</div>
|
||||
@empty
|
||||
<span class="font-mono text-neutral-400 mb-2">No logs yet.</span>
|
||||
<span class="font-logs text-neutral-400 mb-2">No logs yet.</span>
|
||||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -480,7 +480,7 @@ class="flex overflow-y-auto overflow-x-hidden flex-col px-4 py-2 w-full min-w-0
|
|||
@php
|
||||
$displayLines = collect(explode("\n", $outputs))->filter(fn($line) => trim($line) !== '');
|
||||
@endphp
|
||||
<div id="logs" class="font-mono max-w-full cursor-default">
|
||||
<div id="logs" class="font-logs max-w-full cursor-default">
|
||||
<div x-show="searchQuery.trim() && matchCount === 0"
|
||||
class="text-gray-500 dark:text-gray-400 py-2">
|
||||
No matches found.
|
||||
|
|
@ -518,7 +518,7 @@ class="text-gray-500 dark:text-gray-400 py-2">
|
|||
</div>
|
||||
@else
|
||||
<pre id="logs"
|
||||
class="font-mono whitespace-pre-wrap break-all max-w-full text-neutral-400">No logs yet.</pre>
|
||||
class="font-logs whitespace-pre-wrap break-all max-w-full text-neutral-400">No logs yet.</pre>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@
|
|||
<svg class="h-5 w-5 shrink-0 text-gray-500 dark:text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<code class="flex-1 text-sm font-mono text-gray-700 dark:text-gray-300">{{ data_get($result, 'command') }}</code>
|
||||
<code class="flex-1 text-sm font-logs text-gray-700 dark:text-gray-300">{{ data_get($result, 'command') }}</code>
|
||||
</div>
|
||||
@php
|
||||
$output = data_get($result, 'output');
|
||||
|
|
@ -108,7 +108,7 @@
|
|||
@endphp
|
||||
<div class="p-4">
|
||||
@if($hasOutput)
|
||||
<pre class="font-mono text-sm text-gray-600 dark:text-gray-300 whitespace-pre-wrap">{{ $output }}</pre>
|
||||
<pre class="font-logs text-sm text-gray-600 dark:text-gray-300 whitespace-pre-wrap">{{ $output }}</pre>
|
||||
@else
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400 italic">
|
||||
No output returned - command completed successfully
|
||||
|
|
|
|||
|
|
@ -1,15 +1,18 @@
|
|||
<?php
|
||||
|
||||
use App\Livewire\Project\Shared\ResourceOperations;
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationPreview;
|
||||
use App\Models\ApplicationSetting;
|
||||
use App\Models\Environment;
|
||||
use App\Models\LocalPersistentVolume;
|
||||
use App\Models\Project;
|
||||
use App\Models\ScheduledTask;
|
||||
use App\Models\Server;
|
||||
use App\Models\StandaloneDocker;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Livewire\Livewire;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->user = User::factory()->create();
|
||||
|
|
@ -17,7 +20,7 @@
|
|||
$this->user->teams()->attach($this->team, ['role' => 'owner']);
|
||||
|
||||
$this->server = Server::factory()->create(['team_id' => $this->team->id]);
|
||||
$this->destination = StandaloneDocker::factory()->create(['server_id' => $this->server->id]);
|
||||
$this->destination = $this->server->standaloneDockers()->firstOrFail();
|
||||
$this->project = Project::factory()->create(['team_id' => $this->team->id]);
|
||||
$this->environment = Environment::factory()->create(['project_id' => $this->project->id]);
|
||||
|
||||
|
|
@ -25,8 +28,13 @@
|
|||
'environment_id' => $this->environment->id,
|
||||
'destination_id' => $this->destination->id,
|
||||
'destination_type' => $this->destination->getMorphClass(),
|
||||
'redirect' => 'both',
|
||||
]);
|
||||
|
||||
$this->application->settings->forceFill([
|
||||
'is_container_label_readonly_enabled' => false,
|
||||
])->save();
|
||||
|
||||
$this->actingAs($this->user);
|
||||
session(['currentTeam' => $this->team]);
|
||||
});
|
||||
|
|
@ -82,3 +90,71 @@
|
|||
expect($clonedUuids)->each->not->toBeIn($originalUuids);
|
||||
expect(array_unique($clonedUuids))->toHaveCount(2);
|
||||
});
|
||||
|
||||
test('cloning application reassigns settings to the cloned application', function () {
|
||||
$this->application->settings->forceFill([
|
||||
'is_static' => true,
|
||||
'is_spa' => true,
|
||||
'is_build_server_enabled' => true,
|
||||
])->save();
|
||||
|
||||
$newApp = clone_application($this->application, $this->destination, [
|
||||
'environment_id' => $this->environment->id,
|
||||
]);
|
||||
|
||||
$sourceSettingsCount = ApplicationSetting::query()
|
||||
->where('application_id', $this->application->id)
|
||||
->count();
|
||||
$clonedSettings = ApplicationSetting::query()
|
||||
->where('application_id', $newApp->id)
|
||||
->first();
|
||||
|
||||
expect($sourceSettingsCount)->toBe(1)
|
||||
->and($clonedSettings)->not->toBeNull()
|
||||
->and($clonedSettings?->application_id)->toBe($newApp->id)
|
||||
->and($clonedSettings?->is_static)->toBeTrue()
|
||||
->and($clonedSettings?->is_spa)->toBeTrue()
|
||||
->and($clonedSettings?->is_build_server_enabled)->toBeTrue();
|
||||
});
|
||||
|
||||
test('cloning application reassigns scheduled tasks and previews to the cloned application', function () {
|
||||
$scheduledTask = ScheduledTask::forceCreate([
|
||||
'uuid' => 'scheduled-task-original',
|
||||
'application_id' => $this->application->id,
|
||||
'team_id' => $this->team->id,
|
||||
'name' => 'nightly-task',
|
||||
'command' => 'php artisan schedule:run',
|
||||
'frequency' => '* * * * *',
|
||||
'container' => 'app',
|
||||
'timeout' => 120,
|
||||
]);
|
||||
|
||||
$preview = ApplicationPreview::forceCreate([
|
||||
'uuid' => 'preview-original',
|
||||
'application_id' => $this->application->id,
|
||||
'pull_request_id' => 123,
|
||||
'pull_request_html_url' => 'https://example.com/pull/123',
|
||||
'fqdn' => 'https://preview.example.com',
|
||||
'status' => 'running',
|
||||
]);
|
||||
|
||||
$newApp = clone_application($this->application, $this->destination, [
|
||||
'environment_id' => $this->environment->id,
|
||||
]);
|
||||
|
||||
$clonedTask = ScheduledTask::query()
|
||||
->where('application_id', $newApp->id)
|
||||
->first();
|
||||
$clonedPreview = ApplicationPreview::query()
|
||||
->where('application_id', $newApp->id)
|
||||
->first();
|
||||
|
||||
expect($clonedTask)->not->toBeNull()
|
||||
->and($clonedTask?->uuid)->not->toBe($scheduledTask->uuid)
|
||||
->and($clonedTask?->application_id)->toBe($newApp->id)
|
||||
->and($clonedTask?->team_id)->toBe($this->team->id)
|
||||
->and($clonedPreview)->not->toBeNull()
|
||||
->and($clonedPreview?->uuid)->not->toBe($preview->uuid)
|
||||
->and($clonedPreview?->application_id)->toBe($newApp->id)
|
||||
->and($clonedPreview?->status)->toBe('exited');
|
||||
});
|
||||
|
|
|
|||
77
tests/Feature/DatabaseSslStatusRefreshTest.php
Normal file
77
tests/Feature/DatabaseSslStatusRefreshTest.php
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
<?php
|
||||
|
||||
use App\Livewire\Project\Database\Dragonfly\General as DragonflyGeneral;
|
||||
use App\Livewire\Project\Database\Keydb\General as KeydbGeneral;
|
||||
use App\Livewire\Project\Database\Mariadb\General as MariadbGeneral;
|
||||
use App\Livewire\Project\Database\Mongodb\General as MongodbGeneral;
|
||||
use App\Livewire\Project\Database\Mysql\General as MysqlGeneral;
|
||||
use App\Livewire\Project\Database\Postgresql\General as PostgresqlGeneral;
|
||||
use App\Livewire\Project\Database\Redis\General as RedisGeneral;
|
||||
use App\Models\Environment;
|
||||
use App\Models\Project;
|
||||
use App\Models\Server;
|
||||
use App\Models\StandaloneDocker;
|
||||
use App\Models\StandaloneMysql;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->team = Team::factory()->create();
|
||||
$this->user = User::factory()->create();
|
||||
$this->team->members()->attach($this->user->id, ['role' => 'owner']);
|
||||
|
||||
$this->actingAs($this->user);
|
||||
session(['currentTeam' => $this->team]);
|
||||
});
|
||||
|
||||
dataset('ssl-aware-database-general-components', [
|
||||
MysqlGeneral::class,
|
||||
MariadbGeneral::class,
|
||||
MongodbGeneral::class,
|
||||
RedisGeneral::class,
|
||||
PostgresqlGeneral::class,
|
||||
KeydbGeneral::class,
|
||||
DragonflyGeneral::class,
|
||||
]);
|
||||
|
||||
it('maps database status broadcasts to refresh for ssl-aware database general components', function (string $componentClass) {
|
||||
$component = app($componentClass);
|
||||
$listeners = $component->getListeners();
|
||||
|
||||
expect($listeners["echo-private:user.{$this->user->id},DatabaseStatusChanged"])->toBe('refresh')
|
||||
->and($listeners["echo-private:team.{$this->team->id},ServiceChecked"])->toBe('refresh');
|
||||
})->with('ssl-aware-database-general-components');
|
||||
|
||||
it('reloads the mysql database model when refreshing so ssl controls follow the latest status', function () {
|
||||
$server = Server::factory()->create(['team_id' => $this->team->id]);
|
||||
$destination = StandaloneDocker::where('server_id', $server->id)->first();
|
||||
$project = Project::factory()->create(['team_id' => $this->team->id]);
|
||||
$environment = Environment::factory()->create(['project_id' => $project->id]);
|
||||
|
||||
$database = StandaloneMysql::forceCreate([
|
||||
'name' => 'test-mysql',
|
||||
'image' => 'mysql:8',
|
||||
'mysql_root_password' => 'password',
|
||||
'mysql_user' => 'coolify',
|
||||
'mysql_password' => 'password',
|
||||
'mysql_database' => 'coolify',
|
||||
'status' => 'exited:unhealthy',
|
||||
'enable_ssl' => true,
|
||||
'is_log_drain_enabled' => false,
|
||||
'environment_id' => $environment->id,
|
||||
'destination_id' => $destination->id,
|
||||
'destination_type' => $destination->getMorphClass(),
|
||||
]);
|
||||
|
||||
$component = Livewire::test(MysqlGeneral::class, ['database' => $database])
|
||||
->assertDontSee('Database should be stopped to change this settings.');
|
||||
|
||||
$database->forceFill(['status' => 'running:healthy'])->save();
|
||||
|
||||
$component->call('refresh')
|
||||
->assertSee('Database should be stopped to change this settings.');
|
||||
});
|
||||
45
tests/Feature/LogFontStylingTest.php
Normal file
45
tests/Feature/LogFontStylingTest.php
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
|
||||
it('registers geist mono from a local asset for log surfaces', function () {
|
||||
$fontsCss = file_get_contents(resource_path('css/fonts.css'));
|
||||
$appCss = file_get_contents(resource_path('css/app.css'));
|
||||
$fontPath = resource_path('fonts/geist-mono-variable.woff2');
|
||||
$geistSansPath = resource_path('fonts/geist-sans-variable.woff2');
|
||||
|
||||
expect($fontsCss)
|
||||
->toContain("font-family: 'Geist Mono'")
|
||||
->toContain("url('../fonts/geist-mono-variable.woff2')")
|
||||
->toContain("font-family: 'Geist Sans'")
|
||||
->toContain("url('../fonts/geist-sans-variable.woff2')")
|
||||
->and($appCss)
|
||||
->toContain("--font-sans: 'Geist Sans', Inter, sans-serif")
|
||||
->toContain('@apply min-h-screen text-sm font-sans antialiased scrollbar overflow-x-hidden;')
|
||||
->toContain("--font-logs: 'Geist Mono'")
|
||||
->toContain("--font-geist-sans: 'Geist Sans'")
|
||||
->and($fontPath)
|
||||
->toBeFile()
|
||||
->and($geistSansPath)
|
||||
->toBeFile();
|
||||
});
|
||||
|
||||
it('uses geist mono for shared logs and terminal rendering', function () {
|
||||
$sharedLogsView = file_get_contents(resource_path('views/livewire/project/shared/get-logs.blade.php'));
|
||||
$deploymentLogsView = file_get_contents(resource_path('views/livewire/project/application/deployment/show.blade.php'));
|
||||
$activityMonitorView = file_get_contents(resource_path('views/livewire/activity-monitor.blade.php'));
|
||||
$dockerCleanupView = file_get_contents(resource_path('views/livewire/server/docker-cleanup-executions.blade.php'));
|
||||
$terminalClient = file_get_contents(resource_path('js/terminal.js'));
|
||||
|
||||
expect($sharedLogsView)
|
||||
->toContain('class="font-logs max-w-full cursor-default"')
|
||||
->toContain('class="font-logs whitespace-pre-wrap break-all max-w-full text-neutral-400"')
|
||||
->and($deploymentLogsView)
|
||||
->toContain('class="flex flex-col font-logs"')
|
||||
->toContain('class="font-logs text-neutral-400 mb-2"')
|
||||
->and($activityMonitorView)
|
||||
->toContain('<pre class="font-logs whitespace-pre-wrap"')
|
||||
->and($dockerCleanupView)
|
||||
->toContain('class="flex-1 text-sm font-logs text-gray-700 dark:text-gray-300"')
|
||||
->toContain('class="font-logs text-sm text-gray-600 dark:text-gray-300 whitespace-pre-wrap"')
|
||||
->and($terminalClient)
|
||||
->toContain('"Geist Mono"');
|
||||
});
|
||||
|
|
@ -41,7 +41,8 @@
|
|||
// The new code checks for null within the else block and creates only if needed
|
||||
expect($sharedFile)
|
||||
->toContain('if (is_null($savedService)) {')
|
||||
->toContain('$savedService = ServiceDatabase::create([');
|
||||
->toContain('$savedService = ServiceDatabase::forceCreate([')
|
||||
->toContain('$savedService = ServiceApplication::forceCreate([');
|
||||
});
|
||||
|
||||
it('verifies image update logic is present in parseDockerComposeFile', function () {
|
||||
|
|
|
|||
Loading…
Reference in a new issue