create(); $node = HostingNode::create([ 'name' => 'Local Node', 'hostname' => 'local-node', 'ip_address' => '127.0.0.1', 'type' => 'shared', 'provider' => 'local', 'status' => 'active', 'ssh_port' => '22', ]); $account = HostingAccount::unguarded(function () use ($user, $node) { return HostingAccount::create([ 'user_id' => $user->id, 'hosting_node_id' => $node->id, 'username' => 'acctuser', 'primary_domain' => 'example.com', 'type' => 'shared', 'status' => 'active', 'php_version' => '8.2', ]); }); $site = HostedSite::create([ 'hosting_account_id' => $account->id, 'domain' => 'example.com', 'document_root' => '/home/acctuser/public_html', 'type' => 'primary', 'status' => 'active', ]); $provider = \Mockery::mock(SharedNodeProvider::class); $provider->shouldReceive('createDatabase') ->once() ->withArgs(fn ($database, string $password): bool => $database->hosting_account_id === $account->id && $database->hosted_site_id === $site->id && $database->name === $database->username && strlen($password) === 16) ->andReturn(['name' => 'acctuser_abcd12', 'username' => 'acctuser_abcd12', 'host' => 'localhost']); $provider->shouldReceive('deleteDatabase') ->once() ->andReturn(true); $provider->shouldReceive('prepareDirectoryForUser') ->once() ->withArgs(fn (HostingAccount $passedAccount, string $path): bool => $passedAccount->is($account) && $path === '/home/acctuser/public_html'); $provider->shouldReceive('executeCommand') ->once() ->withArgs(fn (HostingAccount $passedAccount, string $command): bool => $passedAccount->is($account) && str_starts_with($command, 'test -w ')) ->andReturn(['output' => '', 'exit_code' => 0]); $provider->shouldReceive('executeCommand') ->once() ->withArgs(fn (HostingAccount $passedAccount, string $command): bool => $passedAccount->is($account) && str_contains($command, 'curl -fLsS')) ->andReturn(['output' => '', 'exit_code' => 6]); $this->app->instance(SharedNodeProvider::class, $provider); $session = $this->app['session.store']; $session->start(); $request = Request::create('http://localhost/hosting/panel/' . $account->id . '/apps/install', 'POST', [ 'app' => 'wordpress', 'site_id' => $site->id, 'directory' => '', ], [], [], [ 'HTTP_REFERER' => 'http://localhost/hosting/panel/' . $account->id . '/apps', ]); $request->setUserResolver(fn (): User => $user); $request->setLaravelSession($session); $response = $this->app->make(HostingPanelController::class)->installApp($request, $account); $this->assertSame(302, $response->getStatusCode()); $this->assertSame( 'Failed to install application: WordPress installation failed. Download WordPress package failed with exit code 6.', $session->get('error') ); $this->assertNull($site->fresh()->installed_app); $this->assertSame(0, $account->fresh()->databases()->count()); } public function test_wordpress_install_provisions_database_and_returns_admin_credentials(): void { $user = User::factory()->create(['email' => 'owner@example.com']); $node = HostingNode::create([ 'name' => 'Local Node', 'hostname' => 'local-node', 'ip_address' => '127.0.0.1', 'type' => 'shared', 'provider' => 'local', 'status' => 'active', 'ssh_port' => '22', ]); $account = HostingAccount::unguarded(function () use ($user, $node) { return HostingAccount::create([ 'user_id' => $user->id, 'hosting_node_id' => $node->id, 'username' => 'acctuser', 'primary_domain' => 'example.com', 'type' => 'shared', 'status' => 'active', 'php_version' => '8.2', ]); }); $site = HostedSite::create([ 'hosting_account_id' => $account->id, 'domain' => 'example.com', 'document_root' => '/home/acctuser/public_html', 'type' => 'primary', 'status' => 'active', 'ssl_enabled' => false, ]); $provider = \Mockery::mock(SharedNodeProvider::class); $provider->shouldReceive('createDatabase') ->once() ->withArgs(fn ($database, string $password): bool => $database->hosting_account_id === $account->id && $database->hosted_site_id === $site->id && $database->name === $database->username && strlen($password) === 16) ->andReturnUsing(fn ($database) => ['name' => $database->name, 'username' => $database->username, 'host' => 'localhost']); $provider->shouldReceive('prepareDirectoryForUser') ->once() ->withArgs(fn (HostingAccount $passedAccount, string $path): bool => $passedAccount->is($account) && $path === '/home/acctuser/public_html'); $provider->shouldReceive('executeCommand') ->times(10) ->andReturnUsing(function (HostingAccount $passedAccount, string $command) use ($account) { $this->assertTrue($passedAccount->is($account)); return match (true) { str_starts_with($command, 'test -w ') => ['output' => '', 'exit_code' => 0], str_contains($command, 'https://wordpress.org/latest.tar.gz') => ['output' => '', 'exit_code' => 0], str_contains($command, 'tar -xzf latest.tar.gz') => ['output' => '', 'exit_code' => 0], str_contains($command, 'rm -f latest.tar.gz') => ['output' => '', 'exit_code' => 0], str_contains($command, 'https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar') => ['output' => '', 'exit_code' => 0], str_contains($command, 'php wp-cli.phar config create') => ['output' => 'Success: Generated wp-config.php file.', 'exit_code' => 0], str_contains($command, 'php wp-cli.phar core install') => ['output' => 'Success: WordPress installed successfully.', 'exit_code' => 0], str_contains($command, "php wp-cli.phar rewrite structure '/%postname%/'") => ['output' => 'Success: Rewrite structure set.', 'exit_code' => 0], str_contains($command, 'rm -f wp-cli.phar') => ['output' => '', 'exit_code' => 0], str_contains($command, 'wp-load.php') && str_contains($command, 'wp-config.php') => ['output' => '', 'exit_code' => 0], default => throw new \RuntimeException("Unexpected command: {$command}"), }; }); $provider->shouldNotReceive('deleteDatabase'); $this->app->instance(SharedNodeProvider::class, $provider); $session = $this->app['session.store']; $session->start(); $request = Request::create('http://localhost/hosting/panel/' . $account->id . '/apps/install', 'POST', [ 'app' => 'wordpress', 'site_id' => $site->id, 'directory' => '', ], [], [], [ 'HTTP_REFERER' => 'http://localhost/hosting/panel/' . $account->id . '/apps', ]); $request->setUserResolver(fn (): User => $user); $request->setLaravelSession($session); $response = $this->app->make(HostingPanelController::class)->installApp($request, $account); $this->assertSame(302, $response->getStatusCode()); $this->assertStringContainsString('Wordpress installed successfully in example.com/', $session->get('success')); $this->assertStringContainsString('Admin URL: http://example.com/wp-admin', $session->get('success')); $this->assertStringContainsString('Database:', $session->get('success')); $site = $site->fresh(); $this->assertSame('wordpress', $site->installed_app); $this->assertSame('http://example.com/wp-admin', $site->app_config['admin_url'] ?? null); $this->assertNotNull($site->app_config['database_name'] ?? null); $this->assertNotNull($site->app_config['database_username'] ?? null); $this->assertSame(1, $account->fresh()->databases()->count()); } public function test_database_panel_shows_configured_phpmyadmin_link(): void { Config::set('hosting.shared.phpmyadmin_url', 'https://db.example.com/?account={account}&domain={primary_domain}'); $user = User::factory()->create(); $node = HostingNode::create([ 'name' => 'Local Node', 'hostname' => 'local-node', 'ip_address' => '127.0.0.1', 'type' => 'shared', 'provider' => 'local', 'status' => 'active', 'ssh_port' => '22', ]); $account = HostingAccount::unguarded(function () use ($user, $node) { return HostingAccount::create([ 'user_id' => $user->id, 'hosting_node_id' => $node->id, 'username' => 'acctuser', 'primary_domain' => 'example.com', 'type' => 'shared', 'status' => 'active', 'php_version' => '8.2', ]); }); $request = Request::create('http://localhost/hosting/panel/' . $account->id . '/databases', 'GET'); $request->setUserResolver(fn (): User => $user); $response = $this->app->make(HostingPanelController::class)->databases($request, $account); $this->assertSame('https://db.example.com/?account=acctuser&domain=example.com', $response->getData()['phpMyAdminUrl']); } public function test_extract_file_uses_quiet_archive_commands_for_large_zip_uploads(): void { $user = User::factory()->create(); $node = HostingNode::create([ 'name' => 'Local Node', 'hostname' => 'local-node', 'ip_address' => '127.0.0.1', 'type' => 'shared', 'provider' => 'local', 'status' => 'active', 'ssh_port' => '22', ]); $account = HostingAccount::unguarded(function () use ($user, $node) { return HostingAccount::create([ 'user_id' => $user->id, 'hosting_node_id' => $node->id, 'username' => 'acctuser', 'primary_domain' => 'example.com', 'type' => 'shared', 'status' => 'active', 'php_version' => '8.2', ]); }); $archivePath = '/public_html/archive.zip'; $fullArchivePath = '/home/acctuser/public_html/archive.zip'; $archiveDir = '/home/acctuser/public_html'; $provider = \Mockery::mock(SharedNodeProvider::class); $provider->shouldReceive('executeCommand') ->once() ->ordered() ->withArgs(fn (HostingAccount $passedAccount, string $command, int $timeout): bool => $passedAccount->is($account) && $command === "unzip -tqq '{$fullArchivePath}' 2>&1" && $timeout === 1800) ->andReturn(['output' => '', 'exit_code' => 0]); $provider->shouldReceive('executeCommand') ->once() ->ordered() ->withArgs(fn (HostingAccount $passedAccount, string $command, int $timeout): bool => $passedAccount->is($account) && $command === "unzip -Z1 '{$fullArchivePath}' 2>/dev/null | wc -l" && $timeout === 1800) ->andReturn(['output' => "423\n", 'exit_code' => 0]); $provider->shouldReceive('executeCommand') ->once() ->ordered() ->withArgs(fn (HostingAccount $passedAccount, string $command, int $timeout): bool => $passedAccount->is($account) && $command === "cd '{$archiveDir}' && unzip -oq '{$fullArchivePath}' 2>&1" && $timeout === 1800) ->andReturn(['output' => '', 'exit_code' => 0]); $provider->shouldReceive('executeCommand') ->once() ->ordered() ->withArgs(fn (HostingAccount $passedAccount, string $command, int $timeout): bool => $passedAccount->is($account) && $command === "find '{$archiveDir}' -type f -exec chmod 644 {} + 2>/dev/null; find '{$archiveDir}' -type d -exec chmod 755 {} + 2>/dev/null" && $timeout === 1800) ->andReturn(['output' => '', 'exit_code' => 0]); $this->app->instance(SharedNodeProvider::class, $provider); $request = Request::create('http://localhost/hosting/panel/' . $account->id . '/files/extract', 'POST', [ 'path' => $archivePath, ]); $request->setUserResolver(fn (): User => $user); $response = $this->app->make(HostingPanelController::class)->extractFile($request, $account); $this->assertSame(200, $response->getStatusCode()); $this->assertSame([ 'success' => true, 'message' => 'Archive extracted successfully', 'total_files' => 423, 'extracted_files' => 423, ], $response->getData(true)); } }