create(); $product = HostingProduct::create([ 'name' => 'Multi Domain Hosting', 'slug' => 'multi-domain-hosting-test', 'category' => 'shared', 'type' => 'multi_domain', 'price_monthly' => 10, 'max_domains' => 5, ]); $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, $product) { return HostingAccount::create([ 'user_id' => $user->id, 'hosting_product_id' => $product->id, 'hosting_node_id' => $node->id, 'username' => 'acctuser', 'primary_domain' => 'primary.example.com', 'type' => 'shared', 'status' => 'active', 'php_version' => '8.2', ]); }); $provider = \Mockery::mock(SharedNodeProvider::class); $provider->shouldReceive('addSite') ->once() ->withArgs(fn (HostedSite $site): bool => $site->hosting_account_id === $account->id && $site->domain === 'linked-example.com') ->andReturn([ 'document_root' => '/home/acctuser/public_html/linked-example.com', 'domain' => 'linked-example.com', ]); $verification = \Mockery::mock(DomainVerificationService::class); $verification->shouldReceive('reprovision')->once(); $verification->shouldNotReceive('prepareNameserverZone'); $verification->shouldNotReceive('prepareManualDnsPack'); $this->app->instance(SharedNodeProvider::class, $provider); $this->app->instance(DomainVerificationService::class, $verification); $session = $this->app['session.store']; $session->start(); $request = Request::create('http://localhost/hosting/panel/' . $account->id . '/domains', 'POST', [ 'domain' => 'linked-example.com', ], [], [], [ 'HTTP_REFERER' => 'http://localhost/hosting/accounts/' . $account->id, ]); $request->setUserResolver(fn (): User => $user); $request->setLaravelSession($session); $response = $this->app->make(HostingPanelController::class)->addDomain( $request, $account, $this->app->make(DomainDnsBlueprintService::class), $this->app->make(DomainVerificationService::class), ); $this->assertSame(302, $response->getStatusCode()); $domain = Domain::query()->where('host', 'linked-example.com')->firstOrFail(); $this->assertSame($account->id, $domain->hosting_account_id); $this->assertSame(Domain::MODE_NS_AUTO, $domain->onboarding_mode); $this->assertSame('managed', $domain->dns_mode); $this->assertDatabaseHas('hosted_sites', [ 'hosting_account_id' => $account->id, 'domain' => 'linked-example.com', 'domain_id' => $domain->id, ]); } public function test_hosting_account_page_renders_linked_domains(): void { $user = User::factory()->create(); $product = HostingProduct::create([ 'name' => 'Multi Domain Hosting', 'slug' => 'multi-domain-account-view-test', 'category' => 'shared', 'type' => 'multi_domain', 'price_monthly' => 10, 'max_domains' => 3, ]); $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, $product) { return HostingAccount::create([ 'user_id' => $user->id, 'hosting_product_id' => $product->id, 'hosting_node_id' => $node->id, 'username' => 'acctuser', 'primary_domain' => 'primary.example.com', 'type' => 'shared', 'status' => 'active', 'php_version' => '8.2', ]); }); HostedSite::create([ 'hosting_account_id' => $account->id, 'domain' => 'primary.example.com', 'document_root' => '/home/acctuser/public_html', 'type' => 'primary', 'status' => 'active', 'ssl_enabled' => true, ]); HostedSite::create([ 'hosting_account_id' => $account->id, 'domain' => 'linked-example.com', 'document_root' => '/home/acctuser/public_html/linked-example.com', 'type' => 'addon', 'status' => 'active', 'ssl_enabled' => false, ]); $request = Request::create('http://localhost/hosting/accounts/' . $account->id, 'GET'); $request->setUserResolver(fn (): User => $user); $view = $this->app->make(HostingProductController::class)->showAccount($request, $account); $html = $view->render(); $this->assertStringContainsString('primary.example.com', $html); $this->assertStringContainsString('linked-example.com', $html); $this->assertStringContainsString('2 of 3 domain slots in use.', $html); $this->assertStringContainsString('0 of 15 subdomain slots in use.', $html); } public function test_add_domain_does_not_restore_soft_deleted_site_from_another_account(): void { $user = User::factory()->create(); $otherUser = User::factory()->create(); $product = HostingProduct::create([ 'name' => 'Multi Domain Hosting', 'slug' => 'multi-domain-restore-guard-test', 'category' => 'shared', 'type' => 'multi_domain', 'price_monthly' => 10, 'max_domains' => 5, ]); $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, $product) { return HostingAccount::create([ 'user_id' => $user->id, 'hosting_product_id' => $product->id, 'hosting_node_id' => $node->id, 'username' => 'acctuser', 'primary_domain' => 'primary.example.com', 'type' => 'shared', 'status' => 'active', 'php_version' => '8.2', ]); }); $otherAccount = HostingAccount::unguarded(function () use ($otherUser, $node, $product) { return HostingAccount::create([ 'user_id' => $otherUser->id, 'hosting_product_id' => $product->id, 'hosting_node_id' => $node->id, 'username' => 'otheracct', 'primary_domain' => 'other.example.com', 'type' => 'shared', 'status' => 'active', 'php_version' => '8.2', ]); }); $site = HostedSite::create([ 'hosting_account_id' => $otherAccount->id, 'domain' => 'linked-example.com', 'document_root' => '/home/otheracct/public_html/linked-example.com', 'type' => 'addon', 'status' => 'active', ]); $site->delete(); $provider = \Mockery::mock(SharedNodeProvider::class); $provider->shouldNotReceive('addSite'); $this->app->instance(SharedNodeProvider::class, $provider); $session = $this->app['session.store']; $session->start(); $request = Request::create('http://localhost/hosting/panel/' . $account->id . '/domains', 'POST', [ 'domain' => 'linked-example.com', ], [], [], [ 'HTTP_REFERER' => 'http://localhost/hosting/accounts/' . $account->id, ]); $request->setUserResolver(fn (): User => $user); $request->setLaravelSession($session); $response = $this->app->make(HostingPanelController::class)->addDomain( $request, $account, $this->app->make(DomainDnsBlueprintService::class), $this->app->make(DomainVerificationService::class), ); $this->assertSame(302, $response->getStatusCode()); $this->assertSame('This domain is already in use on another hosting account.', $session->get('error')); $this->assertSoftDeleted('hosted_sites', [ 'id' => $site->id, 'hosting_account_id' => $otherAccount->id, 'domain' => 'linked-example.com', ]); } public function test_add_subdomain_creates_hosted_site_under_parent_domain(): void { [$user, $account, $parent] = $this->makeAccountWithPrimary(maxDomains: 1, withManagedDomain: true); $provider = \Mockery::mock(SharedNodeProvider::class); $provider->shouldReceive('addSite') ->once() ->withArgs(fn (HostedSite $site): bool => $site->domain === 'blog.primary.example.com' && $site->type === 'subdomain') ->andReturn(['document_root' => '/home/acctuser/public_html/blog.primary.example.com', 'domain' => 'blog.primary.example.com']); $pdns = \Mockery::mock(\App\Services\Dns\PowerDnsClient::class); $pdns->shouldReceive('upsertRecords') ->once() ->withArgs(function (string $zone, array $records) { return $zone === 'primary.example.com' && ($records[0]['name'] ?? null) === 'blog.primary.example.com' && ($records[0]['contents'][0] ?? null) === '127.0.0.1'; }) ->andReturn(['status' => 'updated', 'count' => 1]); $this->app->instance(SharedNodeProvider::class, $provider); $this->app->instance(\App\Services\Dns\PowerDnsClient::class, $pdns); $session = $this->app['session.store']; $session->start(); $request = Request::create('http://localhost/hosting/panel/'.$account->id.'/domains/subdomains', 'POST', [ 'parent_site_id' => $parent->id, 'subdomain' => 'blog', ], [], [], [ 'HTTP_REFERER' => 'http://localhost/hosting/panel/'.$account->id.'/domains', ]); $request->setUserResolver(fn (): User => $user); $request->setLaravelSession($session); $response = $this->app->make(HostingPanelController::class)->addSubdomain( $request, $account, $this->app->make(\App\Services\Dns\PowerDnsClient::class), ); $this->assertSame(302, $response->getStatusCode()); $this->assertDatabaseHas('hosted_sites', [ 'hosting_account_id' => $account->id, 'domain' => 'blog.primary.example.com', 'type' => 'subdomain', 'domain_id' => $parent->domain_id, 'document_root' => '/home/acctuser/public_html/blog.primary.example.com', ]); $this->assertSame(1, $account->fresh()->subdomainSitesCount()); $this->assertSame(5, $account->fresh()->maxSubdomainsLimit()); } public function test_subdomain_quota_is_max_domains_times_five(): void { [$user, $account, $parent] = $this->makeAccountWithPrimary(maxDomains: 1, withManagedDomain: false); $provider = \Mockery::mock(SharedNodeProvider::class); $provider->shouldReceive('addSite')->times(5)->andReturnUsing(function (HostedSite $site) { return ['document_root' => $site->document_root, 'domain' => $site->domain]; }); $this->app->instance(SharedNodeProvider::class, $provider); $this->app->instance(\App\Services\Dns\PowerDnsClient::class, \Mockery::mock(\App\Services\Dns\PowerDnsClient::class)); $controller = $this->app->make(HostingPanelController::class); for ($i = 1; $i <= 5; $i++) { $session = $this->app['session.store']; $session->start(); $request = Request::create('/domains/subdomains', 'POST', [ 'parent_site_id' => $parent->id, 'subdomain' => 'sub'.$i, ]); $request->setUserResolver(fn (): User => $user); $request->setLaravelSession($session); $response = $controller->addSubdomain($request, $account->fresh(), $this->app->make(\App\Services\Dns\PowerDnsClient::class)); $this->assertSame(302, $response->getStatusCode()); $this->assertNull($session->get('error'), 'sub'.$i.' should succeed'); } $this->assertSame(5, $account->fresh()->subdomainSitesCount()); $session = $this->app['session.store']; $session->start(); $request = Request::create('/domains/subdomains', 'POST', [ 'parent_site_id' => $parent->id, 'subdomain' => 'sub6', ]); $request->setUserResolver(fn (): User => $user); $request->setLaravelSession($session); $response = $controller->addSubdomain($request, $account->fresh(), $this->app->make(\App\Services\Dns\PowerDnsClient::class)); $this->assertSame(302, $response->getStatusCode()); $this->assertSame('You have reached the maximum number of subdomains (5).', $session->get('error')); $this->assertSame(5, $account->fresh()->subdomainSitesCount()); } public function test_addon_domain_cap_ignores_subdomains(): void { [$user, $account, $parent] = $this->makeAccountWithPrimary(maxDomains: 2, withManagedDomain: false); HostedSite::create([ 'hosting_account_id' => $account->id, 'domain' => 'blog.primary.example.com', 'document_root' => '/home/acctuser/public_html/blog.primary.example.com', 'type' => 'subdomain', 'status' => 'active', 'domain_id' => $parent->domain_id, ]); $provider = \Mockery::mock(SharedNodeProvider::class); $provider->shouldReceive('addSite')->once()->andReturn([ 'document_root' => '/home/acctuser/public_html/addon.example.com', 'domain' => 'addon.example.com', ]); $verification = \Mockery::mock(DomainVerificationService::class); $verification->shouldReceive('reprovision')->once(); $this->app->instance(SharedNodeProvider::class, $provider); $this->app->instance(DomainVerificationService::class, $verification); $session = $this->app['session.store']; $session->start(); $request = Request::create('/domains', 'POST', ['domain' => 'addon.example.com']); $request->setUserResolver(fn (): User => $user); $request->setLaravelSession($session); $response = $this->app->make(HostingPanelController::class)->addDomain( $request, $account->fresh(), $this->app->make(DomainDnsBlueprintService::class), $this->app->make(DomainVerificationService::class), ); $this->assertSame(302, $response->getStatusCode()); $this->assertNull($session->get('error')); $this->assertDatabaseHas('hosted_sites', [ 'hosting_account_id' => $account->id, 'domain' => 'addon.example.com', 'type' => 'addon', ]); } public function test_add_subdomain_rejects_reserved_and_invalid_labels(): void { [$user, $account, $parent] = $this->makeAccountWithPrimary(maxDomains: 1, withManagedDomain: false); $this->app->instance(SharedNodeProvider::class, \Mockery::mock(SharedNodeProvider::class)); $this->app->instance(\App\Services\Dns\PowerDnsClient::class, \Mockery::mock(\App\Services\Dns\PowerDnsClient::class)); $controller = $this->app->make(HostingPanelController::class); $session = $this->app['session.store']; $session->start(); $request = Request::create('/domains/subdomains', 'POST', [ 'parent_site_id' => $parent->id, 'subdomain' => 'www', ]); $request->setUserResolver(fn (): User => $user); $request->setLaravelSession($session); $controller->addSubdomain($request, $account, $this->app->make(\App\Services\Dns\PowerDnsClient::class)); $this->assertSame('The subdomain "www" is reserved.', $session->get('error')); $this->expectException(\Illuminate\Validation\ValidationException::class); $session = $this->app['session.store']; $session->start(); $request = Request::create('/domains/subdomains', 'POST', [ 'parent_site_id' => $parent->id, 'subdomain' => 'bad.label', ]); $request->setUserResolver(fn (): User => $user); $request->setLaravelSession($session); $controller->addSubdomain($request, $account, $this->app->make(\App\Services\Dns\PowerDnsClient::class)); } /** * @return array{0: User, 1: HostingAccount, 2: HostedSite} */ private function makeAccountWithPrimary(int $maxDomains, bool $withManagedDomain): array { $user = User::factory()->create(); $product = HostingProduct::create([ 'name' => 'Hosting Plan', 'slug' => 'hosting-plan-'.uniqid(), 'category' => 'shared', 'type' => 'multi_domain', 'price_monthly' => 10, 'max_domains' => $maxDomains, ]); $node = HostingNode::create([ 'name' => 'Local Node', 'hostname' => 'local-node-'.uniqid(), 'ip_address' => '127.0.0.1', 'type' => 'shared', 'provider' => 'local', 'status' => 'active', 'ssh_port' => '22', ]); $account = HostingAccount::unguarded(function () use ($user, $node, $product, $maxDomains) { return HostingAccount::create([ 'user_id' => $user->id, 'hosting_product_id' => $product->id, 'hosting_node_id' => $node->id, 'username' => 'acctuser', 'primary_domain' => 'primary.example.com', 'type' => 'shared', 'status' => 'active', 'php_version' => '8.2', 'resource_limits' => ['max_domains' => $maxDomains], ]); }); $domain = null; if ($withManagedDomain) { $domain = Domain::create([ 'user_id' => $user->id, 'hosting_account_id' => $account->id, 'host' => 'primary.example.com', 'type' => 'custom', 'source' => 'hosting', 'onboarding_mode' => Domain::MODE_NS_AUTO, 'status' => 'verified', 'onboarding_state' => Domain::STATE_ACTIVE, 'dns_mode' => 'managed', ]); } $parent = HostedSite::create([ 'hosting_account_id' => $account->id, 'domain_id' => $domain?->id, 'domain' => 'primary.example.com', 'document_root' => '/home/acctuser/public_html', 'type' => 'primary', 'status' => 'active', 'ssl_enabled' => true, ]); return [$user, $account->fresh(['sites', 'product', 'node']), $parent->fresh()]; } }