Files
ladill-servers/tests/Feature/HostingPanelTerminalTest.php
isaaccladandCursor b6c8ac343f Extract Ladill Servers as standalone app at servers.ladill.com.
VPS and dedicated server ordering, managed panels, SSO, billing, and launcher integration — forked from hosting infrastructure with server-focused routes and dashboard.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 19:18:30 +00:00

159 lines
5.6 KiB
PHP

<?php
namespace Tests\Feature;
use App\Models\HostingAccount;
use App\Models\HostingAccountMember;
use App\Models\HostingNode;
use App\Models\HostingProduct;
use App\Models\User;
use App\Services\Hosting\BrowserTerminalSessionManager;
use App\Services\Hosting\Providers\SharedNodeProvider;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class HostingPanelTerminalTest extends TestCase
{
use RefreshDatabase;
public function test_owner_can_open_terminal_and_run_an_allowed_command(): void
{
$owner = User::factory()->create();
$account = $this->createHostingAccount($owner);
$provider = \Mockery::mock(SharedNodeProvider::class);
$provider->shouldReceive('executeTerminalCommand')
->once()
->withArgs(fn (HostingAccount $passedAccount, string $command, string $workingDirectory) => $passedAccount->is($account)
&& $command === 'php artisan about'
&& $workingDirectory === '/home/acctuser/public_html')
->andReturn([
'output' => 'Laravel Framework 12.x',
'exit_code' => 0,
]);
$this->app->instance(SharedNodeProvider::class, $provider);
$this->actingAs($owner)
->get(route('hosting.panel.terminal', $account))
->assertOk()
->assertSee('Terminal')
->assertSee($account->username.'@ladill');
$this->actingAs($owner)
->postJson(route('hosting.panel.terminal.run', $account), [
'command' => 'php artisan about',
'path' => '/public_html',
])
->assertOk()
->assertJson([
'success' => true,
'exit_code' => 0,
'output' => 'Laravel Framework 12.x',
'cwd' => '/public_html',
]);
}
public function test_terminal_rejects_disallowed_commands_before_execution(): void
{
$owner = User::factory()->create();
$account = $this->createHostingAccount($owner);
$provider = \Mockery::mock(SharedNodeProvider::class);
$provider->shouldNotReceive('executeTerminalCommand');
$this->app->instance(SharedNodeProvider::class, $provider);
$this->actingAs($owner)
->postJson(route('hosting.panel.terminal.run', $account), [
'command' => 'curl https://example.com',
'path' => '/public_html',
])
->assertStatus(422)
->assertJsonPath('error', '`curl` is not available in the panel terminal.');
}
public function test_developer_can_view_terminal(): void
{
$owner = User::factory()->create();
$developer = User::factory()->create();
$account = $this->createHostingAccount($owner);
HostingAccountMember::create([
'hosting_account_id' => $account->id,
'user_id' => $developer->id,
'invited_by_user_id' => $owner->id,
'role' => HostingAccountMember::ROLE_DEVELOPER,
'invited_at' => now(),
]);
$this->actingAs($developer)
->get(route('hosting.panel.terminal', $account))
->assertOk()
->assertSee('Terminal');
}
public function test_terminal_input_endpoint_accepts_plain_text_payloads(): void
{
$owner = User::factory()->create();
$account = $this->createHostingAccount($owner);
$sessionManager = \Mockery::mock(BrowserTerminalSessionManager::class);
$sessionManager->shouldReceive('appendInput')
->once()
->withArgs(fn (HostingAccount $passedAccount, int $userId, string $sessionId, string $input) => $passedAccount->is($account)
&& $userId === (int) $owner->id
&& $sessionId === 'session-123'
&& $input === "ls\r");
$this->app->instance(BrowserTerminalSessionManager::class, $sessionManager);
$this->actingAs($owner)
->call(
'POST',
route('hosting.panel.terminal.sessions.input', [$account, 'session-123']),
server: [
'CONTENT_TYPE' => 'text/plain',
'HTTP_ACCEPT' => 'application/json',
],
content: "ls\r",
)
->assertOk()
->assertJson(['ok' => true]);
}
private function createHostingAccount(User $owner): HostingAccount
{
$product = HostingProduct::create([
'name' => 'Single Domain Hosting',
'slug' => 'terminal-test-'.$owner->id,
'category' => 'shared',
'type' => HostingProduct::TYPE_SINGLE_DOMAIN,
'price_monthly' => 10,
'max_domains' => 1,
'is_active' => true,
'is_visible' => true,
]);
$node = HostingNode::create([
'name' => 'Local Node',
'hostname' => 'local-node-terminal-'.$owner->id,
'ip_address' => '127.0.0.1',
'type' => 'shared',
'provider' => 'local',
'status' => 'active',
'ssh_port' => '22',
]);
return HostingAccount::unguarded(function () use ($owner, $product, $node) {
return HostingAccount::create([
'user_id' => $owner->id,
'hosting_product_id' => $product->id,
'hosting_node_id' => $node->id,
'username' => 'acctuser',
'primary_domain' => 'example.com',
'type' => 'shared',
'status' => 'active',
'php_version' => '8.2',
]);
});
}
}