Initial Ladill Hosting app with Gitea deploy pipeline.
Deploy Ladill Hosting / deploy (push) Failing after 17s
Deploy Ladill Hosting / deploy (push) Failing after 17s
Shared web hosting extracted from the platform monolith, with CI deploy to /var/www/ladill-hosting matching Bird/Domains/Email. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,273 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\HostingAccount;
|
||||
use App\Models\HostingBillingInvoice;
|
||||
use App\Models\HostingProduct;
|
||||
use App\Models\User;
|
||||
use App\Services\Billing\PaystackService;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AdminHostingAccountDurationTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
Carbon::setTestNow();
|
||||
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
public function test_admin_can_assign_hosting_package_with_duration(): void
|
||||
{
|
||||
Carbon::setTestNow('2026-04-05 10:00:00');
|
||||
|
||||
$admin = User::factory()->create(['is_admin' => true]);
|
||||
$user = User::factory()->create();
|
||||
$product = $this->createHostingProduct();
|
||||
|
||||
$response = $this->actingAs($admin)->post(route('admin.users.assign-hosting-package', $user), [
|
||||
'hosting_product_id' => $product->id,
|
||||
'duration_months' => 6,
|
||||
'primary_domain' => 'example.com',
|
||||
'username' => 'exampleuser',
|
||||
]);
|
||||
|
||||
$response->assertRedirect();
|
||||
|
||||
$account = HostingAccount::query()->where('user_id', $user->id)->firstOrFail();
|
||||
|
||||
$this->assertSame('exampleuser', $account->username);
|
||||
$this->assertSame('example.com', $account->primary_domain);
|
||||
$this->assertSame($product->id, $account->hosting_product_id);
|
||||
$this->assertSame('active', $account->status);
|
||||
$this->assertNotNull($account->expires_at);
|
||||
$this->assertTrue($account->expires_at->equalTo(Carbon::parse('2026-10-05 10:00:00')));
|
||||
$this->assertSame(6, data_get($account->metadata, 'assigned_duration_months'));
|
||||
}
|
||||
|
||||
public function test_admin_can_assign_same_hosting_product_multiple_times_to_one_user(): void
|
||||
{
|
||||
Carbon::setTestNow('2026-04-05 10:00:00');
|
||||
|
||||
$admin = User::factory()->create(['is_admin' => true]);
|
||||
$user = User::factory()->create();
|
||||
$product = $this->createHostingProduct();
|
||||
|
||||
$first = $this->actingAs($admin)->post(route('admin.users.assign-hosting-package', $user), [
|
||||
'hosting_product_id' => $product->id,
|
||||
'duration_months' => 6,
|
||||
'primary_domain' => 'first-example.com',
|
||||
'username' => 'firstacct',
|
||||
]);
|
||||
|
||||
$second = $this->actingAs($admin)->post(route('admin.users.assign-hosting-package', $user), [
|
||||
'hosting_product_id' => $product->id,
|
||||
'duration_months' => 12,
|
||||
'primary_domain' => 'second-example.com',
|
||||
'username' => 'secondacct',
|
||||
]);
|
||||
|
||||
$first->assertRedirect();
|
||||
$second->assertRedirect();
|
||||
|
||||
$accounts = HostingAccount::query()
|
||||
->where('user_id', $user->id)
|
||||
->where('hosting_product_id', $product->id)
|
||||
->orderBy('username')
|
||||
->get();
|
||||
|
||||
$this->assertCount(2, $accounts);
|
||||
$this->assertSame(['firstacct', 'secondacct'], $accounts->pluck('username')->all());
|
||||
$this->assertSame(['first-example.com', 'second-example.com'], $accounts->pluck('primary_domain')->all());
|
||||
}
|
||||
|
||||
public function test_admin_can_update_duration_for_existing_hosting_account(): void
|
||||
{
|
||||
Carbon::setTestNow('2026-04-05 10:00:00');
|
||||
|
||||
$admin = User::factory()->create(['is_admin' => true]);
|
||||
$user = User::factory()->create();
|
||||
$product = $this->createHostingProduct();
|
||||
|
||||
$account = HostingAccount::query()->create([
|
||||
'user_id' => $user->id,
|
||||
'hosting_product_id' => $product->id,
|
||||
'username' => 'existinguser',
|
||||
'type' => 'shared',
|
||||
'status' => 'active',
|
||||
'expires_at' => Carbon::parse('2026-05-05 10:00:00'),
|
||||
'metadata' => [
|
||||
'assigned_duration_months' => 1,
|
||||
],
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($admin)->patch(route('admin.users.hosting-accounts.update-duration', [$user, $account]), [
|
||||
'duration_months' => 12,
|
||||
]);
|
||||
|
||||
$response->assertRedirect();
|
||||
|
||||
$account->refresh();
|
||||
|
||||
$this->assertTrue($account->expires_at->equalTo(Carbon::parse('2027-04-05 10:00:00')));
|
||||
$this->assertSame(12, data_get($account->metadata, 'assigned_duration_months'));
|
||||
$this->assertSame($admin->id, data_get($account->metadata, 'duration_updated_by_admin'));
|
||||
$this->assertNotNull(data_get($account->metadata, 'duration_updated_at'));
|
||||
}
|
||||
|
||||
public function test_admin_can_renew_existing_hosting_account_using_assigned_duration(): void
|
||||
{
|
||||
Carbon::setTestNow('2026-04-05 10:00:00');
|
||||
|
||||
$admin = User::factory()->create(['is_admin' => true]);
|
||||
$user = User::factory()->create();
|
||||
$product = $this->createHostingProduct();
|
||||
|
||||
$account = HostingAccount::query()->create([
|
||||
'user_id' => $user->id,
|
||||
'hosting_product_id' => $product->id,
|
||||
'username' => 'renewadmin',
|
||||
'type' => 'shared',
|
||||
'status' => 'active',
|
||||
'expires_at' => Carbon::parse('2026-10-05 10:00:00'),
|
||||
'metadata' => [
|
||||
'assigned_duration_months' => 6,
|
||||
],
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($admin)->post(route('admin.users.hosting-accounts.renew', [$user, $account]));
|
||||
|
||||
$response->assertRedirect();
|
||||
|
||||
$account->refresh();
|
||||
|
||||
$this->assertTrue($account->expires_at->equalTo(Carbon::parse('2027-04-05 10:00:00')));
|
||||
$this->assertSame($admin->id, data_get($account->metadata, 'renewed_by_admin'));
|
||||
$this->assertNotNull(data_get($account->metadata, 'renewed_at'));
|
||||
}
|
||||
|
||||
public function test_customer_renewal_starts_payment_checkout_instead_of_renewing_immediately(): void
|
||||
{
|
||||
Carbon::setTestNow('2026-04-05 10:00:00');
|
||||
|
||||
$user = User::factory()->create();
|
||||
$product = $this->createHostingProduct();
|
||||
|
||||
$account = HostingAccount::query()->create([
|
||||
'user_id' => $user->id,
|
||||
'hosting_product_id' => $product->id,
|
||||
'username' => 'renewcustomer',
|
||||
'type' => 'shared',
|
||||
'status' => 'active',
|
||||
'expires_at' => Carbon::parse('2026-05-05 10:00:00'),
|
||||
'metadata' => [
|
||||
'assigned_duration_months' => 3,
|
||||
],
|
||||
]);
|
||||
|
||||
$paystack = \Mockery::mock(PaystackService::class);
|
||||
$paystack->shouldReceive('initializeTransaction')
|
||||
->once()
|
||||
->andReturn([
|
||||
'authorization_url' => 'https://paystack.test/renew/authorize',
|
||||
]);
|
||||
$this->app->instance(PaystackService::class, $paystack);
|
||||
|
||||
$response = $this->actingAs($user)->post(route('hosting.accounts.renew', $account));
|
||||
|
||||
$response->assertRedirect('https://paystack.test/renew/authorize');
|
||||
|
||||
$account->refresh();
|
||||
|
||||
$this->assertTrue($account->expires_at->equalTo(Carbon::parse('2026-05-05 10:00:00')));
|
||||
$this->assertNull(data_get($account->metadata, 'renewed_by_user'));
|
||||
$this->assertDatabaseHas('hosting_billing_invoices', [
|
||||
'hosting_account_id' => $account->id,
|
||||
'invoice_type' => HostingBillingInvoice::TYPE_RENEWAL,
|
||||
'status' => HostingBillingInvoice::STATUS_PENDING,
|
||||
'total_amount' => 30,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_customer_renewal_callback_applies_extension_after_successful_payment(): void
|
||||
{
|
||||
Carbon::setTestNow('2026-04-05 10:00:00');
|
||||
|
||||
$user = User::factory()->create();
|
||||
$product = $this->createHostingProduct();
|
||||
|
||||
$account = HostingAccount::query()->create([
|
||||
'user_id' => $user->id,
|
||||
'hosting_product_id' => $product->id,
|
||||
'username' => 'renewcallback',
|
||||
'type' => 'shared',
|
||||
'status' => 'active',
|
||||
'expires_at' => Carbon::parse('2026-05-05 10:00:00'),
|
||||
'metadata' => [
|
||||
'assigned_duration_months' => 3,
|
||||
],
|
||||
]);
|
||||
|
||||
$invoice = HostingBillingInvoice::query()->create([
|
||||
'user_id' => $user->id,
|
||||
'hosting_account_id' => $account->id,
|
||||
'invoice_type' => HostingBillingInvoice::TYPE_RENEWAL,
|
||||
'status' => HostingBillingInvoice::STATUS_PENDING,
|
||||
'currency' => 'GHS',
|
||||
'subtotal_amount' => 30,
|
||||
'credit_amount' => 0,
|
||||
'total_amount' => 30,
|
||||
'provider_reference' => 'HOST-REN-CALLBACK',
|
||||
'metadata' => [
|
||||
'months' => 3,
|
||||
'billing_cycle' => 'quarterly',
|
||||
],
|
||||
]);
|
||||
|
||||
$paystack = \Mockery::mock(PaystackService::class);
|
||||
$paystack->shouldReceive('verifyTransaction')
|
||||
->once()
|
||||
->with('HOST-REN-CALLBACK')
|
||||
->andReturn([
|
||||
'reference' => 'HOST-REN-CALLBACK',
|
||||
'status' => 'success',
|
||||
]);
|
||||
$this->app->instance(PaystackService::class, $paystack);
|
||||
|
||||
$response = $this->actingAs($user)->get(route('hosting.accounts.renew.callback', $account) . '?reference=HOST-REN-CALLBACK');
|
||||
|
||||
$response->assertRedirect(route('hosting.accounts.show', $account));
|
||||
|
||||
$account->refresh();
|
||||
$invoice->refresh();
|
||||
|
||||
$this->assertTrue($account->expires_at->equalTo(Carbon::parse('2026-08-05 10:00:00')));
|
||||
$this->assertSame($user->id, data_get($account->metadata, 'renewed_by_user'));
|
||||
$this->assertNotNull($invoice->paid_at);
|
||||
$this->assertSame(HostingBillingInvoice::STATUS_PAID, $invoice->status);
|
||||
}
|
||||
|
||||
private function createHostingProduct(): HostingProduct
|
||||
{
|
||||
return HostingProduct::query()->create([
|
||||
'name' => 'Admin Assigned Shared Hosting',
|
||||
'slug' => 'admin-assigned-shared-hosting',
|
||||
'category' => 'shared',
|
||||
'type' => 'single_domain',
|
||||
'price_monthly' => 10,
|
||||
'price_quarterly' => 30,
|
||||
'price_yearly' => 120,
|
||||
'price_biennial' => 240,
|
||||
'currency' => 'GHS',
|
||||
'max_domains' => 1,
|
||||
'is_active' => true,
|
||||
'is_visible' => true,
|
||||
'sort_order' => 1,
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user