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', ]); }); } }