"plesk-{$ip}", 'ip_address' => $ip, 'username' => 'admin', 'password' => Crypt::encryptString('secret'), 'status' => 'activated', 'type' => 'hosting', ]); } private function plan(DnsServer ...$servers): ServicePlan { $plan = ServicePlan::create(['title' => 'Basic', 'vcpu' => 1, 'ram' => 1, 'disk' => 10, 'domain' => 1]); $plan->plesk()->create(['plesk_plan_name' => 'basic-hosting']); $plan->syncServers(array_map(fn (DnsServer $server) => $server->id, $servers)); return $plan->fresh(); } public function test_pick_skips_unhealthy_and_inactive_servers_in_priority_order(): void { Cache::flush(); $down = $this->server('10.0.0.1'); $inactive = $this->server('10.0.0.2', ['status' => 'deactivated']); $up = $this->server('10.0.0.3'); $plan = $this->plan($down, $inactive, $up); Http::fake([ '10.0.0.1:8443/*' => Http::response([], 500), '*' => Http::response(['platform' => 'Linux'], 200), ]); $selector = app(HostingServerSelector::class); $this->assertSame($up->id, $selector->pick($plan)->id); $this->assertNull($selector->pick($plan, [$up->id])); } public function test_plan_primary_server_column_follows_first_priority(): void { $a = $this->server('10.0.0.1'); $b = $this->server('10.0.0.2'); $plan = $this->plan($b, $a); $this->assertSame($b->id, (int) $plan->dns_manager_server_id); $this->assertSame([$b->id, $a->id], $plan->servers->pluck('id')->all()); } public function test_provisioning_fails_over_to_the_backup_server_when_the_primary_goes_down(): void { Cache::flush(); Bus::fake([HostingAccountCreatedJob::class, HostingAccountCreatedAdminNotificationJob::class]); $primary = $this->server('10.0.0.1'); $backup = $this->server('10.0.0.2'); $plan = $this->plan($primary, $backup); Http::fake([ '10.0.0.1:8443/api/v2/server*' => Http::sequence()->push([], 200)->push([], 500), '10.0.0.1:8443/*' => Http::response(['message' => 'down'], 500), '10.0.0.2:8443/api/v2/server*' => Http::response([], 200), '10.0.0.2:8443/api/v2/clients*' => Http::response(['id' => 7, 'guid' => 'client-guid'], 201), '10.0.0.2:8443/*' => Http::response(['id' => 9, 'guid' => 'sub-guid', 'www_root' => '/var/www/vhosts/khach.test/httpdocs'], 201), '*' => Http::response([], 200), ]); [$item, $service] = $this->paidHostingOrder($plan); dispatch_sync(new ProvisionHostingAccountJob($item)); $this->assertSame('active', $service->fresh()->status); $this->assertSame($backup->id, ModuleHostingAccount::where('service_id', $service->id)->value('dns_manager_server_id')); } public function test_provisioning_fails_and_refunds_when_every_server_is_down(): void { Cache::flush(); Bus::fake([VMCreationFailedJob::class]); $plan = $this->plan($this->server('10.0.0.1'), $this->server('10.0.0.2')); Http::fake(['10.0.0.*' => Http::response([], 500), '*' => Http::response([], 200)]); [$item, $service, $customer, $invoice, $order] = $this->paidHostingOrder($plan); dispatch_sync(new ProvisionHostingAccountJob($item)); $this->assertSame('failed', $service->fresh()->status); $this->assertSame(500_000, (int) $customer->fresh()->total); $this->assertSame(InvoiceStatus::REFUNDED->value, (int) $invoice->fresh()->status); $this->assertSame(OrderStatus::CANCELLED->value, $order->fresh()->status); } private function paidHostingOrder(ServicePlan $plan): array { $customer = Customer::factory()->create(['total' => 500_000]); $product = Product::factory()->create(['product_type' => 'hosting', 'service_plan_id' => $plan->id]); $order = Order::create([ 'order_num' => 'ORD-' . uniqid(), 'customer_id' => $customer->id, 'sub_total' => 200_000, 'total' => 200_000, 'status' => OrderStatus::PENDING->value, ]); $item = OrderItem::create([ 'order_id' => $order->id, 'product_id' => $product->id, 'billing_cycle' => 'monthly', 'months' => 1, 'server_name' => 'khach.test', 'quantity' => 1, 'price' => 200_000, ]); $invoice = Invoice::create([ 'invoice_number' => 'INV-' . uniqid(), 'customer_id' => $customer->id, 'order_id' => $order->id, 'invoice_type' => 1, 'sub_total' => 200_000, 'tax' => 0, 'total' => 200_000, 'status' => InvoiceStatus::UNPAID->value, ]); $service = Service::create([ 'customer_id' => $customer->id, 'product_id' => $product->id, 'order_item_id' => $item->id, 'invoice_id' => $invoice->id, 'server_name' => 'khach.test', 'status' => 'pending', 'started_at' => now(), 'expires_at' => now()->addMonth(), ]); app(InvoicePaymentService::class)->payWithBalance($invoice, $customer, 'balance'); return [$item->fresh(), $service, $customer, $invoice, $order]; } }