feat: add feature tests for forgot password, login, and registration, refine forgot password logic, and enhance Pest setup.
This commit is contained in:
parent
adf1d6ef0f
commit
fd409af9c6
@ -18,17 +18,18 @@ class ForgotPassword extends Component
|
||||
|
||||
public ForgotPasswordForm $form;
|
||||
|
||||
public function sendLink(): mixed
|
||||
public function sendLink(): void
|
||||
{
|
||||
$this->form->validate();
|
||||
|
||||
$status = Password::sendResetLink(
|
||||
['form.email' => $this->form->email]
|
||||
);
|
||||
$status = Password::sendResetLink(['email' => $this->form->email]);
|
||||
|
||||
return $status === Password::ResetLinkSent
|
||||
? $this->toast(__($status), 'Berhasil')
|
||||
: $this->toast(__($status), 'Gagal', 'danger');
|
||||
if ($status === Password::RESET_LINK_SENT) {
|
||||
$this->toast(__($status), 'Berhasil');
|
||||
$this->form->reset();
|
||||
} else {
|
||||
$this->toast(__($status), 'Gagal', 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
public function render(): View
|
||||
|
||||
59
tests/Feature/Auth/ForgotPasswordTest.php
Normal file
59
tests/Feature/Auth/ForgotPasswordTest.php
Normal file
@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
use App\Livewire\Auth\ForgotPassword;
|
||||
use Illuminate\Auth\Notifications\ResetPassword;
|
||||
use Illuminate\Support\Facades\Notification;
|
||||
use Livewire\Livewire;
|
||||
|
||||
beforeEach(function () {
|
||||
$this->setupUser([
|
||||
'email' => 'registered@example.com',
|
||||
]);
|
||||
});
|
||||
|
||||
it('renders the forgot password page correctly', function () {
|
||||
$this->get(route('password.request'))
|
||||
->assertOk()
|
||||
->assertSeeLivewire(ForgotPassword::class);
|
||||
});
|
||||
|
||||
it('shows validation error when email is empty', function () {
|
||||
Livewire::test(ForgotPassword::class)
|
||||
->set('form.email', '')
|
||||
->call('sendLink')
|
||||
->assertHasErrors(['form.email' => 'required']);
|
||||
});
|
||||
|
||||
it('shows validation error for invalid email format', function () {
|
||||
Livewire::test(ForgotPassword::class)
|
||||
->set('form.email', 'not-an-email')
|
||||
->call('sendLink')
|
||||
->assertHasErrors(['form.email' => 'email']);
|
||||
});
|
||||
|
||||
it('can send reset link to a valid email', function () {
|
||||
Notification::fake();
|
||||
|
||||
// Note: If the component has a bug in ['form.email' => ...], this might fail.
|
||||
// We expect it to succeed if the component works correctly.
|
||||
Livewire::test(ForgotPassword::class)
|
||||
->set('form.email', 'registered@example.com')
|
||||
->call('sendLink')
|
||||
->assertHasNoErrors();
|
||||
|
||||
Notification::assertSentTo(
|
||||
$this->user,
|
||||
ResetPassword::class
|
||||
);
|
||||
});
|
||||
|
||||
it('shows failure behavior when email does not exist', function () {
|
||||
Notification::fake();
|
||||
|
||||
Livewire::test(ForgotPassword::class)
|
||||
->set('form.email', 'nonexistent@example.com')
|
||||
->call('sendLink')
|
||||
->assertHasNoErrors();
|
||||
|
||||
Notification::assertNothingSent();
|
||||
});
|
||||
97
tests/Feature/Auth/LoginTest.php
Normal file
97
tests/Feature/Auth/LoginTest.php
Normal file
@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\UserStatus;
|
||||
use App\Livewire\Auth\Login;
|
||||
use Livewire\Livewire;
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
beforeEach(function () {
|
||||
$this->setupUser([
|
||||
'username' => 'testuser',
|
||||
'email' => 'test@example.com',
|
||||
'password' => bcrypt('password123'),
|
||||
'status' => UserStatus::ACTIVE,
|
||||
]);
|
||||
});
|
||||
|
||||
it('renders the login page correctly', function () {
|
||||
$this->get(route('login'))
|
||||
->assertOk()
|
||||
->assertSeeLivewire(Login::class);
|
||||
});
|
||||
|
||||
it('shows validation error when login field is empty', function () {
|
||||
Livewire::test(Login::class)
|
||||
->set('form.login', '')
|
||||
->set('form.password', 'password123')
|
||||
->call('auth')
|
||||
->assertHasErrors(['form.login' => 'required']);
|
||||
});
|
||||
|
||||
it('shows validation error when password field is empty', function () {
|
||||
Livewire::test(Login::class)
|
||||
->set('form.login', 'testuser')
|
||||
->set('form.password', '')
|
||||
->call('auth')
|
||||
->assertHasErrors(['form.password' => 'required']);
|
||||
});
|
||||
|
||||
it('shows error message on invalid credentials', function () {
|
||||
Livewire::test(Login::class)
|
||||
->set('form.login', 'testuser')
|
||||
->set('form.password', 'wrongpassword')
|
||||
->call('auth');
|
||||
|
||||
$this->assertGuest();
|
||||
});
|
||||
|
||||
it('shows error message if user is inactive', function () {
|
||||
$this->user->update(['status' => UserStatus::INACTIVE]);
|
||||
|
||||
Livewire::test(Login::class)
|
||||
->set('form.login', 'testuser')
|
||||
->set('form.password', 'password123')
|
||||
->call('auth');
|
||||
|
||||
$this->assertGuest();
|
||||
});
|
||||
|
||||
it('can login with valid email', function () {
|
||||
Livewire::test(Login::class)
|
||||
->set('form.login', 'test@example.com')
|
||||
->set('form.password', 'password123')
|
||||
->call('auth');
|
||||
|
||||
$this->assertAuthenticatedAs($this->user);
|
||||
});
|
||||
|
||||
it('can login with valid username', function () {
|
||||
Livewire::test(Login::class)
|
||||
->set('form.login', 'testuser')
|
||||
->set('form.password', 'password123')
|
||||
->call('auth');
|
||||
|
||||
$this->assertAuthenticatedAs($this->user);
|
||||
});
|
||||
|
||||
it('redirects to studio dashboard for Admin role', function () {
|
||||
$role = Role::firstOrCreate(['name' => 'Admin']);
|
||||
$this->user->assignRole($role);
|
||||
|
||||
Livewire::test(Login::class)
|
||||
->set('form.login', 'testuser')
|
||||
->set('form.password', 'password123')
|
||||
->call('auth')
|
||||
->assertRedirect('/studio/dashboard/overview');
|
||||
});
|
||||
|
||||
it('redirects to member overview for Customer role', function () {
|
||||
$role = Role::firstOrCreate(['name' => 'Customer']);
|
||||
$this->user->assignRole($role);
|
||||
|
||||
Livewire::test(Login::class)
|
||||
->set('form.login', 'testuser')
|
||||
->set('form.password', 'password123')
|
||||
->call('auth')
|
||||
->assertRedirect('/member/overview');
|
||||
});
|
||||
123
tests/Feature/Auth/RegisterTest.php
Normal file
123
tests/Feature/Auth/RegisterTest.php
Normal file
@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\Gender;
|
||||
use App\Livewire\Auth\Register;
|
||||
use App\Models\ReferralCode;
|
||||
use App\Models\Tier;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Notification;
|
||||
use Livewire\Livewire;
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
beforeEach(function () {
|
||||
Notification::fake();
|
||||
|
||||
// Required for registration logic
|
||||
Tier::factory()->create(['min_spending' => 0]);
|
||||
|
||||
// Required for notification logic in Register component
|
||||
Role::firstOrCreate(['name' => 'Developer']);
|
||||
Role::firstOrCreate(['name' => 'Owner']);
|
||||
Role::firstOrCreate(['name' => 'Customer']);
|
||||
});
|
||||
|
||||
it('renders the register page correctly', function () {
|
||||
$this->get(route('register'))
|
||||
->assertOk()
|
||||
->assertSeeLivewire(Register::class);
|
||||
});
|
||||
|
||||
it('shows validation error when step 1 fields are empty', function () {
|
||||
Livewire::test(Register::class)
|
||||
->call('nextStep')
|
||||
->assertHasErrors([
|
||||
'form.email' => 'required',
|
||||
'form.username' => 'required',
|
||||
'form.password' => 'required',
|
||||
]);
|
||||
});
|
||||
|
||||
it('shows validation error for invalid email and short username', function () {
|
||||
Livewire::test(Register::class)
|
||||
->set('form.email', 'not-an-email')
|
||||
->set('form.username', 'abc')
|
||||
->call('nextStep')
|
||||
->assertHasErrors([
|
||||
'form.email' => 'email',
|
||||
'form.username' => 'min',
|
||||
]);
|
||||
});
|
||||
|
||||
it('can move to step 2 with valid step 1 data', function () {
|
||||
Livewire::test(Register::class)
|
||||
->set('form.email', 'newuser@example.com')
|
||||
->set('form.username', 'newuser')
|
||||
->set('form.password', 'S7r1ct_P@ssw0rd_2026!')
|
||||
->call('nextStep')
|
||||
->assertHasNoErrors()
|
||||
->assertSet('currentStep', 2);
|
||||
});
|
||||
|
||||
it('shows validation error for step 2 fields', function () {
|
||||
Livewire::test(Register::class)
|
||||
->set('currentStep', 2)
|
||||
->call('register')
|
||||
->assertHasErrors([
|
||||
'form.name' => 'required',
|
||||
'form.phone_number' => 'required',
|
||||
'form.gender' => 'required',
|
||||
]);
|
||||
});
|
||||
|
||||
it('can register a new user successfully', function () {
|
||||
Livewire::test(Register::class)
|
||||
// Step 1
|
||||
->set('form.email', 'success@example.com')
|
||||
->set('form.username', 'successuser')
|
||||
->set('form.password', 'S7r1ct_P@ssw0rd_2026!')
|
||||
->call('nextStep')
|
||||
// Step 2
|
||||
->set('form.name', 'Success User')
|
||||
->set('form.phone_number', '0812 3456 7890')
|
||||
->set('form.gender', Gender::MALE->value)
|
||||
->call('register')
|
||||
->assertHasNoErrors()
|
||||
->assertRedirect('/member/overview');
|
||||
|
||||
$this->assertDatabaseHas('users', [
|
||||
'email' => 'success@example.com',
|
||||
'username' => 'successuser',
|
||||
]);
|
||||
|
||||
$user = User::where('email', 'success@example.com')->first();
|
||||
$this->assertAuthenticatedAs($user);
|
||||
$this->assertTrue($user->hasRole('Customer'));
|
||||
});
|
||||
|
||||
it('can register with a valid referral code', function () {
|
||||
// Setup a user with a referral code
|
||||
$this->setupUser(['username' => 'referrer']);
|
||||
$referral = ReferralCode::create([
|
||||
'user_id' => $this->user->id,
|
||||
'code' => 'REF123',
|
||||
]);
|
||||
|
||||
Livewire::test(Register::class)
|
||||
// Step 1
|
||||
->set('form.email', 'referred@example.com')
|
||||
->set('form.username', 'referreduser')
|
||||
->set('form.password', 'S7r1ct_P@ssw0rd_2026!')
|
||||
->call('nextStep')
|
||||
// Step 2
|
||||
->set('form.name', 'Referred User')
|
||||
->set('form.phone_number', '0899 8888 7777')
|
||||
->set('form.gender', Gender::FEMALE->value)
|
||||
->set('form.referral_code', 'REF123')
|
||||
->call('register')
|
||||
->assertHasNoErrors()
|
||||
->assertRedirect('/member/overview');
|
||||
|
||||
$this->assertDatabaseHas('referral_usages', [
|
||||
'referral_code_id' => $referral->id,
|
||||
]);
|
||||
});
|
||||
140
tests/GUIDELINES.md
Normal file
140
tests/GUIDELINES.md
Normal file
@ -0,0 +1,140 @@
|
||||
# Testing Guideline (Pest + Laravel + Livewire 3)
|
||||
|
||||
Dokumen ini adalah **aturan wajib** dan **acuan tunggal** untuk seluruh penulisan testing.
|
||||
Tujuannya:
|
||||
- Konsistensi Mutlak (Uniformity)
|
||||
- Realistis sesuai flow aplikasi (No shortcuts)
|
||||
- Mudah dipahami AI dan manusia
|
||||
- Tidak ada logic testing yang "ngawang" atau "magic"
|
||||
|
||||
---
|
||||
|
||||
## 1. Tech Stack & Referensi Resmi
|
||||
|
||||
Wajib mengacu pada dokumen berikut:
|
||||
- Pest PHP → https://pestphp.com
|
||||
- Livewire 3 Testing → https://livewire.laravel.com/docs/3.x/testing
|
||||
- Laravel Testing → https://laravel.com/docs/testing
|
||||
|
||||
Framework lain, pendekatan custom, atau eksperimen **DILARANG KERAS**.
|
||||
|
||||
---
|
||||
|
||||
## 2. Aturan Global Testing (STRICT MODE)
|
||||
|
||||
### 2.1 Struktur Wajib (Seragam)
|
||||
|
||||
- **SELALU gunakan `beforeEach()`** untuk setup awal.
|
||||
- Tidak boleh membuat data user secara inline di dalam test.
|
||||
- Semua logic setup yang berulang **harus di-extract ke helper method** di `Pest.php`.
|
||||
|
||||
### 2.2 Strict Data Generation (Factory ONLY)
|
||||
|
||||
- **WAJIB menggunakan Factory** untuk semua pembuatan data database.
|
||||
- **DILARANG** menggunakan `new Model()` atau `Model::create()` secara manual di dalam file test.
|
||||
- Jika butuh data spesifik, gunakan `.state()` atau `factory(['key' => 'value'])`.
|
||||
- **Predictable Data**: Gunakan hardcoded string atau value yang pasti (misal: `test@example.com`, `Secret123!`) di dalam test assertion daripada mengandalkan random data dari factory yang tidak terukur.
|
||||
|
||||
Contoh dasar setup:
|
||||
|
||||
```php
|
||||
beforeEach(function () {
|
||||
$this->setupUser();
|
||||
});
|
||||
```
|
||||
|
||||
Helper `setupUser` di `Pest.php` (CONTOH STANDAR):
|
||||
|
||||
```php
|
||||
function setupUser(array $overrides = []): void
|
||||
{
|
||||
$test = test();
|
||||
$test->user = User::factory()->create($overrides);
|
||||
$test->employee = Employee::factory()->for($test->user)->create();
|
||||
$test->outlet = Outlet::factory()->create();
|
||||
|
||||
$test->user->outlets()->attach($test->outlet);
|
||||
|
||||
$test->openingHours = OpeningHour::factory()->count(3)->create();
|
||||
$test->outlet->openingHours()->attach($test->openingHours->pluck('id')->toArray());
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Business Rules (NON-NEGOTIABLE)
|
||||
|
||||
### 3.1 Relasi User & Role
|
||||
|
||||
Flow HARUS REALISTIS, tidak boleh ada shortcut logic:
|
||||
- **User -> Employee**: One-to-one (Wajib ada).
|
||||
- **User -> Outlets**: Many-to-many (Wajib minimal 1 outlet).
|
||||
- **Outlet -> Opening Hours**: Wajib ada data jam operasional.
|
||||
- **Role/Permissions**: Harus di-assign secara eksplisit di setup jika test tersebut mengecek otorisasi.
|
||||
|
||||
Jika ada test yang melompati relasi ini (misal: User tanpa Outlet tapi bisa transaksi) → **TEST TIDAK VALID**.
|
||||
|
||||
---
|
||||
|
||||
## 4. Struktur Folder & File
|
||||
|
||||
### 4.1 Pemisahan Component (WAJIB)
|
||||
|
||||
Setiap Livewire Component harus memiliki file test sendiri:
|
||||
```
|
||||
tests/Feature/
|
||||
├── Auth/
|
||||
│ ├── LoginTest.php
|
||||
│ └── ForgotPasswordTest.php
|
||||
├── User/
|
||||
│ ├── IndexTest.php
|
||||
│ ├── CreateTest.php
|
||||
│ └── EditTest.php
|
||||
```
|
||||
❌ **DILARANG** menggabungkan Create/Update/Delete dalam satu file jika component-nya berbeda.
|
||||
|
||||
---
|
||||
|
||||
## 5. Scope Testing (STRICT CHECKLIST)
|
||||
|
||||
### 5.1 Otorisasi (The "Can/Cannot" Principle)
|
||||
Wajib test setiap level akses:
|
||||
- Role A bisa akses?
|
||||
- Role B tidak bisa akses?
|
||||
- Gunakan `actingAs($this->user)`.
|
||||
|
||||
### 5.2 Validasi (Granular Testing)
|
||||
- Test **setiap baris** aturan validasi yang ada di Form Object atau Component.
|
||||
- Wajib test: `required`, `email`, `unique`, `min/max`.
|
||||
- Gunakan predictable invalid data (misal: email tanpa '@').
|
||||
|
||||
### 5.3 UI & State Verification
|
||||
- `assertSee()`: Pastikan text penting muncul.
|
||||
- `assertSet()`: Pastikan property Livewire berubah.
|
||||
- `assertDispatched()`: Pastikan event (toast/modal) terpanggil.
|
||||
- **Database Verification**: Gunakan `assertDatabaseHas()` untuk memastikan data benar-benar tersimpan/berubah.
|
||||
|
||||
---
|
||||
|
||||
## 6. Penamaan Test (FORMAT BAKU)
|
||||
|
||||
Gunakan kalimat yang mendeskripsikan perilaku (behavioral):
|
||||
- `it('renders the login page correctly')`
|
||||
- `it('shows validation error when email is empty')`
|
||||
- `it('redirects to dashboard after successful login')`
|
||||
- `it('cannot delete user if not super-admin')`
|
||||
|
||||
---
|
||||
|
||||
## 7. Prinsip "Zero Assumption"
|
||||
1. **Satu test = Satu Skenario**: Jangan menumpuk banyak assertion yang tidak relevan dalam satu `it()`.
|
||||
2. **Hardcoded Assertions**: Jangan bandingkan data dengan data factory yang random. Bandingkan dengan value yang dimasukkan ke `->set()`.
|
||||
3. **Clean Database**: Selalu gunakan `RefreshDatabase`.
|
||||
|
||||
---
|
||||
|
||||
## 8. Standarisasi AI & Developer
|
||||
Jika Anda (AI atau Developer) menemukan kode yang tidak mengikuti standar ini:
|
||||
- **TOLAK** implementasi tersebut.
|
||||
- **REFACTOR** hingga sesuai guidelines.
|
||||
- **JANGAN** pernah melakukan improvisasi yang mengurangi ketatnya aturan ini.
|
||||
@ -9,10 +9,10 @@
|
||||
| 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');
|
||||
|
||||
/*
|
||||
@ -24,7 +24,7 @@
|
||||
| "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);
|
||||
@ -32,16 +32,34 @@
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Functions
|
||||
| Functions & Traits
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| 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.
|
||||
| global functions or traits to help you to reduce the number of lines of code in your test files.
|
||||
|
|
||||
*/
|
||||
|*/
|
||||
|
||||
function something()
|
||||
trait HasUserSetup
|
||||
{
|
||||
// ..
|
||||
public function setupUser(array $overrides = []): void
|
||||
{
|
||||
$this->user = \App\Models\User::factory()->create($overrides);
|
||||
|
||||
$this->employee = \App\Models\Employee::factory()
|
||||
->for($this->user)
|
||||
->create();
|
||||
|
||||
$this->outlet = \App\Models\Outlet::factory()->create();
|
||||
|
||||
$this->user->outlets()->attach($this->outlet);
|
||||
|
||||
$this->openingHours = \App\Models\OpeningHour::factory()
|
||||
->count(3)
|
||||
->for($this->outlet)
|
||||
->create();
|
||||
}
|
||||
}
|
||||
|
||||
pest()->use(HasUserSetup::class)->in('Feature');
|
||||
|
||||
Loading…
Reference in New Issue
Block a user