Add notification system, notification bell, admin/account tests, and footer legal links
- 6 notification classes: PaymentSucceeded, PaymentFailed, SubscriptionCreated, SubscriptionCancelled, ServiceProvisioned, InvoiceGenerated (mail + database) - Wire notifications to existing event listeners + new subscription listeners - NotificationBell component in Account and Admin layouts - NotificationController with index, markAsRead, markAllAsRead endpoints - 62 new Pest tests: AdminPanelTest (admin CRUD) + CustomerAccountTest (account features) - Add Legal links column to marketing footer - 114 tests passing (623 assertions) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
284
website/tests/Feature/Account/CustomerAccountTest.php
Normal file
284
website/tests/Feature/Account/CustomerAccountTest.php
Normal file
@@ -0,0 +1,284 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Models\Invoice;
|
||||
use App\Models\Plan;
|
||||
use App\Models\Service;
|
||||
use App\Models\User;
|
||||
use Database\Seeders\RoleAndPermissionSeeder;
|
||||
|
||||
beforeEach(function (): void {
|
||||
$this->seed(RoleAndPermissionSeeder::class);
|
||||
$this->accountUrl = 'http://'.config('app.domains.account');
|
||||
});
|
||||
|
||||
describe('Dashboard', function (): void {
|
||||
it('allows a customer to view the dashboard', function (): void {
|
||||
$customer = User::factory()->customer()->create();
|
||||
|
||||
$this->actingAs($customer)
|
||||
->get($this->accountUrl.'/dashboard')
|
||||
->assertOk()
|
||||
->assertInertia(fn ($page) => $page
|
||||
->component('Dashboard')
|
||||
->has('activeServicesCount')
|
||||
->has('activeSubscriptionsCount')
|
||||
->has('pendingInvoicesAmount')
|
||||
->has('latestInvoices')
|
||||
);
|
||||
});
|
||||
|
||||
it('redirects guests to login', function (): void {
|
||||
$this->get($this->accountUrl.'/dashboard')
|
||||
->assertRedirect();
|
||||
});
|
||||
|
||||
it('shows correct active services count', function (): void {
|
||||
$customer = User::factory()->customer()->create();
|
||||
Service::factory()->count(3)->create(['user_id' => $customer->id, 'status' => 'active']);
|
||||
Service::factory()->create(['user_id' => $customer->id, 'status' => 'suspended']);
|
||||
|
||||
$this->actingAs($customer)
|
||||
->get($this->accountUrl.'/dashboard')
|
||||
->assertOk()
|
||||
->assertInertia(fn ($page) => $page
|
||||
->component('Dashboard')
|
||||
->where('activeServicesCount', 3)
|
||||
);
|
||||
});
|
||||
|
||||
it('shows correct pending invoices amount', function (): void {
|
||||
$customer = User::factory()->customer()->create();
|
||||
Invoice::factory()->create(['user_id' => $customer->id, 'status' => 'pending', 'total' => 50.00]);
|
||||
Invoice::factory()->create(['user_id' => $customer->id, 'status' => 'overdue', 'total' => 25.50]);
|
||||
Invoice::factory()->create(['user_id' => $customer->id, 'status' => 'paid', 'total' => 100.00]);
|
||||
|
||||
$this->actingAs($customer)
|
||||
->get($this->accountUrl.'/dashboard')
|
||||
->assertOk()
|
||||
->assertInertia(fn ($page) => $page
|
||||
->component('Dashboard')
|
||||
->where('pendingInvoicesAmount', '75.50')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Profile', function (): void {
|
||||
it('allows a customer to view their profile', function (): void {
|
||||
$customer = User::factory()->customer()->create();
|
||||
|
||||
$this->actingAs($customer)
|
||||
->get($this->accountUrl.'/profile')
|
||||
->assertOk()
|
||||
->assertInertia(fn ($page) => $page
|
||||
->component('Profile/Show')
|
||||
->has('user')
|
||||
->has('twoFactorEnabled')
|
||||
);
|
||||
});
|
||||
|
||||
it('allows a customer to update their profile', function (): void {
|
||||
$customer = User::factory()->customer()->create();
|
||||
|
||||
$this->actingAs($customer)
|
||||
->put($this->accountUrl.'/profile', [
|
||||
'first_name' => 'Jane',
|
||||
'last_name' => 'Smith',
|
||||
'phone' => '+1234567890',
|
||||
'company' => 'Acme Inc',
|
||||
])
|
||||
->assertRedirect()
|
||||
->assertSessionHas('success', 'Profile updated successfully.');
|
||||
|
||||
$customer->refresh();
|
||||
expect($customer->name)->toBe('Jane Smith');
|
||||
expect($customer->phone)->toBe('+1234567890');
|
||||
expect($customer->company)->toBe('Acme Inc');
|
||||
});
|
||||
|
||||
it('validates profile update requires first and last name', function (): void {
|
||||
$customer = User::factory()->customer()->create();
|
||||
|
||||
$this->actingAs($customer)
|
||||
->put($this->accountUrl.'/profile', [
|
||||
'first_name' => '',
|
||||
'last_name' => '',
|
||||
])
|
||||
->assertSessionHasErrors(['first_name', 'last_name']);
|
||||
});
|
||||
|
||||
it('allows a customer to update their password', function (): void {
|
||||
$customer = User::factory()->customer()->create();
|
||||
|
||||
$this->actingAs($customer)
|
||||
->put($this->accountUrl.'/profile/password', [
|
||||
'current_password' => 'password',
|
||||
'password' => 'NewSecurePass1!',
|
||||
'password_confirmation' => 'NewSecurePass1!',
|
||||
])
|
||||
->assertRedirect()
|
||||
->assertSessionHas('success', 'Password updated successfully.');
|
||||
});
|
||||
|
||||
it('rejects password update with wrong current password', function (): void {
|
||||
$customer = User::factory()->customer()->create();
|
||||
|
||||
$this->actingAs($customer)
|
||||
->put($this->accountUrl.'/profile/password', [
|
||||
'current_password' => 'wrong-password',
|
||||
'password' => 'NewSecurePass1!',
|
||||
'password_confirmation' => 'NewSecurePass1!',
|
||||
])
|
||||
->assertSessionHasErrors('current_password');
|
||||
});
|
||||
|
||||
it('rejects password update when confirmation does not match', function (): void {
|
||||
$customer = User::factory()->customer()->create();
|
||||
|
||||
$this->actingAs($customer)
|
||||
->put($this->accountUrl.'/profile/password', [
|
||||
'current_password' => 'password',
|
||||
'password' => 'NewSecurePass1!',
|
||||
'password_confirmation' => 'DifferentPass2!',
|
||||
])
|
||||
->assertSessionHasErrors('password');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Plans', function (): void {
|
||||
it('displays active plans to a customer', function (): void {
|
||||
$customer = User::factory()->customer()->create();
|
||||
Plan::factory()->count(3)->create(['status' => 'active']);
|
||||
Plan::factory()->create(['status' => 'inactive']);
|
||||
|
||||
$this->actingAs($customer)
|
||||
->get($this->accountUrl.'/plans')
|
||||
->assertOk()
|
||||
->assertInertia(fn ($page) => $page
|
||||
->component('Plans/Index')
|
||||
->has('plansByType')
|
||||
);
|
||||
});
|
||||
|
||||
it('displays a single plan detail page', function (): void {
|
||||
$customer = User::factory()->customer()->create();
|
||||
$plan = Plan::factory()->create(['status' => 'active']);
|
||||
|
||||
$this->actingAs($customer)
|
||||
->get($this->accountUrl.'/plans/'.$plan->id)
|
||||
->assertOk()
|
||||
->assertInertia(fn ($page) => $page
|
||||
->component('Plans/Show')
|
||||
->has('plan')
|
||||
->where('plan.id', $plan->id)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Services', function (): void {
|
||||
it('displays only the customers own services', function (): void {
|
||||
$customer = User::factory()->customer()->create();
|
||||
$otherUser = User::factory()->customer()->create();
|
||||
|
||||
Service::factory()->count(2)->create(['user_id' => $customer->id]);
|
||||
Service::factory()->count(3)->create(['user_id' => $otherUser->id]);
|
||||
|
||||
$this->actingAs($customer)
|
||||
->get($this->accountUrl.'/services')
|
||||
->assertOk()
|
||||
->assertInertia(fn ($page) => $page
|
||||
->component('Services/Index')
|
||||
->has('services', 2)
|
||||
);
|
||||
});
|
||||
|
||||
it('allows a customer to view their own service detail', function (): void {
|
||||
$customer = User::factory()->customer()->create();
|
||||
$service = Service::factory()->create(['user_id' => $customer->id]);
|
||||
|
||||
$this->actingAs($customer)
|
||||
->get($this->accountUrl.'/services/'.$service->id)
|
||||
->assertOk()
|
||||
->assertInertia(fn ($page) => $page
|
||||
->component('Services/Show')
|
||||
->has('service')
|
||||
->where('service.id', $service->id)
|
||||
);
|
||||
});
|
||||
|
||||
it('forbids a customer from viewing another users service', function (): void {
|
||||
$customer = User::factory()->customer()->create();
|
||||
$otherUser = User::factory()->customer()->create();
|
||||
$service = Service::factory()->create(['user_id' => $otherUser->id]);
|
||||
|
||||
$this->actingAs($customer)
|
||||
->get($this->accountUrl.'/services/'.$service->id)
|
||||
->assertForbidden();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Subscriptions', function (): void {
|
||||
it('displays the subscriptions list page', function (): void {
|
||||
$customer = User::factory()->customer()->create();
|
||||
|
||||
$this->actingAs($customer)
|
||||
->get($this->accountUrl.'/subscriptions')
|
||||
->assertOk()
|
||||
->assertInertia(fn ($page) => $page
|
||||
->component('Subscriptions/Index')
|
||||
->has('subscriptions')
|
||||
);
|
||||
});
|
||||
|
||||
it('requires authentication to view subscriptions', function (): void {
|
||||
$this->get($this->accountUrl.'/subscriptions')
|
||||
->assertRedirect();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Billing', function (): void {
|
||||
it('displays the billing index page', function (): void {
|
||||
$customer = User::factory()->customer()->create();
|
||||
|
||||
$this->actingAs($customer)
|
||||
->get($this->accountUrl.'/billing')
|
||||
->assertOk()
|
||||
->assertInertia(fn ($page) => $page
|
||||
->component('Billing/Index')
|
||||
->has('paymentMethods')
|
||||
->has('invoices')
|
||||
->has('transactions')
|
||||
);
|
||||
});
|
||||
|
||||
it('displays the invoices page with customer invoices', function (): void {
|
||||
$customer = User::factory()->customer()->create();
|
||||
Invoice::factory()->count(3)->create(['user_id' => $customer->id]);
|
||||
|
||||
$this->actingAs($customer)
|
||||
->get($this->accountUrl.'/billing/invoices')
|
||||
->assertOk()
|
||||
->assertInertia(fn ($page) => $page
|
||||
->component('Billing/Invoices')
|
||||
->has('invoices.data', 3)
|
||||
);
|
||||
});
|
||||
|
||||
it('displays the transactions page', function (): void {
|
||||
$customer = User::factory()->customer()->create();
|
||||
|
||||
$this->actingAs($customer)
|
||||
->get($this->accountUrl.'/billing/transactions')
|
||||
->assertOk()
|
||||
->assertInertia(fn ($page) => $page
|
||||
->component('Billing/Transactions')
|
||||
->has('transactions')
|
||||
);
|
||||
});
|
||||
|
||||
it('requires authentication to view billing', function (): void {
|
||||
$this->get($this->accountUrl.'/billing')
|
||||
->assertRedirect();
|
||||
});
|
||||
});
|
||||
635
website/tests/Feature/Admin/AdminPanelTest.php
Normal file
635
website/tests/Feature/Admin/AdminPanelTest.php
Normal file
@@ -0,0 +1,635 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Models\AuditLog;
|
||||
use App\Models\Coupon;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\Plan;
|
||||
use App\Models\Service;
|
||||
use App\Models\Setting;
|
||||
use App\Models\User;
|
||||
use Database\Seeders\RoleAndPermissionSeeder;
|
||||
|
||||
beforeEach(function (): void {
|
||||
$this->seed(RoleAndPermissionSeeder::class);
|
||||
$this->adminUrl = 'http://'.config('app.domains.admin');
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Dashboard
|
||||
// ---------------------------------------------------------------------------
|
||||
describe('Dashboard', function (): void {
|
||||
it('allows admin to access the dashboard', function (): void {
|
||||
$admin = User::factory()->admin()->create();
|
||||
|
||||
$this->actingAs($admin)
|
||||
->get($this->adminUrl.'/dashboard')
|
||||
->assertOk()
|
||||
->assertInertia(fn ($page) => $page
|
||||
->component('Admin/Dashboard')
|
||||
->has('totalCustomers')
|
||||
->has('mrr')
|
||||
->has('totalRevenue')
|
||||
->has('activeServices')
|
||||
->has('recentInvoices')
|
||||
->has('recentSubscriptions')
|
||||
->has('popularPlans')
|
||||
);
|
||||
});
|
||||
|
||||
it('denies customer access to the admin dashboard', function (): void {
|
||||
$customer = User::factory()->customer()->create();
|
||||
|
||||
$this->actingAs($customer)
|
||||
->get($this->adminUrl.'/dashboard')
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
it('redirects guest to login when accessing admin dashboard', function (): void {
|
||||
$this->get($this->adminUrl.'/dashboard')
|
||||
->assertRedirect();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Customer Management
|
||||
// ---------------------------------------------------------------------------
|
||||
describe('Customer Management', function (): void {
|
||||
it('displays the customer list page', function (): void {
|
||||
$admin = User::factory()->admin()->create();
|
||||
User::factory()->customer()->count(3)->create();
|
||||
|
||||
$this->actingAs($admin)
|
||||
->get($this->adminUrl.'/customers')
|
||||
->assertOk()
|
||||
->assertInertia(fn ($page) => $page
|
||||
->component('Admin/Customers/Index')
|
||||
->has('customers.data', 3)
|
||||
->has('filters')
|
||||
);
|
||||
});
|
||||
|
||||
it('filters customers by search query', function (): void {
|
||||
$admin = User::factory()->admin()->create();
|
||||
User::factory()->customer()->create(['name' => 'Alice Wonderland']);
|
||||
User::factory()->customer()->create(['name' => 'Bob Builder']);
|
||||
|
||||
$this->actingAs($admin)
|
||||
->get($this->adminUrl.'/customers?search=Alice')
|
||||
->assertOk()
|
||||
->assertInertia(fn ($page) => $page
|
||||
->component('Admin/Customers/Index')
|
||||
->has('customers.data', 1)
|
||||
);
|
||||
});
|
||||
|
||||
it('shows a customer detail page with services and invoices', function (): void {
|
||||
$admin = User::factory()->admin()->create();
|
||||
$customer = User::factory()->customer()->create();
|
||||
Service::factory()->create(['user_id' => $customer->id]);
|
||||
Invoice::factory()->count(2)->create(['user_id' => $customer->id]);
|
||||
|
||||
$this->actingAs($admin)
|
||||
->get($this->adminUrl.'/customers/'.$customer->id)
|
||||
->assertOk()
|
||||
->assertInertia(fn ($page) => $page
|
||||
->component('Admin/Customers/Show')
|
||||
->has('customer')
|
||||
->has('recentInvoices')
|
||||
->has('auditLogs')
|
||||
);
|
||||
});
|
||||
|
||||
it('suspends a customer account', function (): void {
|
||||
$admin = User::factory()->admin()->create();
|
||||
$customer = User::factory()->customer()->create();
|
||||
|
||||
$this->actingAs($admin)
|
||||
->post($this->adminUrl.'/customers/'.$customer->id.'/suspend')
|
||||
->assertRedirect();
|
||||
|
||||
expect($customer->fresh()->status)->toBe('suspended');
|
||||
$this->assertDatabaseHas('audit_logs', [
|
||||
'user_id' => $customer->id,
|
||||
'admin_id' => $admin->id,
|
||||
'action' => 'suspend_account',
|
||||
]);
|
||||
});
|
||||
|
||||
it('unsuspends a customer account', function (): void {
|
||||
$admin = User::factory()->admin()->create();
|
||||
$customer = User::factory()->customer()->suspended()->create();
|
||||
|
||||
$this->actingAs($admin)
|
||||
->post($this->adminUrl.'/customers/'.$customer->id.'/unsuspend')
|
||||
->assertRedirect();
|
||||
|
||||
expect($customer->fresh()->status)->toBe('active');
|
||||
$this->assertDatabaseHas('audit_logs', [
|
||||
'user_id' => $customer->id,
|
||||
'admin_id' => $admin->id,
|
||||
'action' => 'unsuspend_account',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Plan Management
|
||||
// ---------------------------------------------------------------------------
|
||||
describe('Plan Management', function (): void {
|
||||
it('displays the plan index page', function (): void {
|
||||
$admin = User::factory()->admin()->create();
|
||||
Plan::factory()->count(4)->create();
|
||||
|
||||
$this->actingAs($admin)
|
||||
->get($this->adminUrl.'/plans')
|
||||
->assertOk()
|
||||
->assertInertia(fn ($page) => $page
|
||||
->component('Admin/Plans/Index')
|
||||
->has('plans', 4)
|
||||
->has('filters')
|
||||
);
|
||||
});
|
||||
|
||||
it('filters plans by service type', function (): void {
|
||||
$admin = User::factory()->admin()->create();
|
||||
Plan::factory()->create(['service_type' => 'vps']);
|
||||
Plan::factory()->create(['service_type' => 'dedicated']);
|
||||
Plan::factory()->create(['service_type' => 'vps']);
|
||||
|
||||
$this->actingAs($admin)
|
||||
->get($this->adminUrl.'/plans?service_type=vps')
|
||||
->assertOk()
|
||||
->assertInertia(fn ($page) => $page
|
||||
->component('Admin/Plans/Index')
|
||||
->has('plans', 2)
|
||||
);
|
||||
});
|
||||
|
||||
it('displays the create plan page', function (): void {
|
||||
$admin = User::factory()->admin()->create();
|
||||
|
||||
$this->actingAs($admin)
|
||||
->get($this->adminUrl.'/plans/create')
|
||||
->assertOk()
|
||||
->assertInertia(fn ($page) => $page
|
||||
->component('Admin/Plans/Create')
|
||||
);
|
||||
});
|
||||
|
||||
it('stores a new plan', function (): void {
|
||||
$admin = User::factory()->admin()->create();
|
||||
|
||||
$this->actingAs($admin)
|
||||
->post($this->adminUrl.'/plans', [
|
||||
'name' => 'Starter VPS',
|
||||
'slug' => 'starter-vps',
|
||||
'description' => 'A basic VPS plan',
|
||||
'service_type' => 'vps',
|
||||
'price' => 9.99,
|
||||
'billing_cycle' => 'monthly',
|
||||
'features' => [
|
||||
['key' => 'cpu', 'value' => '2 vCPU'],
|
||||
['key' => 'ram', 'value' => '4GB'],
|
||||
],
|
||||
'stock_quantity' => 100,
|
||||
'sort_order' => 1,
|
||||
])
|
||||
->assertRedirect();
|
||||
|
||||
$this->assertDatabaseHas('plans', [
|
||||
'name' => 'Starter VPS',
|
||||
'slug' => 'starter-vps',
|
||||
'service_type' => 'vps',
|
||||
'status' => 'active',
|
||||
]);
|
||||
});
|
||||
|
||||
it('validates required fields when storing a plan', function (): void {
|
||||
$admin = User::factory()->admin()->create();
|
||||
|
||||
$this->actingAs($admin)
|
||||
->post($this->adminUrl.'/plans', [])
|
||||
->assertSessionHasErrors(['name', 'slug', 'service_type', 'price', 'billing_cycle']);
|
||||
});
|
||||
|
||||
it('displays the edit plan page', function (): void {
|
||||
$admin = User::factory()->admin()->create();
|
||||
$plan = Plan::factory()->create();
|
||||
|
||||
$this->actingAs($admin)
|
||||
->get($this->adminUrl.'/plans/'.$plan->id.'/edit')
|
||||
->assertOk()
|
||||
->assertInertia(fn ($page) => $page
|
||||
->component('Admin/Plans/Edit')
|
||||
->has('plan')
|
||||
->has('subscribersCount')
|
||||
);
|
||||
});
|
||||
|
||||
it('updates an existing plan', function (): void {
|
||||
$admin = User::factory()->admin()->create();
|
||||
$plan = Plan::factory()->create(['name' => 'Old Name', 'slug' => 'old-name']);
|
||||
|
||||
$this->actingAs($admin)
|
||||
->put($this->adminUrl.'/plans/'.$plan->id, [
|
||||
'name' => 'New Name',
|
||||
'slug' => 'new-name',
|
||||
'service_type' => $plan->service_type,
|
||||
'price' => 19.99,
|
||||
'billing_cycle' => 'monthly',
|
||||
'sort_order' => 0,
|
||||
])
|
||||
->assertRedirect();
|
||||
|
||||
expect($plan->fresh())
|
||||
->name->toBe('New Name')
|
||||
->slug->toBe('new-name');
|
||||
});
|
||||
|
||||
it('archives a plan on destroy', function (): void {
|
||||
$admin = User::factory()->admin()->create();
|
||||
$plan = Plan::factory()->create(['status' => 'active']);
|
||||
|
||||
$this->actingAs($admin)
|
||||
->delete($this->adminUrl.'/plans/'.$plan->id)
|
||||
->assertRedirect();
|
||||
|
||||
expect($plan->fresh()->status)->toBe('inactive');
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Service Management
|
||||
// ---------------------------------------------------------------------------
|
||||
describe('Service Management', function (): void {
|
||||
it('displays the service list page', function (): void {
|
||||
$admin = User::factory()->admin()->create();
|
||||
$customer = User::factory()->customer()->create();
|
||||
Service::factory()->count(3)->create(['user_id' => $customer->id]);
|
||||
|
||||
$this->actingAs($admin)
|
||||
->get($this->adminUrl.'/services')
|
||||
->assertOk()
|
||||
->assertInertia(fn ($page) => $page
|
||||
->component('Admin/Services/Index')
|
||||
->has('services.data', 3)
|
||||
->has('filters')
|
||||
);
|
||||
});
|
||||
|
||||
it('shows a service detail page', function (): void {
|
||||
$admin = User::factory()->admin()->create();
|
||||
$service = Service::factory()->create();
|
||||
|
||||
$this->actingAs($admin)
|
||||
->get($this->adminUrl.'/services/'.$service->id)
|
||||
->assertOk()
|
||||
->assertInertia(fn ($page) => $page
|
||||
->component('Admin/Services/Show')
|
||||
->has('service')
|
||||
);
|
||||
});
|
||||
|
||||
it('suspends a service', function (): void {
|
||||
$admin = User::factory()->admin()->create();
|
||||
$service = Service::factory()->create(['status' => 'active']);
|
||||
|
||||
$this->actingAs($admin)
|
||||
->post($this->adminUrl.'/services/'.$service->id.'/suspend')
|
||||
->assertRedirect();
|
||||
|
||||
expect($service->fresh()->status)->toBe('suspended');
|
||||
$this->assertDatabaseHas('audit_logs', [
|
||||
'user_id' => $service->user_id,
|
||||
'admin_id' => $admin->id,
|
||||
'action' => 'suspend_service',
|
||||
'resource_type' => 'service',
|
||||
'resource_id' => $service->id,
|
||||
]);
|
||||
});
|
||||
|
||||
it('unsuspends a service', function (): void {
|
||||
$admin = User::factory()->admin()->create();
|
||||
$service = Service::factory()->suspended()->create();
|
||||
|
||||
$this->actingAs($admin)
|
||||
->post($this->adminUrl.'/services/'.$service->id.'/unsuspend')
|
||||
->assertRedirect();
|
||||
|
||||
expect($service->fresh()->status)->toBe('active');
|
||||
$this->assertDatabaseHas('audit_logs', [
|
||||
'action' => 'unsuspend_service',
|
||||
'resource_id' => $service->id,
|
||||
]);
|
||||
});
|
||||
|
||||
it('terminates a service', function (): void {
|
||||
$admin = User::factory()->admin()->create();
|
||||
$service = Service::factory()->create(['status' => 'active']);
|
||||
|
||||
$this->actingAs($admin)
|
||||
->post($this->adminUrl.'/services/'.$service->id.'/terminate')
|
||||
->assertRedirect();
|
||||
|
||||
$freshService = $service->fresh();
|
||||
expect($freshService->status)->toBe('terminated');
|
||||
expect($freshService->terminated_at)->not->toBeNull();
|
||||
$this->assertDatabaseHas('audit_logs', [
|
||||
'action' => 'terminate_service',
|
||||
'resource_id' => $service->id,
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Invoice Management
|
||||
// ---------------------------------------------------------------------------
|
||||
describe('Invoice Management', function (): void {
|
||||
it('displays the invoice list page', function (): void {
|
||||
$admin = User::factory()->admin()->create();
|
||||
$customer = User::factory()->customer()->create();
|
||||
Invoice::factory()->count(5)->create(['user_id' => $customer->id]);
|
||||
|
||||
$this->actingAs($admin)
|
||||
->get($this->adminUrl.'/invoices')
|
||||
->assertOk()
|
||||
->assertInertia(fn ($page) => $page
|
||||
->component('Admin/Invoices/Index')
|
||||
->has('invoices.data', 5)
|
||||
->has('filters')
|
||||
);
|
||||
});
|
||||
|
||||
it('filters invoices by status', function (): void {
|
||||
$admin = User::factory()->admin()->create();
|
||||
$customer = User::factory()->customer()->create();
|
||||
Invoice::factory()->count(2)->create(['user_id' => $customer->id, 'status' => 'paid']);
|
||||
Invoice::factory()->count(3)->create(['user_id' => $customer->id, 'status' => 'draft']);
|
||||
|
||||
$this->actingAs($admin)
|
||||
->get($this->adminUrl.'/invoices?status=paid')
|
||||
->assertOk()
|
||||
->assertInertia(fn ($page) => $page
|
||||
->component('Admin/Invoices/Index')
|
||||
->has('invoices.data', 2)
|
||||
);
|
||||
});
|
||||
|
||||
it('shows an invoice detail page', function (): void {
|
||||
$admin = User::factory()->admin()->create();
|
||||
$invoice = Invoice::factory()->create();
|
||||
|
||||
$this->actingAs($admin)
|
||||
->get($this->adminUrl.'/invoices/'.$invoice->id)
|
||||
->assertOk()
|
||||
->assertInertia(fn ($page) => $page
|
||||
->component('Admin/Invoices/Show')
|
||||
->has('invoice')
|
||||
);
|
||||
});
|
||||
|
||||
it('voids an invoice', function (): void {
|
||||
$admin = User::factory()->admin()->create();
|
||||
$invoice = Invoice::factory()->create(['status' => 'sent']);
|
||||
|
||||
$this->actingAs($admin)
|
||||
->post($this->adminUrl.'/invoices/'.$invoice->id.'/void')
|
||||
->assertRedirect();
|
||||
|
||||
expect($invoice->fresh()->status)->toBe('void');
|
||||
$this->assertDatabaseHas('audit_logs', [
|
||||
'user_id' => $invoice->user_id,
|
||||
'admin_id' => $admin->id,
|
||||
'action' => 'void_invoice',
|
||||
'resource_type' => 'invoice',
|
||||
'resource_id' => $invoice->id,
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Coupon Management
|
||||
// ---------------------------------------------------------------------------
|
||||
describe('Coupon Management', function (): void {
|
||||
it('displays the coupon index page', function (): void {
|
||||
$admin = User::factory()->admin()->create();
|
||||
Coupon::factory()->count(3)->create();
|
||||
|
||||
$this->actingAs($admin)
|
||||
->get($this->adminUrl.'/coupons')
|
||||
->assertOk()
|
||||
->assertInertia(fn ($page) => $page
|
||||
->component('Admin/Coupons/Index')
|
||||
->has('coupons.data', 3)
|
||||
);
|
||||
});
|
||||
|
||||
it('displays the create coupon page', function (): void {
|
||||
$admin = User::factory()->admin()->create();
|
||||
|
||||
$this->actingAs($admin)
|
||||
->get($this->adminUrl.'/coupons/create')
|
||||
->assertOk()
|
||||
->assertInertia(fn ($page) => $page
|
||||
->component('Admin/Coupons/Create')
|
||||
->has('plans')
|
||||
);
|
||||
});
|
||||
|
||||
it('stores a new coupon', function (): void {
|
||||
$admin = User::factory()->admin()->create();
|
||||
|
||||
$this->actingAs($admin)
|
||||
->post($this->adminUrl.'/coupons', [
|
||||
'code' => 'SAVE20',
|
||||
'type' => 'percentage',
|
||||
'value' => 20,
|
||||
'max_uses' => 100,
|
||||
'expires_at' => now()->addMonth()->toDateTimeString(),
|
||||
])
|
||||
->assertRedirect();
|
||||
|
||||
$this->assertDatabaseHas('coupons', [
|
||||
'code' => 'SAVE20',
|
||||
'type' => 'percentage',
|
||||
'active' => true,
|
||||
]);
|
||||
});
|
||||
|
||||
it('validates required fields when storing a coupon', function (): void {
|
||||
$admin = User::factory()->admin()->create();
|
||||
|
||||
$this->actingAs($admin)
|
||||
->post($this->adminUrl.'/coupons', [])
|
||||
->assertSessionHasErrors(['code', 'type', 'value']);
|
||||
});
|
||||
|
||||
it('prevents duplicate coupon codes', function (): void {
|
||||
$admin = User::factory()->admin()->create();
|
||||
Coupon::factory()->create(['code' => 'EXISTING']);
|
||||
|
||||
$this->actingAs($admin)
|
||||
->post($this->adminUrl.'/coupons', [
|
||||
'code' => 'EXISTING',
|
||||
'type' => 'percentage',
|
||||
'value' => 10,
|
||||
])
|
||||
->assertSessionHasErrors(['code']);
|
||||
});
|
||||
|
||||
it('displays the edit coupon page', function (): void {
|
||||
$admin = User::factory()->admin()->create();
|
||||
$coupon = Coupon::factory()->create();
|
||||
|
||||
$this->actingAs($admin)
|
||||
->get($this->adminUrl.'/coupons/'.$coupon->id.'/edit')
|
||||
->assertOk()
|
||||
->assertInertia(fn ($page) => $page
|
||||
->component('Admin/Coupons/Edit')
|
||||
->has('coupon')
|
||||
->has('plans')
|
||||
->has('redemptions')
|
||||
);
|
||||
});
|
||||
|
||||
it('updates an existing coupon', function (): void {
|
||||
$admin = User::factory()->admin()->create();
|
||||
$coupon = Coupon::factory()->create(['code' => 'OLD10', 'value' => 10]);
|
||||
|
||||
$this->actingAs($admin)
|
||||
->put($this->adminUrl.'/coupons/'.$coupon->id, [
|
||||
'code' => 'NEW25',
|
||||
'type' => 'fixed',
|
||||
'value' => 25,
|
||||
'max_uses' => 50,
|
||||
])
|
||||
->assertRedirect();
|
||||
|
||||
$fresh = $coupon->fresh();
|
||||
expect($fresh->code)->toBe('NEW25');
|
||||
expect((float) $fresh->value)->toBe(25.0);
|
||||
});
|
||||
|
||||
it('deactivates a coupon on destroy', function (): void {
|
||||
$admin = User::factory()->admin()->create();
|
||||
$coupon = Coupon::factory()->create(['active' => true]);
|
||||
|
||||
$this->actingAs($admin)
|
||||
->delete($this->adminUrl.'/coupons/'.$coupon->id)
|
||||
->assertRedirect();
|
||||
|
||||
expect($coupon->fresh()->active)->toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Audit Logs
|
||||
// ---------------------------------------------------------------------------
|
||||
describe('Audit Logs', function (): void {
|
||||
it('displays the audit log index page', function (): void {
|
||||
$admin = User::factory()->admin()->create();
|
||||
AuditLog::factory()->count(5)->create();
|
||||
|
||||
$this->actingAs($admin)
|
||||
->get($this->adminUrl.'/audit-logs')
|
||||
->assertOk()
|
||||
->assertInertia(fn ($page) => $page
|
||||
->component('Admin/AuditLogs/Index')
|
||||
->has('auditLogs.data', 5)
|
||||
->has('actions')
|
||||
->has('filters')
|
||||
);
|
||||
});
|
||||
|
||||
it('filters audit logs by action', function (): void {
|
||||
$admin = User::factory()->admin()->create();
|
||||
AuditLog::factory()->count(2)->create(['action' => 'login']);
|
||||
AuditLog::factory()->count(3)->create(['action' => 'payment_failed']);
|
||||
|
||||
$this->actingAs($admin)
|
||||
->get($this->adminUrl.'/audit-logs?action=login')
|
||||
->assertOk()
|
||||
->assertInertia(fn ($page) => $page
|
||||
->component('Admin/AuditLogs/Index')
|
||||
->has('auditLogs.data', 2)
|
||||
);
|
||||
});
|
||||
|
||||
it('denies customer access to audit logs', function (): void {
|
||||
$customer = User::factory()->customer()->create();
|
||||
|
||||
$this->actingAs($customer)
|
||||
->get($this->adminUrl.'/audit-logs')
|
||||
->assertForbidden();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Settings
|
||||
// ---------------------------------------------------------------------------
|
||||
describe('Settings', function (): void {
|
||||
it('displays the settings index page', function (): void {
|
||||
$admin = User::factory()->admin()->create();
|
||||
|
||||
$this->actingAs($admin)
|
||||
->get($this->adminUrl.'/settings')
|
||||
->assertOk()
|
||||
->assertInertia(fn ($page) => $page
|
||||
->component('Admin/Settings/Index')
|
||||
->has('settings')
|
||||
);
|
||||
});
|
||||
|
||||
it('updates general settings', function (): void {
|
||||
$admin = User::factory()->admin()->create();
|
||||
|
||||
$this->actingAs($admin)
|
||||
->put($this->adminUrl.'/settings', [
|
||||
'group' => 'general',
|
||||
'company_name' => 'EZSCALE Cloud',
|
||||
'company_email' => 'hello@ezscale.cloud',
|
||||
])
|
||||
->assertRedirect();
|
||||
|
||||
expect(Setting::get('company_name'))->toBe('EZSCALE Cloud');
|
||||
expect(Setting::get('company_email'))->toBe('hello@ezscale.cloud');
|
||||
});
|
||||
|
||||
it('updates billing settings', function (): void {
|
||||
$admin = User::factory()->admin()->create();
|
||||
|
||||
$this->actingAs($admin)
|
||||
->put($this->adminUrl.'/settings', [
|
||||
'group' => 'billing',
|
||||
'default_currency' => 'USD',
|
||||
'grace_period_days' => 14,
|
||||
'suspension_warning_days' => 5,
|
||||
'auto_terminate_days' => 30,
|
||||
'bandwidth_overage_rate' => 0.10,
|
||||
])
|
||||
->assertRedirect();
|
||||
|
||||
expect(Setting::get('grace_period_days'))->toBe('14');
|
||||
});
|
||||
|
||||
it('validates settings update with invalid data', function (): void {
|
||||
$admin = User::factory()->admin()->create();
|
||||
|
||||
$this->actingAs($admin)
|
||||
->put($this->adminUrl.'/settings', [
|
||||
'group' => 'general',
|
||||
'company_email' => 'not-an-email',
|
||||
])
|
||||
->assertSessionHasErrors(['company_email']);
|
||||
});
|
||||
|
||||
it('denies customer access to settings', function (): void {
|
||||
$customer = User::factory()->customer()->create();
|
||||
|
||||
$this->actingAs($customer)
|
||||
->get($this->adminUrl.'/settings')
|
||||
->assertForbidden();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user