Implement Phase 1: Foundation & Core Setup
Complete foundation for the EZSCALE billing platform replacing WHMCS: - Install Composer deps (Fortify, Passport, Cashier, PayPal, Spatie Permissions, Inertia) - Install Vue 3 + Inertia.js with Vite, 3 layouts (App, Auth, Admin) - Configure subdomain routing (marketing, account, admin) with domain-based route files - Create 30 database migrations (15 custom tables + package defaults) - Create 14 Eloquent models with relationships, factories, and encrypted casts - Set up Fortify auth with 7 Vue pages (Login, Register, ForgotPassword, ResetPassword, VerifyEmail, ConfirmPassword, TwoFactorChallenge) - Add 2FA TOTP setup page with QR code and recovery codes - Configure middleware (Inertia, Spatie roles/permissions, EnsureUserNotSuspended) - Create seeders for roles/permissions, sample plans, and admin user - Build dashboard controllers and Vue pages for customer and admin panels - Add 4 shared Vue components (Card, Button, NavLink, FlashMessages) - Generate Passport OAuth2 keys for future SSO/API use - Write 24 Pest tests (auth, role-based access, models) — all passing Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
48
website/tests/Feature/Auth/LoginTest.php
Normal file
48
website/tests/Feature/Auth/LoginTest.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Models\User;
|
||||
use Database\Seeders\RoleAndPermissionSeeder;
|
||||
|
||||
beforeEach(function (): void {
|
||||
$this->seed(RoleAndPermissionSeeder::class);
|
||||
$this->accountUrl = 'http://'.config('app.domains.account');
|
||||
});
|
||||
|
||||
it('renders the login page', function (): void {
|
||||
$this->get($this->accountUrl.'/login')
|
||||
->assertOk();
|
||||
});
|
||||
|
||||
it('authenticates a user with valid credentials', function (): void {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->post($this->accountUrl.'/login', [
|
||||
'email' => $user->email,
|
||||
'password' => 'password',
|
||||
])->assertRedirect();
|
||||
|
||||
$this->assertAuthenticated();
|
||||
});
|
||||
|
||||
it('rejects invalid credentials', function (): void {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->post($this->accountUrl.'/login', [
|
||||
'email' => $user->email,
|
||||
'password' => 'wrong-password',
|
||||
]);
|
||||
|
||||
$this->assertGuest();
|
||||
});
|
||||
|
||||
it('logs a user out', function (): void {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->actingAs($user)
|
||||
->post($this->accountUrl.'/logout')
|
||||
->assertRedirect();
|
||||
|
||||
$this->assertGuest();
|
||||
});
|
||||
73
website/tests/Feature/Auth/RegistrationTest.php
Normal file
73
website/tests/Feature/Auth/RegistrationTest.php
Normal file
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Models\User;
|
||||
use Database\Seeders\RoleAndPermissionSeeder;
|
||||
|
||||
beforeEach(function (): void {
|
||||
$this->seed(RoleAndPermissionSeeder::class);
|
||||
$this->accountUrl = 'http://'.config('app.domains.account');
|
||||
});
|
||||
|
||||
it('renders the registration page', function (): void {
|
||||
$this->get($this->accountUrl.'/register')
|
||||
->assertOk();
|
||||
});
|
||||
|
||||
it('registers a new user', function (): void {
|
||||
$this->post($this->accountUrl.'/register', [
|
||||
'name' => 'Test User',
|
||||
'email' => 'test@example.com',
|
||||
'password' => 'password123!',
|
||||
'password_confirmation' => 'password123!',
|
||||
])->assertRedirect();
|
||||
|
||||
$this->assertDatabaseHas('users', [
|
||||
'email' => 'test@example.com',
|
||||
]);
|
||||
});
|
||||
|
||||
it('assigns customer role on registration', function (): void {
|
||||
$this->post($this->accountUrl.'/register', [
|
||||
'name' => 'Test User',
|
||||
'email' => 'test@example.com',
|
||||
'password' => 'password123!',
|
||||
'password_confirmation' => 'password123!',
|
||||
]);
|
||||
|
||||
$user = User::where('email', 'test@example.com')->first();
|
||||
expect($user->hasRole('customer'))->toBeTrue();
|
||||
});
|
||||
|
||||
it('creates user profile on registration', function (): void {
|
||||
$this->post($this->accountUrl.'/register', [
|
||||
'name' => 'Test User',
|
||||
'email' => 'test@example.com',
|
||||
'password' => 'password123!',
|
||||
'password_confirmation' => 'password123!',
|
||||
]);
|
||||
|
||||
$user = User::where('email', 'test@example.com')->first();
|
||||
expect($user->profile)->not->toBeNull();
|
||||
});
|
||||
|
||||
it('validates registration input', function (): void {
|
||||
$this->post($this->accountUrl.'/register', [
|
||||
'name' => '',
|
||||
'email' => 'not-an-email',
|
||||
'password' => 'short',
|
||||
'password_confirmation' => 'mismatch',
|
||||
])->assertSessionHasErrors(['name', 'email', 'password']);
|
||||
});
|
||||
|
||||
it('prevents duplicate email registration', function (): void {
|
||||
User::factory()->create(['email' => 'taken@example.com']);
|
||||
|
||||
$this->post($this->accountUrl.'/register', [
|
||||
'name' => 'Test User',
|
||||
'email' => 'taken@example.com',
|
||||
'password' => 'password123!',
|
||||
'password_confirmation' => 'password123!',
|
||||
])->assertSessionHasErrors('email');
|
||||
});
|
||||
44
website/tests/Feature/Auth/RoleBasedAccessTest.php
Normal file
44
website/tests/Feature/Auth/RoleBasedAccessTest.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Models\User;
|
||||
use Database\Seeders\RoleAndPermissionSeeder;
|
||||
|
||||
beforeEach(function (): void {
|
||||
$this->seed(RoleAndPermissionSeeder::class);
|
||||
$this->accountUrl = 'http://'.config('app.domains.account');
|
||||
});
|
||||
|
||||
it('redirects unauthenticated users to login', function (): void {
|
||||
$this->get($this->accountUrl.'/dashboard')
|
||||
->assertRedirect($this->accountUrl.'/login');
|
||||
});
|
||||
|
||||
it('identifies admin users correctly', function (): void {
|
||||
$admin = User::factory()->admin()->create();
|
||||
|
||||
expect($admin->isAdmin())->toBeTrue();
|
||||
expect($admin->isCustomer())->toBeFalse();
|
||||
});
|
||||
|
||||
it('identifies customer users correctly', function (): void {
|
||||
$customer = User::factory()->customer()->create();
|
||||
|
||||
expect($customer->isCustomer())->toBeTrue();
|
||||
expect($customer->isAdmin())->toBeFalse();
|
||||
});
|
||||
|
||||
it('identifies suspended users correctly', function (): void {
|
||||
$user = User::factory()->suspended()->create();
|
||||
|
||||
expect($user->isSuspended())->toBeTrue();
|
||||
expect($user->isBanned())->toBeFalse();
|
||||
});
|
||||
|
||||
it('identifies banned users correctly', function (): void {
|
||||
$user = User::factory()->banned()->create();
|
||||
|
||||
expect($user->isBanned())->toBeTrue();
|
||||
expect($user->isSuspended())->toBeFalse();
|
||||
});
|
||||
@@ -1,7 +1,8 @@
|
||||
<?php
|
||||
|
||||
test('the application returns a successful response', function () {
|
||||
$response = $this->get('/');
|
||||
declare(strict_types=1);
|
||||
|
||||
$response->assertStatus(200);
|
||||
test('the application redirects to account login', function () {
|
||||
$this->get('/')
|
||||
->assertRedirect('https://'.config('app.domains.account').'/login');
|
||||
});
|
||||
|
||||
32
website/tests/Feature/Models/PlanTest.php
Normal file
32
website/tests/Feature/Models/PlanTest.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Models\Plan;
|
||||
|
||||
it('creates a plan with factory', function (): void {
|
||||
$plan = Plan::factory()->create();
|
||||
|
||||
expect($plan)->toBeInstanceOf(Plan::class);
|
||||
expect($plan->status)->toBe('active');
|
||||
});
|
||||
|
||||
it('casts features to array', function (): void {
|
||||
$plan = Plan::factory()->create([
|
||||
'features' => ['cpu' => '2 vCPU', 'ram' => '4GB'],
|
||||
]);
|
||||
|
||||
expect($plan->features)->toBeArray();
|
||||
expect($plan->features['cpu'])->toBe('2 vCPU');
|
||||
});
|
||||
|
||||
it('checks availability correctly', function (): void {
|
||||
$plan = Plan::factory()->create(['status' => 'active', 'stock_quantity' => null]);
|
||||
expect($plan->isAvailable())->toBeTrue();
|
||||
|
||||
$plan = Plan::factory()->create(['status' => 'hidden']);
|
||||
expect($plan->isAvailable())->toBeFalse();
|
||||
|
||||
$plan = Plan::factory()->create(['status' => 'active', 'stock_quantity' => 0]);
|
||||
expect($plan->isAvailable())->toBeFalse();
|
||||
});
|
||||
42
website/tests/Feature/Models/UserTest.php
Normal file
42
website/tests/Feature/Models/UserTest.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\UserProfile;
|
||||
use Database\Seeders\RoleAndPermissionSeeder;
|
||||
|
||||
beforeEach(function (): void {
|
||||
$this->seed(RoleAndPermissionSeeder::class);
|
||||
});
|
||||
|
||||
it('has correct fillable attributes', function (): void {
|
||||
$user = new User;
|
||||
|
||||
expect($user->getFillable())->toBe([
|
||||
'name', 'email', 'password', 'status', 'phone', 'company',
|
||||
]);
|
||||
});
|
||||
|
||||
it('has correct hidden attributes', function (): void {
|
||||
$user = new User;
|
||||
|
||||
expect($user->getHidden())->toBe([
|
||||
'password', 'remember_token', 'two_factor_secret', 'two_factor_recovery_codes',
|
||||
]);
|
||||
});
|
||||
|
||||
it('has profile relationship', function (): void {
|
||||
$user = User::factory()->create();
|
||||
$profile = UserProfile::factory()->create(['user_id' => $user->id]);
|
||||
|
||||
expect($user->profile)->toBeInstanceOf(UserProfile::class);
|
||||
expect($user->profile->id)->toBe($profile->id);
|
||||
});
|
||||
|
||||
it('creates user with factory', function (): void {
|
||||
$user = User::factory()->create();
|
||||
|
||||
expect($user)->toBeInstanceOf(User::class);
|
||||
expect($user->status)->toBe('active');
|
||||
});
|
||||
@@ -1,47 +1,5 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Test Case
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The closure you provide to your test functions is always bound to a specific PHPUnit test
|
||||
| case class. By default, that class is "PHPUnit\Framework\TestCase". Of course, you may
|
||||
| need to change it using the "pest()" function to bind a different classes or traits.
|
||||
|
|
||||
*/
|
||||
|
||||
pest()->extend(Tests\TestCase::class)
|
||||
// ->use(Illuminate\Foundation\Testing\RefreshDatabase::class)
|
||||
->use(Illuminate\Foundation\Testing\RefreshDatabase::class)
|
||||
->in('Feature');
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Expectations
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When you're writing tests, you often need to check that values meet certain conditions. The
|
||||
| "expect()" function gives you access to a set of "expectations" methods that you can use
|
||||
| to assert different things. Of course, you may extend the Expectation API at any time.
|
||||
|
|
||||
*/
|
||||
|
||||
expect()->extend('toBeOne', function () {
|
||||
return $this->toBe(1);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Functions
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| While Pest is very powerful out-of-the-box, you may have some testing code specific to your
|
||||
| project that you don't want to repeat in every file. Here you can also expose helpers as
|
||||
| global functions to help you to reduce the number of lines of code in your test files.
|
||||
|
|
||||
*/
|
||||
|
||||
function something()
|
||||
{
|
||||
// ..
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user