mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-06 13:10:37 -04:00
Raise the Reverb max request size and store the remembered list view cookies unencrypted, so the WebSocket handshake no longer fails with 413 and live updates (like proxy restart logs) reach the browser.
477 lines
23 KiB
PHP
477 lines
23 KiB
PHP
<?php
|
|
|
|
use App\Livewire\Server\Index as ServerIndex;
|
|
use App\Models\Application;
|
|
use App\Models\Environment;
|
|
use App\Models\InstanceSettings;
|
|
use App\Models\Project;
|
|
use App\Models\ResourceLink;
|
|
use App\Models\Server;
|
|
use App\Models\StandaloneDocker;
|
|
use App\Models\Team;
|
|
use App\Models\User;
|
|
use App\Services\ServerCanvas;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\Cookie;
|
|
use Livewire\Livewire;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
beforeEach(function () {
|
|
config([
|
|
'cache.default' => 'array',
|
|
'session.driver' => 'array',
|
|
'queue.default' => 'sync',
|
|
'app.maintenance.driver' => 'file',
|
|
]);
|
|
InstanceSettings::unguarded(fn () => InstanceSettings::firstOrCreate(['id' => 0], ['is_api_enabled' => true]));
|
|
Server::flushIdentityMap();
|
|
|
|
$this->team = Team::factory()->create(['show_boarding' => false]);
|
|
$this->admin = User::factory()->create();
|
|
$this->team->members()->attach($this->admin->id, ['role' => 'admin']);
|
|
|
|
$this->web = Server::factory()->create(['team_id' => $this->team->id, 'name' => 'web-server', 'ip' => '203.0.113.20']);
|
|
$this->db = Server::factory()->create(['team_id' => $this->team->id, 'name' => 'db-server']);
|
|
$this->project = Project::factory()->create(['team_id' => $this->team->id, 'name' => 'Shop']);
|
|
$this->environment = Environment::factory()->create(['project_id' => $this->project->id, 'name' => 'staging']);
|
|
});
|
|
|
|
function serverCanvasApp(Environment $environment, Server $server, string $name): Application
|
|
{
|
|
$destination = StandaloneDocker::query()->where('server_id', $server->id)->firstOrFail();
|
|
|
|
return Application::factory()->create([
|
|
'name' => $name,
|
|
'environment_id' => $environment->id,
|
|
'destination_id' => $destination->id,
|
|
'destination_type' => $destination->getMorphClass(),
|
|
]);
|
|
}
|
|
|
|
function serverCanvasSession(object $test, User $user, Team $team): object
|
|
{
|
|
return $test->actingAs($user)->withSession(['currentTeam' => $team])
|
|
->withHeader('X-Canvas-Team', (string) $team->id);
|
|
}
|
|
|
|
test('the servers canvas shows every server of the team with its resources and links', function () {
|
|
$app = serverCanvasApp($this->environment, $this->web, 'storefront');
|
|
$api = serverCanvasApp($this->environment, $this->db, 'orders-api');
|
|
$link = ResourceLink::create([
|
|
'environment_id' => $this->environment->id,
|
|
'source_type' => $app->getMorphClass(), 'source_id' => $app->id,
|
|
'target_type' => $api->getMorphClass(), 'target_id' => $api->id,
|
|
]);
|
|
|
|
$response = serverCanvasSession($this, $this->admin, $this->team)
|
|
->getJson('/canvas/api/servers/canvas')
|
|
->assertOk()
|
|
->assertJsonPath('title', 'Servers')
|
|
->assertJsonPath('title_link', null)
|
|
->assertJsonPath('can_edit', true)
|
|
->assertJsonPath('layout', 'server')
|
|
->assertJsonPath('positions', [])
|
|
->assertJsonPath('links.0.id', $link->uuid);
|
|
|
|
$resources = collect($response->json('nodes'))->where('type', '!=', 'empty');
|
|
|
|
// The project's empty production environment shows as a placeholder card on the server of its other environments.
|
|
expect(collect($response->json('zones'))->pluck('name')->all())->toBe(['db-server', 'web-server'])
|
|
->and(collect($response->json('nodes'))->mapWithKeys(fn ($node) => [$node['name'] => [$node['zone'], $node['context']]])->all())->toBe([
|
|
'storefront' => [$this->web->uuid, 'Shop / staging'],
|
|
'orders-api' => [$this->db->uuid, 'Shop / staging'],
|
|
'No resources yet' => [$this->web->uuid, 'Shop / production'],
|
|
])
|
|
->and($response->json('nodes.0.project'))->toBe(['uuid' => $this->project->uuid, 'name' => 'Shop'])
|
|
->and($response->json('nodes.0.environment'))->toBe(['uuid' => $this->environment->uuid, 'name' => 'staging'])
|
|
->and($resources->pluck('context_link')->unique()->all())->toBe([
|
|
route('project.resource.index', ['project_uuid' => $this->project->uuid, 'environment_uuid' => $this->environment->uuid]),
|
|
])
|
|
->and(collect($response->json('zones'))->pluck('link')->all())->toBe([
|
|
route('server.show', ['server_uuid' => $this->db->uuid]),
|
|
route('server.show', ['server_uuid' => $this->web->uuid]),
|
|
]);
|
|
});
|
|
|
|
test('the servers canvas puts empty environments of a project without resources on the first server', function () {
|
|
$bare = Project::factory()->create(['team_id' => $this->team->id, 'name' => 'Bare']);
|
|
|
|
$response = serverCanvasSession($this, $this->admin, $this->team)
|
|
->getJson('/canvas/api/servers/canvas')
|
|
->assertOk();
|
|
|
|
expect(collect($response->json('zones'))->pluck('name')->all())->toBe(['db-server', 'web-server'])
|
|
->and(collect($response->json('nodes'))->where('project.uuid', $bare->uuid)->pluck('zone')->all())->toBe([$this->db->uuid]);
|
|
});
|
|
|
|
test('the servers canvas shows a server without resources as a placeholder card in its zone', function () {
|
|
serverCanvasApp($this->environment, $this->web, 'storefront');
|
|
Environment::query()->where('project_id', $this->project->id)->where('name', '!=', 'staging')->delete();
|
|
|
|
$response = serverCanvasSession($this, $this->admin, $this->team)
|
|
->getJson('/canvas/api/servers/canvas')
|
|
->assertOk();
|
|
|
|
$placeholder = collect($response->json('nodes'))->firstWhere('zone', $this->db->uuid);
|
|
|
|
expect($placeholder)->toMatchArray([
|
|
'id' => 'empty-server:'.$this->db->uuid,
|
|
'type' => 'empty',
|
|
'name' => 'No resources yet',
|
|
'link' => route('server.show', ['server_uuid' => $this->db->uuid]),
|
|
])->and(collect($response->json('nodes'))->where('zone', $this->web->uuid)->pluck('name')->all())->toBe(['storefront']);
|
|
});
|
|
|
|
test('one server canvas shows only that server, and links that leave it are hidden', function () {
|
|
$app = serverCanvasApp($this->environment, $this->web, 'storefront');
|
|
$api = serverCanvasApp($this->environment, $this->db, 'orders-api');
|
|
ResourceLink::create([
|
|
'environment_id' => $this->environment->id,
|
|
'source_type' => $app->getMorphClass(), 'source_id' => $app->id,
|
|
'target_type' => $api->getMorphClass(), 'target_id' => $api->id,
|
|
]);
|
|
|
|
serverCanvasSession($this, $this->admin, $this->team)
|
|
->getJson("/canvas/api/servers/{$this->web->uuid}/canvas")
|
|
->assertOk()
|
|
->assertJsonPath('title', 'web-server')
|
|
->assertJsonPath('title_link', route('server.show', ['server_uuid' => $this->web->uuid]))
|
|
->assertJsonCount(1, 'zones')
|
|
->assertJsonPath('nodes.0.name', 'storefront')
|
|
->assertJsonCount(1, 'nodes')
|
|
->assertJsonPath('links', []);
|
|
});
|
|
|
|
test('the server canvas sends the card positions saved on the environment canvas with each card', function () {
|
|
$app = serverCanvasApp($this->environment, $this->web, 'storefront');
|
|
$other = serverCanvasApp($this->environment, $this->web, 'checkout');
|
|
$this->environment->forceFill(['canvas_positions' => [$app->uuid => ['x' => 120, 'y' => 40]]])->save();
|
|
|
|
$nodes = collect(serverCanvasSession($this, $this->admin, $this->team)
|
|
->getJson('/canvas/api/servers/canvas')
|
|
->assertOk()
|
|
->assertJsonPath('can_edit', true)
|
|
->assertJsonPath('can_move', false)
|
|
->assertJsonPath('positions', [])
|
|
->json('nodes'))->keyBy('id');
|
|
|
|
expect($nodes[$app->uuid]['saved'])->toBe(['x' => 120, 'y' => 40])
|
|
->and($nodes[$other->uuid]['saved'])->toBeNull();
|
|
|
|
serverCanvasSession($this, $this->admin, $this->team)
|
|
->getJson("/canvas/api/servers/{$this->web->uuid}/canvas")
|
|
->assertOk()
|
|
->assertJsonPath('can_move', false)
|
|
->assertJsonPath('positions', []);
|
|
});
|
|
|
|
test('members see the server canvas without server IPs and cannot edit it', function () {
|
|
$member = User::factory()->create();
|
|
$this->team->members()->attach($member->id, ['role' => 'member']);
|
|
serverCanvasApp($this->environment, $this->web, 'storefront');
|
|
|
|
serverCanvasSession($this, $member, $this->team)
|
|
->getJson("/canvas/api/servers/{$this->web->uuid}/canvas")
|
|
->assertOk()
|
|
->assertJsonPath('can_edit', false)
|
|
->assertJsonPath('zones.0.ip', null);
|
|
|
|
serverCanvasSession($this, $this->admin, $this->team)
|
|
->getJson("/canvas/api/servers/{$this->web->uuid}/canvas")
|
|
->assertJsonPath('zones.0.ip', '203.0.113.20');
|
|
});
|
|
|
|
test('the server canvas never shows another team servers or resources', function () {
|
|
$otherTeam = Team::factory()->create(['show_boarding' => false]);
|
|
$outsider = User::factory()->create();
|
|
$otherTeam->members()->attach($outsider->id, ['role' => 'owner']);
|
|
$this->team->members()->attach($outsider->id, ['role' => 'member']);
|
|
serverCanvasApp($this->environment, $this->web, 'storefront');
|
|
|
|
// Session switched to the other team: this team's server is out of reach.
|
|
serverCanvasSession($this, $outsider, $otherTeam)
|
|
->getJson("/canvas/api/servers/{$this->web->uuid}/canvas")
|
|
->assertNotFound();
|
|
$all = serverCanvasSession($this, $outsider, $otherTeam)
|
|
->getJson('/canvas/api/servers/canvas')
|
|
->assertOk();
|
|
expect($all->json('zones'))->toBe([])->and($all->json('nodes'))->toBe([]);
|
|
|
|
serverCanvasSession($this, $outsider, $otherTeam)
|
|
->get(route('server.canvas', ['server_uuid' => $this->web->uuid]))
|
|
->assertNotFound();
|
|
});
|
|
|
|
test('the canvas pages open for team members', function () {
|
|
serverCanvasSession($this, $this->admin, $this->team)
|
|
->withoutVite()
|
|
->get(route('server.canvas.index'))
|
|
->assertOk()
|
|
->assertSee('data-endpoint="/servers/canvas"', false);
|
|
|
|
serverCanvasSession($this, $this->admin, $this->team)
|
|
->withoutVite()
|
|
->get(route('server.canvas', ['server_uuid' => $this->web->uuid]))
|
|
->assertOk()
|
|
->assertSee('data-endpoint="/servers/'.$this->web->uuid.'/canvas"', false);
|
|
});
|
|
|
|
test('the servers page reopens the canvas when the user left it on the canvas view', function () {
|
|
serverCanvasSession($this, $this->admin, $this->team)
|
|
->get(route('server.canvas.index'))
|
|
->assertOk()
|
|
->assertPlainCookie("coolify_{$this->admin->id}_servers_view", 'canvas');
|
|
|
|
serverCanvasSession($this, $this->admin, $this->team)
|
|
->withUnencryptedCookie("coolify_{$this->admin->id}_servers_view", 'canvas')
|
|
->get(route('server.index'))
|
|
->assertRedirect(route('server.canvas.index'));
|
|
|
|
// The table / grid links on the canvas pick the list view again.
|
|
serverCanvasSession($this, $this->admin, $this->team)
|
|
->withUnencryptedCookie("coolify_{$this->admin->id}_servers_view", 'canvas')
|
|
->get(route('server.index', ['view' => 'grid']))
|
|
->assertOk()
|
|
->assertPlainCookie("coolify_{$this->admin->id}_servers_view", 'grid');
|
|
|
|
serverCanvasSession($this, $this->admin, $this->team)
|
|
->withUnencryptedCookie("coolify_{$this->admin->id}_servers_view", 'grid')
|
|
->get(route('server.index'))
|
|
->assertOk();
|
|
});
|
|
|
|
test('the environment page reopens the canvas when the user left it on the canvas view', function () {
|
|
$parameters = ['project_uuid' => $this->project->uuid, 'environment_uuid' => $this->environment->uuid];
|
|
|
|
serverCanvasSession($this, $this->admin, $this->team)
|
|
->get(route('project.environment.canvas', $parameters))
|
|
->assertOk()
|
|
->assertPlainCookie("coolify_{$this->admin->id}_environment_view", 'canvas');
|
|
|
|
serverCanvasSession($this, $this->admin, $this->team)
|
|
->withUnencryptedCookie("coolify_{$this->admin->id}_environment_view", 'canvas')
|
|
->get(route('project.resource.index', $parameters))
|
|
->assertRedirect(route('project.environment.canvas', $parameters));
|
|
|
|
serverCanvasSession($this, $this->admin, $this->team)
|
|
->withUnencryptedCookie("coolify_{$this->admin->id}_environment_view", 'canvas')
|
|
->get(route('project.resource.index', [...$parameters, 'view' => 'table']))
|
|
->assertOk()
|
|
->assertPlainCookie("coolify_{$this->admin->id}_environment_view", 'table');
|
|
});
|
|
|
|
test('the server resources page reopens the one server canvas when the user left it on the canvas view', function () {
|
|
$parameters = ['server_uuid' => $this->web->uuid];
|
|
|
|
serverCanvasSession($this, $this->admin, $this->team)
|
|
->get(route('server.canvas', $parameters))
|
|
->assertOk()
|
|
->assertPlainCookie("coolify_{$this->admin->id}_server_resources_view", 'canvas');
|
|
|
|
serverCanvasSession($this, $this->admin, $this->team)
|
|
->withUnencryptedCookie("coolify_{$this->admin->id}_server_resources_view", 'canvas')
|
|
->get(route('server.resources', $parameters))
|
|
->assertRedirect(route('server.canvas', $parameters));
|
|
|
|
serverCanvasSession($this, $this->admin, $this->team)
|
|
->withUnencryptedCookie("coolify_{$this->admin->id}_server_resources_view", 'canvas')
|
|
->get(route('server.resources', [...$parameters, 'view' => 'list']))
|
|
->assertOk()
|
|
->assertPlainCookie("coolify_{$this->admin->id}_server_resources_view", 'list');
|
|
});
|
|
|
|
test('the canvas API works with the browser session when the instance API is disabled', function () {
|
|
InstanceSettings::query()->whereKey(0)->update(['is_api_enabled' => false]);
|
|
|
|
serverCanvasSession($this, $this->admin, $this->team)
|
|
->getJson('/canvas/api/servers/canvas')
|
|
->assertOk()
|
|
->assertJsonPath('title', 'Servers');
|
|
});
|
|
|
|
test('the remembered view belongs to the user, not to the browser', function () {
|
|
$other = User::factory()->create();
|
|
$this->team->members()->attach($other->id, ['role' => 'admin']);
|
|
|
|
serverCanvasSession($this, $other, $this->team)
|
|
->withUnencryptedCookie("coolify_{$this->admin->id}_servers_view", 'canvas')
|
|
->get(route('server.index'))
|
|
->assertOk();
|
|
});
|
|
|
|
test('the list page remembers the table or grid choice and opens it again', function () {
|
|
$this->actingAs($this->admin)->withSession(['currentTeam' => $this->team]);
|
|
$cookie = "coolify_{$this->admin->id}_servers_view";
|
|
|
|
Livewire::test(ServerIndex::class)->call('saveListView', 'servers', 'grid');
|
|
expect(Cookie::queued($cookie)?->getValue())->toBe('grid');
|
|
|
|
// Unknown pages and views are ignored.
|
|
Cookie::flushQueuedCookies();
|
|
Livewire::test(ServerIndex::class)
|
|
->call('saveListView', 'servers', 'canvas')
|
|
->call('saveListView', 'billing', 'grid');
|
|
expect(Cookie::getQueuedCookies())->toBeEmpty();
|
|
|
|
serverCanvasSession($this, $this->admin, $this->team)
|
|
->withUnencryptedCookie($cookie, 'grid')
|
|
->get(route('server.index'))
|
|
->assertOk()
|
|
->assertSee("viewMode: 'grid'", false);
|
|
});
|
|
|
|
test('one server canvas loads the unmanaged containers of that server', function () {
|
|
$server = Mockery::mock($this->web)->makePartial();
|
|
$server->shouldReceive('loadUnmanagedContainers')->andReturn(collect([
|
|
['ID' => 'abc123', 'Names' => 'portainer', 'Image' => 'portainer/portainer-ce', 'State' => 'running', 'Status' => 'Up 2 hours (healthy)'],
|
|
['ID' => 'def456', 'Names' => 'old-job', 'Image' => 'busybox', 'State' => 'exited', 'Status' => 'Exited (0) 3 days ago'],
|
|
]));
|
|
|
|
$nodes = app(ServerCanvas::class)->unmanagedContainerNodes($server);
|
|
|
|
expect($nodes->map(fn ($node) => [$node['id'], $node['type'], $node['name'], $node['status'], $node['subtitle'], $node['zone'], $node['context']])->all())->toBe([
|
|
['container:def456', 'container', 'old-job', 'exited', 'busybox', $this->web->uuid, 'Unmanaged containers'],
|
|
['container:abc123', 'container', 'portainer', 'running:healthy', 'portainer/portainer-ce', $this->web->uuid, 'Unmanaged containers'],
|
|
]);
|
|
});
|
|
|
|
test('the server canvas still loads when the unmanaged containers cannot be read', function () {
|
|
$server = Mockery::mock($this->web)->makePartial();
|
|
$server->shouldReceive('loadUnmanagedContainers')->andThrow(new RuntimeException('SSH failed'));
|
|
|
|
expect(app(ServerCanvas::class)->unmanagedContainerNodes($server)->all())->toBe([]);
|
|
});
|
|
|
|
test('only the one server canvas points to the unmanaged containers of its server', function () {
|
|
$otherTeam = Team::factory()->create(['show_boarding' => false]);
|
|
$foreign = Server::factory()->create(['team_id' => $otherTeam->id]);
|
|
|
|
serverCanvasSession($this, $this->admin, $this->team)->getJson("/canvas/api/servers/{$this->web->uuid}/canvas")
|
|
->assertJsonPath('containers_endpoint', "/servers/{$this->web->uuid}/canvas/containers");
|
|
serverCanvasSession($this, $this->admin, $this->team)->getJson('/canvas/api/servers/canvas')
|
|
->assertJsonPath('containers_endpoint', null);
|
|
serverCanvasSession($this, $this->admin, $this->team)->getJson("/canvas/api/servers/{$this->web->uuid}/canvas/containers")
|
|
->assertOk()->assertExactJson(['nodes' => []]);
|
|
serverCanvasSession($this, $this->admin, $this->team)->getJson("/canvas/api/servers/{$foreign->uuid}/canvas/containers")
|
|
->assertNotFound();
|
|
});
|
|
|
|
test('server zones show host facts, the proxy and a usage endpoint only when metrics can be read', function () {
|
|
$this->web->forceFill([
|
|
'server_metadata' => ['os' => 'Ubuntu 24.04', 'arch' => 'x86_64', 'cpus' => 4, 'memory_bytes' => 8589934592, 'uptime_since' => '2026-10-01 10:00:00'],
|
|
'proxy' => ['type' => 'TRAEFIK', 'status' => 'running'],
|
|
])->save();
|
|
$this->web->settings->forceFill(['is_reachable' => true, 'is_metrics_enabled' => true])->saveQuietly();
|
|
$this->db->settings->forceFill(['is_reachable' => true, 'is_metrics_enabled' => false])->saveQuietly();
|
|
$this->db->forceFill(['proxy' => ['type' => 'NONE']])->save();
|
|
|
|
$zones = collect(serverCanvasSession($this, $this->admin, $this->team)
|
|
->getJson('/canvas/api/servers/canvas')
|
|
->assertOk()
|
|
->json('zones'))->keyBy('name');
|
|
|
|
expect($zones['web-server']['details'])->toBe([
|
|
'os' => 'Ubuntu 24.04',
|
|
'arch' => 'x86_64',
|
|
'cpus' => 4,
|
|
'memory_bytes' => 8589934592,
|
|
'uptime_since' => '2026-10-01 10:00:00',
|
|
'proxy' => ['type' => 'TRAEFIK', 'status' => 'running'],
|
|
'usage_endpoint' => "/servers/{$this->web->uuid}/canvas/usage",
|
|
])
|
|
->and($zones['db-server']['details']['proxy'])->toBeNull()
|
|
->and($zones['db-server']['details']['usage_endpoint'])->toBeNull();
|
|
});
|
|
|
|
test('the usage endpoint returns the latest Sentinel values and never another team server', function () {
|
|
$server = Mockery::mock($this->web)->makePartial();
|
|
$server->shouldReceive('prefetchMetrics')->once()->with(['cpu', 'memory', 'disk'], 1);
|
|
$server->shouldReceive('getCpuMetrics')->andReturn([[1, 10.0], [2, 12.34]]);
|
|
$server->shouldReceive('getMemoryMetrics')->andReturn([[2, 40.0]]);
|
|
$server->shouldReceive('getDiskMetrics')->andThrow(new RuntimeException('no disk data'));
|
|
|
|
expect(app(ServerCanvas::class)->usage($server))->toBe(['cpu' => 12.3, 'memory' => 40.0, 'disk' => null]);
|
|
|
|
$this->web->settings->forceFill(['is_metrics_enabled' => false])->saveQuietly();
|
|
serverCanvasSession($this, $this->admin, $this->team)->getJson("/canvas/api/servers/{$this->web->uuid}/canvas/usage")
|
|
->assertOk()->assertExactJson(['cpu' => null, 'memory' => null, 'disk' => null]);
|
|
|
|
$otherTeam = Team::factory()->create(['show_boarding' => false]);
|
|
$foreign = Server::factory()->create(['team_id' => $otherTeam->id]);
|
|
serverCanvasSession($this, $this->admin, $this->team)->getJson("/canvas/api/servers/{$foreign->uuid}/canvas/usage")
|
|
->assertNotFound();
|
|
});
|
|
|
|
test('only admins may move the environment groups on the server canvas', function () {
|
|
$member = User::factory()->create();
|
|
$this->team->members()->attach($member->id, ['role' => 'member']);
|
|
|
|
serverCanvasSession($this, $this->admin, $this->team)->getJson('/canvas/api/servers/canvas')
|
|
->assertJsonPath('can_move_groups', true);
|
|
serverCanvasSession($this, $member, $this->team)->getJson('/canvas/api/servers/canvas')
|
|
->assertJsonPath('can_move_groups', false);
|
|
});
|
|
|
|
test('an admin moves server zones on the servers and dashboard canvases and both return their place', function () {
|
|
// Another team's server with the same request stays as it is.
|
|
$foreign = Server::factory()->create(['team_id' => Team::factory()->create()->id]);
|
|
|
|
serverCanvasSession($this, $this->admin, $this->team)->getJson('/canvas/api/servers/canvas')
|
|
->assertJsonPath('can_move_zones', true)
|
|
->assertJsonPath('zones.0.position', null);
|
|
|
|
serverCanvasSession($this, $this->admin, $this->team)
|
|
->patchJson('/canvas/api/dashboard/canvas/positions', ['positions' => [
|
|
$this->web->uuid => ['x' => 640, 'y' => -120],
|
|
$foreign->uuid => ['x' => 5, 'y' => 5],
|
|
'no-server' => ['x' => 0, 'y' => 0],
|
|
]])
|
|
->assertOk()
|
|
->assertJsonPath('positions', [$this->web->uuid => ['x' => 640, 'y' => -120]]);
|
|
serverCanvasSession($this, $this->admin, $this->team)
|
|
->patchJson('/canvas/api/servers/canvas/positions', ['positions' => [$this->db->uuid => ['x' => 0, 'y' => 0]]])
|
|
->assertOk();
|
|
|
|
expect($foreign->fresh()->canvas_position)->toBeNull();
|
|
// Zones are sorted by name: db-server, web-server.
|
|
foreach (['/canvas/api/servers/canvas', '/canvas/api/dashboard/canvas'] as $endpoint) {
|
|
serverCanvasSession($this, $this->admin, $this->team)->getJson($endpoint)
|
|
->assertJsonPath('zones.0.position', ['x' => 0, 'y' => 0])
|
|
->assertJsonPath('zones.1.position', ['x' => 640, 'y' => -120]);
|
|
}
|
|
// The one server canvas has a single zone, which stays in its place.
|
|
serverCanvasSession($this, $this->admin, $this->team)->getJson("/canvas/api/servers/{$this->web->uuid}/canvas")
|
|
->assertJsonPath('can_move_zones', false)
|
|
->assertJsonPath('zones.0.position', null);
|
|
});
|
|
|
|
test('server zone positions must be whole coordinates on the canvas', function () {
|
|
serverCanvasSession($this, $this->admin, $this->team)
|
|
->patchJson('/canvas/api/servers/canvas/positions', ['positions' => [$this->web->uuid => ['x' => 'left', 'y' => 999999999]]])
|
|
->assertUnprocessable()
|
|
->assertJsonValidationErrors(["positions.{$this->web->uuid}.x", "positions.{$this->web->uuid}.y"]);
|
|
|
|
expect($this->web->fresh()->canvas_position)->toBeNull();
|
|
});
|
|
|
|
test('members and other teams cannot move a server zone', function () {
|
|
$member = User::factory()->create();
|
|
$this->team->members()->attach($member->id, ['role' => 'member']);
|
|
serverCanvasSession($this, $member, $this->team)->getJson('/canvas/api/dashboard/canvas')
|
|
->assertJsonPath('can_move_zones', false);
|
|
serverCanvasSession($this, $member, $this->team)
|
|
->patchJson('/canvas/api/servers/canvas/positions', ['positions' => [$this->web->uuid => ['x' => 10, 'y' => 10]]])
|
|
->assertForbidden();
|
|
|
|
// Admin of this team, owner of another team, with the session switched to the other team.
|
|
$otherTeam = Team::factory()->create(['show_boarding' => false]);
|
|
$otherTeam->members()->attach($this->admin->id, ['role' => 'owner']);
|
|
serverCanvasSession($this, $this->admin, $otherTeam)
|
|
->patchJson('/canvas/api/dashboard/canvas/positions', ['positions' => [$this->web->uuid => ['x' => 10, 'y' => 10]]])
|
|
->assertOk()
|
|
->assertJsonPath('positions', []);
|
|
|
|
expect($this->web->fresh()->canvas_position)->toBeNull();
|
|
});
|