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>
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
<?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',
|
||||
]);
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user