mirror of
https://github.com/tiennm99/coolify.git
synced 2026-10-18 08:14:35 +00:00
fix(v5): backfill missing canvas uuid columns
Add guarded migration for existing v5 application, cluster, and resource connection tables missing uuid columns. Add Vitest, jsdom, and React Testing Library coverage for optimistic updates, canvas hooks, ingress, resource merging, viewport behavior, and v5 browser canvas and cluster flows.
This commit is contained in:
1 parent
fbc6fd5ce5
commit
a9501d5476
10 files changed
+2234
-28
No files matched your search
@@ -0,0 +1,164 @@
|
||||
<?php
|
||||
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Project;
|
||||
use App\Models\User;
|
||||
use App\Models\V5\Application as V5Application;
|
||||
use App\Models\V5\ApplicationDomain as V5ApplicationDomain;
|
||||
use App\Models\V5\Server as V5Server;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
Config::set('broadcasting.default', 'log');
|
||||
InstanceSettings::create(['id' => 0, 'is_sponsorship_popup_enabled' => false]);
|
||||
|
||||
$this->user = User::factory()->create([
|
||||
'name' => 'Root User',
|
||||
'email' => 'test@example.com',
|
||||
'password' => Hash::make('password'),
|
||||
]);
|
||||
$this->team = $this->user->teams()->firstOrFail();
|
||||
$this->project = Project::create([
|
||||
'name' => 'Canvas Project',
|
||||
'team_id' => $this->team->id,
|
||||
]);
|
||||
$this->environment = $this->project->environments()->firstOrFail();
|
||||
});
|
||||
|
||||
function createCanvasV5Server(array $attributes = []): V5Server
|
||||
{
|
||||
return V5Server::query()->create([
|
||||
'team_id' => test()->team->id,
|
||||
'created_by_user_id' => test()->user->id,
|
||||
'name' => 'edge-01',
|
||||
'host' => '203.0.113.10',
|
||||
'ssh_user' => 'root',
|
||||
'ssh_port' => 22,
|
||||
'status' => 'installed',
|
||||
'last_bootstrapped_at' => now(),
|
||||
...$attributes,
|
||||
]);
|
||||
}
|
||||
|
||||
function createCanvasV5Application(V5Server $server, array $attributes = []): V5Application
|
||||
{
|
||||
return V5Application::query()->create([
|
||||
'team_id' => test()->team->id,
|
||||
'project_id' => test()->project->id,
|
||||
'environment_id' => test()->environment->id,
|
||||
'server_id' => $server->id,
|
||||
'created_by_user_id' => test()->user->id,
|
||||
'name' => 'nginx-test',
|
||||
'image' => 'docker.io/library/nginx:alpine',
|
||||
'container_name' => 'coolify-v5-nginx-'.strtolower((string) Str::ulid()),
|
||||
'status' => 'running',
|
||||
'mesh_namespace' => 'default',
|
||||
'canvas_x' => 0,
|
||||
'canvas_y' => 0,
|
||||
...$attributes,
|
||||
]);
|
||||
}
|
||||
|
||||
it('renders seeded applications on the canvas', function () {
|
||||
$server = createCanvasV5Server();
|
||||
createCanvasV5Application($server);
|
||||
|
||||
$this->actingAs($this->user);
|
||||
|
||||
$page = visit('/v5');
|
||||
|
||||
$page->assertSee('nginx-test')
|
||||
->assertSee('docker.io/library/nginx:alpine')
|
||||
->assertSee('edge-01')
|
||||
->assertSee('Configure')
|
||||
->assertSee('1 apps')
|
||||
->assertDontSee('No applications on this canvas yet.')
|
||||
->assertNoJavaScriptErrors()
|
||||
->screenshot();
|
||||
});
|
||||
|
||||
it('shows the selected project and environment in the navbar', function () {
|
||||
$this->actingAs($this->user);
|
||||
|
||||
$page = visit('/v5');
|
||||
|
||||
$page->assertSee('Canvas Project')
|
||||
->assertSee('production')
|
||||
->assertNoJavaScriptErrors()
|
||||
->screenshot();
|
||||
});
|
||||
|
||||
it('deploys an nginx container from the toolbar', function () {
|
||||
createCanvasV5Server();
|
||||
fakeSuccessfulNginxFluxDeployment();
|
||||
|
||||
$this->actingAs($this->user);
|
||||
|
||||
$page = visit('/v5');
|
||||
|
||||
$page->assertSee('No applications on this canvas yet.')
|
||||
->click('Deploy')
|
||||
->assertSee('docker.io/library/nginx:alpine')
|
||||
->assertDontSee('No applications on this canvas yet.')
|
||||
->assertNoJavaScriptErrors()
|
||||
->screenshot();
|
||||
|
||||
$application = V5Application::query()->sole();
|
||||
|
||||
expect($application->image)->toBe('docker.io/library/nginx:alpine')
|
||||
->and($application->status)->toBe('running');
|
||||
});
|
||||
|
||||
it('disables app ingress from the inspector sheet', function () {
|
||||
// Server ingress on, but not "installed", so the Caddy config sync is skipped.
|
||||
$server = createCanvasV5Server(['is_ingress' => true, 'status' => 'installing']);
|
||||
$application = createCanvasV5Application($server, ['ingress_enabled' => true, 'internal_port' => 8080]);
|
||||
V5ApplicationDomain::query()->create([
|
||||
'application_id' => $application->id,
|
||||
'domain' => 'app.example.com',
|
||||
]);
|
||||
|
||||
$this->actingAs($this->user);
|
||||
|
||||
$page = visit('/v5');
|
||||
|
||||
$page->assertSee('nginx-test')
|
||||
->click('Configure')
|
||||
->assertSee('App configuration')
|
||||
->click('Networking')
|
||||
->assertSee('Public ingress')
|
||||
->click('[data-slot="sheet-content"] button:has-text("Disable")')
|
||||
->assertSee('Private')
|
||||
->assertNoJavaScriptErrors()
|
||||
->screenshot();
|
||||
|
||||
expect($application->refresh()->ingress_enabled)->toBeFalse();
|
||||
});
|
||||
|
||||
it('enables app ingress through the ingress dialog', function () {
|
||||
$server = createCanvasV5Server(['is_ingress' => true, 'status' => 'installing']);
|
||||
$application = createCanvasV5Application($server, ['ingress_enabled' => false]);
|
||||
|
||||
$this->actingAs($this->user);
|
||||
|
||||
$page = visit('/v5');
|
||||
|
||||
$page->assertSee('nginx-test')
|
||||
->click('button:has-text("Enable")')
|
||||
->assertSee('Enable app ingress')
|
||||
->fill('[placeholder="example.com, www.example.com"]', 'app.example.com')
|
||||
->fill('[placeholder="3000"]', '8080')
|
||||
->click('button:has-text("Enable ingress")')
|
||||
->assertDontSee('Ingress update failed')
|
||||
->assertNoJavaScriptErrors()
|
||||
->screenshot();
|
||||
|
||||
expect($application->refresh()->ingress_enabled)->toBeTrue()
|
||||
->and($application->internal_port)->toBe(8080)
|
||||
->and($application->domains()->pluck('domain')->all())->toBe(['app.example.com']);
|
||||
});
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\User;
|
||||
use App\Models\V5\Cluster as V5Cluster;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
Config::set('broadcasting.default', 'log');
|
||||
InstanceSettings::create(['id' => 0, 'is_sponsorship_popup_enabled' => false]);
|
||||
|
||||
$this->user = User::factory()->create([
|
||||
'name' => 'Root User',
|
||||
'email' => 'test@example.com',
|
||||
'password' => Hash::make('password'),
|
||||
]);
|
||||
$this->team = $this->user->teams()->firstOrFail();
|
||||
});
|
||||
|
||||
it('lists existing clusters', function () {
|
||||
V5Cluster::query()->create([
|
||||
'team_id' => $this->team->id,
|
||||
'created_by_user_id' => $this->user->id,
|
||||
'name' => 'Existing Mesh',
|
||||
]);
|
||||
|
||||
$this->actingAs($this->user);
|
||||
|
||||
$page = visit('/v5/clusters');
|
||||
|
||||
$page->assertSee('Clusters')
|
||||
->assertSee('Existing Mesh')
|
||||
->assertNoJavaScriptErrors()
|
||||
->screenshot();
|
||||
});
|
||||
|
||||
it('creates a cluster from the clusters page', function () {
|
||||
$this->actingAs($this->user);
|
||||
|
||||
$page = visit('/v5/clusters');
|
||||
|
||||
$page->assertSee('Clusters')
|
||||
->click('button[aria-label="Create cluster"]')
|
||||
->assertSee('Create cluster')
|
||||
->fill('[placeholder="Production Mesh"]', 'Test Mesh')
|
||||
->click('button:has-text("Create cluster")')
|
||||
->assertSee('Test Mesh')
|
||||
->assertNoJavaScriptErrors()
|
||||
->screenshot();
|
||||
|
||||
$cluster = V5Cluster::query()->sole();
|
||||
|
||||
expect($cluster->name)->toBe('Test Mesh')
|
||||
->and($cluster->team_id)->toBe($this->team->id);
|
||||
});
|
||||
|
||||
it('shows a validation error when the cluster name is missing', function () {
|
||||
$this->actingAs($this->user);
|
||||
|
||||
$page = visit('/v5/clusters');
|
||||
|
||||
$page->click('button[aria-label="Create cluster"]')
|
||||
->click('button:has-text("Create cluster")')
|
||||
->assertSee('The name field is required.')
|
||||
->screenshot();
|
||||
|
||||
expect(V5Cluster::query()->count())->toBe(0);
|
||||
});
|
||||
Reference in new issue
Block a user