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>
307 lines
14 KiB
PHP
307 lines
14 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use App\Http\Controllers\Hosting\HostingPanelController;
|
|
use App\Models\HostedSite;
|
|
use App\Models\HostingAccount;
|
|
use App\Models\HostingNode;
|
|
use App\Models\User;
|
|
use App\Services\Hosting\Providers\SharedNodeProvider;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\Config;
|
|
use Tests\TestCase;
|
|
|
|
class HostingPanelAppInstallTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
public function test_wordpress_install_failure_surfaces_failed_step_when_command_output_is_empty(): 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',
|
|
]);
|
|
});
|
|
|
|
$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));
|
|
}
|
|
}
|