Deploy Ladill Hosting / deploy (push) Failing after 17s
Shared web hosting extracted from the platform monolith, with CI deploy to /var/www/ladill-hosting matching Bird/Domains/Email. Co-authored-by: Cursor <cursoragent@cursor.com>
159 lines
5.6 KiB
PHP
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',
|
|
]);
|
|
});
|
|
}
|
|
}
|