Add feature tests for Doctor-related functionalities

This commit is contained in:
2026-09-21 05:41:06 +03:30
parent e987a528ef
commit 6e92ebaada
29 changed files with 2634 additions and 29 deletions
@@ -0,0 +1,70 @@
<?php
namespace Tests\Feature\Http\Admin;
use App\Enums\UserEnum;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Inertia\Testing\AssertableInertia as Assert;
use Tests\TestCase;
class AuthControllerTest extends TestCase
{
use RefreshDatabase;
public function test_login_page_renders_for_guests(): void
{
$this->get(route('admin.auth.login'))->assertOk()->assertInertia(fn (Assert $page) => $page->component('Auth/login'));
}
public function test_login_page_redirects_authenticated_users_to_dashboard(): void
{
$this->actingAs(User::factory()->create())->get(route('admin.auth.login'))->assertRedirect('/admin/dashboard');
}
public function test_login_action_authenticates_with_email_and_password(): void
{
$user = User::factory()->create(['email' => 'admin@example.com', 'password' => 'secret123', 'level' => UserEnum::Admin->value]);
$this->postJson('/admin/auth/login', ['email' => 'admin@example.com', 'password' => 'secret123'])->assertOk();
$this->assertAuthenticatedAs($user);
}
public function test_login_action_rejects_wrong_password(): void
{
User::factory()->create(['email' => 'admin@example.com', 'password' => 'secret123']);
$this->postJson('/admin/auth/login', ['email' => 'admin@example.com', 'password' => 'wrongpass'])
->assertUnauthorized()
->assertJsonPath('errors.email.0', 'ایمیل و کلمه عبور شما با هم مطابقت ندارن');
$this->assertGuest();
}
public function test_login_action_validates_input(): void
{
$this->postJson('/admin/auth/login', ['email' => 'unknown@example.com', 'password' => 'short'])
->assertUnprocessable()
->assertJsonValidationErrors(['email', 'password']);
}
public function test_register_page_renders_for_guests(): void
{
$this->get(route('admin.auth.register'))->assertOk()->assertInertia(fn (Assert $page) => $page->component('Auth/register'));
}
public function test_register_action_validates_input(): void
{
$this->postJson('/admin/auth/register', ['name' => 'a', 'email' => 'x', 'password' => 'short'])
->assertUnprocessable()
->assertJsonValidationErrors(['name', 'email', 'password']);
}
public function test_logout_ends_session_and_redirects_to_login(): void
{
$this->actingAs(User::factory()->create())->get('/admin/auth/logout')->assertRedirect('/admin/auth/login');
$this->assertGuest();
}
}
@@ -0,0 +1,165 @@
<?php
namespace Tests\Feature\Http\Admin;
use App\Enums\DocumentPriorityEnum;
use App\Enums\DocumentStatusEnum;
use App\Enums\MediaDiskEnum;
use App\Models\Document;
use App\Models\Medium;
use App\Models\Pet;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Inertia\Testing\AssertableInertia as Assert;
use Maatwebsite\Excel\Facades\Excel;
use Tests\Concerns\InteractsWithAdmin;
use Tests\TestCase;
class DocumentControllerTest extends TestCase
{
use InteractsWithAdmin, RefreshDatabase;
protected function payload(array $overrides = []): array
{
return array_merge([
'pet_id' => Pet::factory()->create()->id,
'status' => DocumentStatusEnum::Registered->name,
'priority' => DocumentPriorityEnum::High->name,
'modality' => 'mri',
], $overrides);
}
public function test_guest_is_redirected(): void
{
$this->get(route('admin.documents.index'))->assertRedirect(route('login'));
}
public function test_admin_without_permission_is_forbidden(): void
{
$admin = $this->adminWithoutPermissions();
$document = Document::factory()->create();
$this->actingAs($admin)->get(route('admin.documents.index'))->assertForbidden();
$this->actingAs($admin)->get(route('admin.documents.create'))->assertForbidden();
$this->actingAs($admin)->post(route('admin.documents.store'), $this->payload())->assertForbidden();
$this->actingAs($admin)->get(route('admin.documents.edit', $document))->assertForbidden();
$this->actingAs($admin)->put(route('admin.documents.update', $document), $this->payload())->assertForbidden();
$this->actingAs($admin)->delete(route('admin.documents.destroy', $document))->assertForbidden();
$this->actingAs($admin)->post('/admin/documents-upload')->assertForbidden();
$this->actingAs($admin)->get('/admin/documents-options')->assertForbidden();
}
public function test_index_renders_with_filters(): void
{
$admin = $this->adminWithAllPermissions();
$pet = Pet::factory()->create();
Document::factory()->registered()->high()->create(['code' => '111111', 'pet_id' => $pet->id]);
Document::factory()->completed()->medium()->create(['code' => '222222']);
$this->actingAs($admin)->get(route('admin.documents.index'))->assertOk()->assertInertia(fn (Assert $page) => $page->component('Documents/index')->has('documents.data', 2));
$this->actingAs($admin)->get(route('admin.documents.index', ['status' => DocumentStatusEnum::Completed->name]))->assertInertia(fn (Assert $page) => $page->has('documents.data', 1));
$this->actingAs($admin)->get(route('admin.documents.index', ['priority' => DocumentPriorityEnum::High->name]))->assertInertia(fn (Assert $page) => $page->has('documents.data', 1));
$this->actingAs($admin)->get(route('admin.documents.index', ['pet' => $pet->id]))->assertInertia(fn (Assert $page) => $page->has('documents.data', 1));
$this->actingAs($admin)->get(route('admin.documents.index', ['search' => '2222']))->assertInertia(fn (Assert $page) => $page->has('documents.data', 1));
}
public function test_create_and_edit_pages_render(): void
{
$admin = $this->adminWithAllPermissions();
$document = Document::factory()->create();
$this->actingAs($admin)->get(route('admin.documents.create'))->assertInertia(fn (Assert $page) => $page->component('Documents/create_and_edit')->has('pets'));
$this->actingAs($admin)->get(route('admin.documents.edit', $document))->assertInertia(fn (Assert $page) => $page->component('Documents/create_and_edit')->where('is_edit', true)->where('document.id', $document->id));
}
public function test_store_creates_document_with_files_attached_by_collection(): void
{
$admin = $this->adminWithAllPermissions();
$primary = Medium::factory()->create();
$history = Medium::factory()->create();
$this->actingAs($admin)->post(route('admin.documents.store'), $this->payload([
'primary_files' => [$primary->id],
'history_files' => [$history->id],
]))->assertOk()->assertSessionHas('success');
$document = Document::first();
$this->assertSame(DocumentStatusEnum::Registered, $document->status);
$this->assertSame(DocumentPriorityEnum::High, $document->priority);
$this->assertSame($admin->id, $document->requester_id);
$this->assertSame([$primary->id], $document->primaryFiles->pluck('id')->all());
$this->assertSame([$history->id], $document->historyFiles->pluck('id')->all());
}
public function test_store_validates_pet(): void
{
$this->actingAs($this->adminWithAllPermissions())->postJson(route('admin.documents.store'), ['pet_id' => 999])->assertUnprocessable()->assertJsonValidationErrors('pet_id');
}
public function test_update_replaces_file_collections(): void
{
$admin = $this->adminWithAllPermissions();
$document = Document::factory()->create();
$old = Medium::factory()->create();
$new = Medium::factory()->create();
$document->media()->attach($old->id, ['collection' => 'primary']);
$this->actingAs($admin)->put(route('admin.documents.update', $document), $this->payload([
'status' => DocumentStatusEnum::InProgress->name,
'primary_files' => [$new->id],
]))->assertOk();
$document->refresh();
$this->assertSame(DocumentStatusEnum::InProgress, $document->status);
$this->assertSame([$new->id], $document->primaryFiles->pluck('id')->all());
}
public function test_destroy_deletes_document(): void
{
$document = Document::factory()->create();
$this->actingAs($this->adminWithAllPermissions())->delete(route('admin.documents.destroy', $document))->assertOk();
$this->assertDatabaseMissing('documents', ['id' => $document->id]);
}
public function test_upload_file_stores_private_medium(): void
{
Storage::fake('private');
$this->actingAs($this->adminWithAllPermissions())->postJson('/admin/documents-upload', ['file' => UploadedFile::fake()->create('scan.pdf', 10, 'application/pdf')])
->assertOk()
->assertJsonPath('disk', MediaDiskEnum::Private->value);
$medium = Medium::first();
$this->assertStringStartsWith('documents/', $medium->path);
Storage::disk('private')->assertExists($medium->path);
}
public function test_upload_file_validates_presence(): void
{
$this->actingAs($this->adminWithAllPermissions())->postJson('/admin/documents-upload', [])->assertUnprocessable()->assertJsonValidationErrors('file');
}
public function test_get_options_returns_matching_documents(): void
{
$document = Document::factory()->create(['code' => '123456', 'modality' => 'mri']);
Document::factory()->create(['code' => '999999']);
$this->actingAs($this->adminWithAllPermissions())->getJson('/admin/documents-options?search=1234')
->assertOk()
->assertJsonCount(1, 'data')
->assertJsonPath('data.0.value', $document->id)
->assertJsonPath('data.0.label', 'پرونده 123456 - mri');
}
public function test_export_data_downloads_excel(): void
{
Excel::fake();
$this->actingAs($this->adminWithAllPermissions())->get('/admin/documents-export-data')->assertOk();
Excel::assertDownloaded('documents.xlsx');
}
}
@@ -0,0 +1,100 @@
<?php
namespace Tests\Feature\Http\Admin;
use App\Models\Medium;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Inertia\Testing\AssertableInertia as Assert;
use Tests\Concerns\InteractsWithAdmin;
use Tests\TestCase;
class MediumControllerTest extends TestCase
{
use InteractsWithAdmin, RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
Storage::fake('public');
Storage::fake('private');
}
public function test_guest_is_redirected(): void
{
$this->get('/admin/medium')->assertRedirect(route('login'));
}
public function test_admin_without_permission_is_forbidden(): void
{
$admin = $this->adminWithoutPermissions();
$medium = Medium::factory()->create();
$this->actingAs($admin)->get('/admin/medium')->assertForbidden();
$this->actingAs($admin)->post('/admin/medium')->assertForbidden();
$this->actingAs($admin)->delete("/admin/medium/{$medium->id}")->assertForbidden();
$this->actingAs($admin)->get('/admin/get-medium')->assertForbidden();
$this->actingAs($admin)->get("/admin/medium/{$medium->id}/download")->assertForbidden();
$this->actingAs($admin)->get("/admin/medium/{$medium->id}/serve")->assertForbidden();
}
public function test_index_renders_paginated_media(): void
{
Medium::factory()->count(2)->create();
$this->actingAs($this->adminWithAllPermissions())->get('/admin/medium')->assertOk()->assertInertia(fn (Assert $page) => $page->component('Medium')->has('medium.data', 2));
}
public function test_store_uploads_public_file_into_media_directory(): void
{
$this->actingAs($this->adminWithAllPermissions())->postJson('/admin/medium', ['file' => UploadedFile::fake()->image('pic.png')])
->assertOk()
->assertJsonPath('disk', 'public');
$medium = Medium::first();
$this->assertStringStartsWith('media/', $medium->path);
Storage::disk('public')->assertExists($medium->path);
}
public function test_store_validates_file(): void
{
$this->actingAs($this->adminWithAllPermissions())->postJson('/admin/medium', [])->assertUnprocessable()->assertJsonValidationErrors('file');
}
public function test_destroy_removes_file_and_record(): void
{
$medium = Medium::factory()->public()->create(['path' => 'media/a.png']);
Storage::disk('public')->put('media/a.png', 'x');
$this->actingAs($this->adminWithAllPermissions())->from('/admin/medium')->delete("/admin/medium/{$medium->id}")->assertRedirect('/admin/medium')->assertSessionHas('success');
Storage::disk('public')->assertMissing('media/a.png');
$this->assertDatabaseMissing('media', ['id' => $medium->id]);
}
public function test_get_medium_returns_paginated_json(): void
{
Medium::factory()->count(3)->create();
$this->actingAs($this->adminWithAllPermissions())->getJson('/admin/get-medium')->assertOk()->assertJsonCount(3, 'data');
}
public function test_download_and_serve_private_file(): void
{
$medium = Medium::factory()->private()->create(['path' => 'documents/f.txt', 'filename' => 'f.txt', 'mime_type' => 'text/plain']);
Storage::disk('private')->put('documents/f.txt', 'body');
$admin = $this->adminWithAllPermissions();
$this->actingAs($admin)->get("/admin/medium/{$medium->id}/download")->assertOk()->assertDownload('f.txt');
$this->actingAs($admin)->get("/admin/medium/{$medium->id}/serve")->assertOk()->assertHeader('Content-Type', 'text/plain; charset=UTF-8')->assertSee('body', false);
}
public function test_download_public_file_is_not_found(): void
{
$medium = Medium::factory()->public()->create();
$this->actingAs($this->adminWithAllPermissions())->get("/admin/medium/{$medium->id}/download")->assertNotFound();
}
}
@@ -0,0 +1,31 @@
<?php
namespace Tests\Feature\Http\Admin;
use App\Enums\UserEnum;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Inertia\Testing\AssertableInertia as Assert;
use Tests\TestCase;
class PanelControllerTest extends TestCase
{
use RefreshDatabase;
public function test_guest_is_redirected_to_login(): void
{
$this->get(route('admin.dashboard'))->assertRedirect(route('login'));
}
public function test_non_admin_user_gets_not_found(): void
{
$this->actingAs(User::factory()->create(['level' => UserEnum::Doctor->value]))->get(route('admin.dashboard'))->assertNotFound();
}
public function test_admin_sees_dashboard(): void
{
$this->actingAs(User::factory()->create(['level' => UserEnum::Admin->value]))->get(route('admin.dashboard'))
->assertOk()
->assertInertia(fn (Assert $page) => $page->component('index'));
}
}
@@ -0,0 +1,124 @@
<?php
namespace Tests\Feature\Http\Admin;
use App\Enums\PetGenderEnum;
use App\Enums\PetTypeEnum;
use App\Models\Pet;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Inertia\Testing\AssertableInertia as Assert;
use Maatwebsite\Excel\Facades\Excel;
use Tests\Concerns\InteractsWithAdmin;
use Tests\TestCase;
class PetControllerTest extends TestCase
{
use InteractsWithAdmin, RefreshDatabase;
protected function payload(array $overrides = []): array
{
return array_merge([
'name' => 'Rex',
'type' => PetTypeEnum::Dog->value,
'gender' => PetGenderEnum::Male->value,
'breed' => 'ژرمن',
'weight' => '20',
'sterilized' => true,
'birthday' => '2020-01-01',
], $overrides);
}
public function test_guest_is_redirected(): void
{
$this->get(route('admin.pets.index'))->assertRedirect(route('login'));
}
public function test_admin_without_permission_is_forbidden(): void
{
$admin = $this->adminWithoutPermissions();
$pet = Pet::factory()->create();
$this->actingAs($admin)->get(route('admin.pets.index'))->assertForbidden();
$this->actingAs($admin)->post(route('admin.pets.store'), $this->payload())->assertForbidden();
$this->actingAs($admin)->put(route('admin.pets.update', $pet), $this->payload())->assertForbidden();
$this->actingAs($admin)->delete(route('admin.pets.destroy', $pet))->assertForbidden();
$this->actingAs($admin)->get('/admin/pets-options')->assertForbidden();
}
public function test_index_renders_with_filters(): void
{
$admin = $this->adminWithAllPermissions();
Pet::factory()->dog()->male()->create(['name' => 'Rex']);
Pet::factory()->cat()->female()->create(['name' => 'Kitty']);
$this->actingAs($admin)->get(route('admin.pets.index'))->assertOk()->assertInertia(fn (Assert $page) => $page->component('Pets/index')->has('pets.data', 2));
$this->actingAs($admin)->get(route('admin.pets.index', ['type' => [PetTypeEnum::Cat->value]]))->assertInertia(fn (Assert $page) => $page->has('pets.data', 1));
$this->actingAs($admin)->get(route('admin.pets.index', ['gender' => PetGenderEnum::Male->value]))->assertInertia(fn (Assert $page) => $page->has('pets.data', 1));
$this->actingAs($admin)->get(route('admin.pets.index', ['search' => 'Kit']))->assertInertia(fn (Assert $page) => $page->has('pets.data', 1));
}
public function test_create_and_edit_pages_render(): void
{
$admin = $this->adminWithAllPermissions();
$pet = Pet::factory()->create();
$this->actingAs($admin)->get(route('admin.pets.create'))->assertInertia(fn (Assert $page) => $page->component('Pets/create_and_edit'));
$this->actingAs($admin)->get(route('admin.pets.edit', $pet))->assertInertia(fn (Assert $page) => $page->component('Pets/create_and_edit')->where('is_edit', true)->where('pet.id', $pet->id));
}
public function test_store_creates_pet_owned_by_admin(): void
{
$admin = $this->adminWithAllPermissions();
$this->actingAs($admin)->post(route('admin.pets.store'), $this->payload(['weight' => '۲۰']))->assertOk()->assertSessionHas('success');
$this->assertDatabaseHas('pets', ['name' => 'Rex', 'type' => PetTypeEnum::Dog->value, 'weight' => 20, 'owner_id' => $admin->id]);
}
public function test_store_validates_input(): void
{
$this->actingAs($this->adminWithAllPermissions())->postJson(route('admin.pets.store'), [])->assertUnprocessable()->assertJsonValidationErrors(['name', 'type', 'weight']);
}
public function test_update_changes_pet(): void
{
$admin = $this->adminWithAllPermissions();
$pet = Pet::factory()->create();
$this->actingAs($admin)->put(route('admin.pets.update', $pet), $this->payload(['name' => 'Max']))->assertOk();
$this->assertDatabaseHas('pets', ['id' => $pet->id, 'name' => 'Max']);
}
public function test_destroy_and_bulk_delete_soft_delete_pets(): void
{
$admin = $this->adminWithAllPermissions();
$pets = Pet::factory()->count(3)->create();
$this->actingAs($admin)->delete(route('admin.pets.destroy', $pets[0]))->assertOk();
$this->actingAs($admin)->post('/admin/pets-bulk-delete', ['ids' => [$pets[1]->id, $pets[2]->id]])->assertOk();
$this->assertSame(0, Pet::count());
$this->assertSame(3, Pet::withTrashed()->count());
}
public function test_get_options_returns_label_value_pairs(): void
{
$pet = Pet::factory()->create(['name' => 'Rex']);
Pet::factory()->create(['name' => 'Kitty']);
$this->actingAs($this->adminWithAllPermissions())->getJson('/admin/pets-options?search=Rex')
->assertOk()
->assertJsonCount(1, 'data')
->assertJsonPath('data.0', ['label' => 'Rex', 'value' => $pet->id]);
}
public function test_export_data_downloads_excel(): void
{
Excel::fake();
$this->actingAs($this->adminWithAllPermissions())->get('/admin/pets-export-data')->assertOk();
Excel::assertDownloaded('pets.xlsx');
}
}
@@ -0,0 +1,91 @@
<?php
namespace Tests\Feature\Http\Admin;
use App\Enums\ReportStatusEnum;
use App\Enums\UserEnum;
use App\Models\Document;
use App\Models\Report;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Inertia\Testing\AssertableInertia as Assert;
use Tests\Concerns\InteractsWithAdmin;
use Tests\TestCase;
class ReportControllerTest extends TestCase
{
use InteractsWithAdmin, RefreshDatabase;
protected function payload(array $overrides = []): array
{
return array_merge([
'document_id' => Document::factory()->create()->id,
'doctor_id' => User::factory()->create()->id,
'status' => ReportStatusEnum::Approved->name,
'body' => 'متن',
'submitted_at' => '2026-09-21 10:00:00',
], $overrides);
}
public function test_guest_and_non_admin_are_blocked(): void
{
$this->get(route('admin.reports.index'))->assertRedirect(route('login'));
$this->actingAs(User::factory()->create())->get(route('admin.reports.index'))->assertNotFound();
}
public function test_index_renders_with_filters(): void
{
$admin = $this->adminWithoutPermissions();
$document = Document::factory()->create(['code' => '123456']);
$doctor = User::factory()->create(['level' => UserEnum::Doctor->value]);
Report::factory()->draft()->create(['created_at' => now()->subMinute()]);
Report::factory()->approved()->create(['document_id' => $document->id, 'doctor_id' => $doctor->id]);
$this->actingAs($admin)->get(route('admin.reports.index'))->assertOk()->assertInertia(fn (Assert $page) => $page
->component('Reports/index')
->has('reports.data', 2)
->where('reports.data.1.doctor', null)
->where('reports.data.0.doctor.id', $doctor->id));
$this->actingAs($admin)->get(route('admin.reports.index', ['status' => ReportStatusEnum::Draft->value]))->assertInertia(fn (Assert $page) => $page->has('reports.data', 1));
$this->actingAs($admin)->get(route('admin.reports.index', ['search' => '1234']))->assertInertia(fn (Assert $page) => $page->has('reports.data', 1));
}
public function test_store_creates_report(): void
{
$payload = $this->payload();
$this->actingAs($this->adminWithoutPermissions())->post(route('admin.reports.store'), $payload)->assertOk()->assertSessionHas('success');
$this->assertDatabaseHas('reports', ['document_id' => $payload['document_id'], 'doctor_id' => $payload['doctor_id'], 'status' => ReportStatusEnum::Approved->value, 'body' => 'متن']);
}
public function test_store_validates_input(): void
{
$this->actingAs($this->adminWithoutPermissions())->postJson(route('admin.reports.store'), [])->assertUnprocessable()->assertJsonValidationErrors(['document_id', 'doctor_id', 'status', 'body', 'submitted_at']);
}
public function test_edit_renders_report_prop_for_missing_page(): void
{
$report = Report::factory()->create();
$this->actingAs($this->adminWithoutPermissions())->get(route('admin.reports.edit', $report))->assertInertia(fn (Assert $page) => $page->component('Reports/edit', false)->where('report.id', $report->id));
}
public function test_update_changes_report(): void
{
$report = Report::factory()->draft()->create();
$this->actingAs($this->adminWithoutPermissions())->put(route('admin.reports.update', $report), $this->payload(['status' => ReportStatusEnum::Rejected->name, 'body' => 'جدید']))->assertOk();
$this->assertDatabaseHas('reports', ['id' => $report->id, 'status' => ReportStatusEnum::Rejected->value, 'body' => 'جدید']);
}
public function test_destroy_deletes_report(): void
{
$report = Report::factory()->create();
$this->actingAs($this->adminWithoutPermissions())->delete(route('admin.reports.destroy', $report))->assertOk();
$this->assertDatabaseMissing('reports', ['id' => $report->id]);
}
}
@@ -0,0 +1,99 @@
<?php
namespace Tests\Feature\Http\Admin\Role;
use App\Models\Permission;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Inertia\Testing\AssertableInertia as Assert;
use Maatwebsite\Excel\Facades\Excel;
use Tests\Concerns\InteractsWithAdmin;
use Tests\TestCase;
class PermissionControllerTest extends TestCase
{
use InteractsWithAdmin, RefreshDatabase;
public function test_guest_is_redirected(): void
{
$this->get(route('admin.permissions.index'))->assertRedirect(route('login'));
}
public function test_admin_without_permission_is_forbidden(): void
{
$admin = $this->adminWithoutPermissions();
$this->actingAs($admin)->get(route('admin.permissions.index'))->assertForbidden();
$this->actingAs($admin)->post(route('admin.permissions.store'), ['name' => 'x', 'label' => 'y'])->assertForbidden();
$this->actingAs($admin)->post('/admin/permissions-bulk-delete', ['ids' => []])->assertForbidden();
}
public function test_index_renders_with_search(): void
{
$admin = $this->adminWithAllPermissions();
$this->actingAs($admin)->get(route('admin.permissions.index'))->assertOk()->assertInertia(fn (Assert $page) => $page->component('Permissions/index')->has('permissions.data', 20));
$this->actingAs($admin)->get(route('admin.permissions.index', ['search' => 'withdrawals']))->assertInertia(fn (Assert $page) => $page->has('permissions.data', 2));
}
public function test_create_and_edit_pages_render_with_parent_permissions(): void
{
$admin = $this->adminWithAllPermissions();
$child = Permission::where('name', 'users.index')->first();
$parents = Permission::where('parent_id', 0)->count();
$this->actingAs($admin)->get(route('admin.permissions.create'))->assertInertia(fn (Assert $page) => $page->component('Permissions/create_and_edit')->has('permissions.data', $parents));
$this->actingAs($admin)->get(route('admin.permissions.edit', $child))->assertInertia(fn (Assert $page) => $page->where('is_edit', true)->where('permission.id', $child->id)->where('permission.parent_id', $child->parent_id));
}
public function test_store_creates_permission_with_parent(): void
{
$admin = $this->adminWithAllPermissions();
$parent = Permission::where('name', 'users')->first();
$this->actingAs($admin)->post(route('admin.permissions.store'), ['name' => 'users.export', 'label' => 'خروجی', 'parent_id' => $parent->id])->assertCreated()->assertSessionHas('success');
$this->assertDatabaseHas('permissions', ['name' => 'users.export', 'label' => 'خروجی', 'parent_id' => $parent->id]);
}
public function test_store_defaults_parent_to_zero_and_validates(): void
{
$admin = $this->adminWithAllPermissions();
$this->actingAs($admin)->post(route('admin.permissions.store'), ['name' => 'reports2', 'label' => 'x'])->assertCreated();
$this->assertDatabaseHas('permissions', ['name' => 'reports2', 'parent_id' => 0]);
$this->actingAs($admin)->postJson(route('admin.permissions.store'), [])->assertUnprocessable()->assertJsonValidationErrors(['name', 'label']);
}
public function test_update_changes_permission(): void
{
$admin = $this->adminWithAllPermissions();
$permission = Permission::where('name', 'users.index')->first();
$this->actingAs($admin)->put(route('admin.permissions.update', $permission), ['name' => 'users.list', 'label' => 'لیست'])->assertOk();
$this->assertDatabaseHas('permissions', ['id' => $permission->id, 'name' => 'users.list', 'label' => 'لیست', 'parent_id' => 0]);
}
public function test_destroy_and_bulk_delete_remove_permissions(): void
{
$admin = $this->adminWithAllPermissions();
$targets = Permission::where('name', 'like', 'pets.%')->take(3)->get();
$this->actingAs($admin)->delete(route('admin.permissions.destroy', $targets[0]))->assertOk();
$this->actingAs($admin)->post('/admin/permissions-bulk-delete', ['ids' => [$targets[1]->id, $targets[2]->id]])->assertOk();
foreach ($targets as $permission) {
$this->assertDatabaseMissing('permissions', ['id' => $permission->id]);
}
}
public function test_export_data_downloads_excel(): void
{
Excel::fake();
$this->actingAs($this->adminWithAllPermissions())->get('/admin/permissions-export-data')->assertOk();
Excel::assertDownloaded('permissions.xlsx');
}
}
@@ -0,0 +1,103 @@
<?php
namespace Tests\Feature\Http\Admin\Role;
use App\Models\Permission;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Inertia\Testing\AssertableInertia as Assert;
use Maatwebsite\Excel\Facades\Excel;
use Spatie\Permission\Models\Role;
use Tests\Concerns\InteractsWithAdmin;
use Tests\TestCase;
class RoleControllerTest extends TestCase
{
use InteractsWithAdmin, RefreshDatabase;
public function test_guest_is_redirected(): void
{
$this->get(route('admin.roles.index'))->assertRedirect(route('login'));
}
public function test_admin_without_permission_is_forbidden(): void
{
$admin = $this->adminWithoutPermissions();
$this->actingAs($admin)->get(route('admin.roles.index'))->assertForbidden();
$this->actingAs($admin)->post(route('admin.roles.store'), ['name' => 'x', 'label' => 'y'])->assertForbidden();
$this->actingAs($admin)->post('/admin/roles-bulk-delete', ['ids' => []])->assertForbidden();
}
public function test_index_renders_with_search(): void
{
$admin = $this->adminWithAllPermissions();
Role::create(['name' => 'editor', 'label' => 'ویرایشگر']);
$this->actingAs($admin)->get(route('admin.roles.index'))->assertOk()->assertInertia(fn (Assert $page) => $page->component('Roles/index')->has('roles.data', 2));
$this->actingAs($admin)->get(route('admin.roles.index', ['search' => 'edit']))->assertInertia(fn (Assert $page) => $page->has('roles.data', 1));
}
public function test_create_and_edit_pages_render_with_grouped_permissions(): void
{
$admin = $this->adminWithAllPermissions();
$role = Role::where('name', 'admin')->first();
$parent = Permission::where('name', 'users')->first();
$this->actingAs($admin)->get(route('admin.roles.create'))->assertInertia(fn (Assert $page) => $page->component('Roles/create_and_edit')->has('permissions'));
$this->actingAs($admin)->get(route('admin.roles.edit', $role))->assertInertia(fn (Assert $page) => $page
->component('Roles/create_and_edit')
->where('is_edit', true)
->where('role.id', $role->id)
->has("role.permissions.{$parent->id}"));
}
public function test_store_creates_role_with_permissions(): void
{
$admin = $this->adminWithAllPermissions();
$permissionIds = Permission::where('name', 'like', 'users.%')->pluck('id')->all();
$this->actingAs($admin)->post(route('admin.roles.store'), ['name' => 'editor', 'label' => 'ویرایشگر', 'selectedPermissionIds' => [$permissionIds]])->assertCreated()->assertSessionHas('success');
$role = Role::where('name', 'editor')->first();
$this->assertNotNull($role);
$this->assertEqualsCanonicalizing($permissionIds, $role->permissions->pluck('id')->all());
}
public function test_store_validates_input(): void
{
$this->actingAs($this->adminWithAllPermissions())->postJson(route('admin.roles.store'), [])->assertUnprocessable()->assertJsonValidationErrors(['name', 'label']);
}
public function test_update_changes_role_and_permissions(): void
{
$admin = $this->adminWithAllPermissions();
$role = Role::create(['name' => 'editor', 'label' => 'x']);
$permission = Permission::where('name', 'pets.index')->first();
$this->actingAs($admin)->put(route('admin.roles.update', $role), ['name' => 'editor2', 'label' => 'y', 'selectedPermissionIds' => [[$permission->id]]])->assertOk();
$role->refresh();
$this->assertSame('editor2', $role->name);
$this->assertTrue($role->hasPermissionTo($permission));
}
public function test_destroy_and_bulk_delete_remove_roles(): void
{
$admin = $this->adminWithAllPermissions();
$roles = collect(['a', 'b', 'c'])->map(fn ($name) => Role::create(['name' => $name, 'label' => $name]));
$this->actingAs($admin)->delete(route('admin.roles.destroy', $roles[0]))->assertOk();
$this->actingAs($admin)->post('/admin/roles-bulk-delete', ['ids' => [$roles[1]->id, $roles[2]->id]])->assertOk();
$this->assertSame(1, Role::count());
}
public function test_export_data_downloads_excel(): void
{
Excel::fake();
$this->actingAs($this->adminWithAllPermissions())->get('/admin/roles-export-data')->assertOk();
Excel::assertDownloaded('roles.xlsx');
}
}
@@ -0,0 +1,122 @@
<?php
namespace Tests\Feature\Http\Admin;
use App\Models\Medium;
use App\Models\Session;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Storage;
use Inertia\Testing\AssertableInertia as Assert;
use Tests\Concerns\InteractsWithAdmin;
use Tests\TestCase;
class SettingUserControllerTest extends TestCase
{
use InteractsWithAdmin, RefreshDatabase;
protected function storedSession(User $user, string $id): void
{
Session::forceCreate(['id' => $id, 'user_id' => $user->id, 'ip_address' => '127.0.0.1', 'user_agent' => 'phpunit', 'payload' => '', 'last_activity' => now()->timestamp]);
}
public function test_guest_is_redirected(): void
{
$this->get('/admin/setting-profile')->assertRedirect(route('login'));
}
public function test_pages_render(): void
{
$admin = $this->adminWithoutPermissions();
$this->storedSession($admin, 'sess-1');
$this->storedSession(User::factory()->create(), 'sess-2');
$this->actingAs($admin)->get('/admin/setting-profile')->assertOk()->assertInertia(fn (Assert $page) => $page->component('Settings/User/Profile'));
$this->actingAs($admin)->get('/admin/setting-profile/security')->assertOk()->assertInertia(fn (Assert $page) => $page->component('Settings/User/Security'));
$this->actingAs($admin)->get('/admin/setting-profile/sessions')->assertOk()->assertInertia(fn (Assert $page) => $page->component('Settings/User/Sessions')->has('sessions', 1)->where('sessions.0.id', 'sess-1'));
}
public function test_update_data_forbids_editing_another_user(): void
{
$admin = $this->adminWithoutPermissions();
$other = User::factory()->create();
$this->actingAs($admin)->post("/admin/setting-profile/update-data/{$other->id}", ['name' => 'x'])->assertForbidden();
}
public function test_upload_avatar_stores_file_and_replaces_previous(): void
{
Storage::fake('public');
$old = Medium::factory()->public()->create(['path' => 'profile/old.png', 'url' => 'http://example.test/storage/profile/old.png']);
Storage::disk('public')->put($old->path, 'old');
$admin = $this->adminWithoutPermissions();
$admin->update(['profile_url' => $old->url]);
$response = $this->actingAs($admin)->post('/admin/setting-profile/update-avatar', ['file' => UploadedFile::fake()->image('new.png')]);
$response->assertOk();
$this->assertDatabaseMissing('media', ['id' => $old->id]);
$this->assertSame($response->json('profile_url'), $admin->fresh()->profile_url);
$this->assertStringStartsWith('profile/', Medium::first()->path);
}
public function test_upload_avatar_validates_type_and_size(): void
{
$this->actingAs($this->adminWithoutPermissions())->postJson('/admin/setting-profile/update-avatar', ['file' => UploadedFile::fake()->create('a.pdf', 10, 'application/pdf')])
->assertUnprocessable()
->assertJsonValidationErrors('file');
}
public function test_delete_session_removes_own_session_only(): void
{
$admin = $this->adminWithoutPermissions();
$this->storedSession($admin, 'own');
$this->storedSession(User::factory()->create(), 'foreign');
$this->actingAs($admin)->delete('/admin/setting-profile/sessions/own')->assertOk()->assertSessionHas('success');
$this->actingAs($admin)->delete('/admin/setting-profile/sessions/foreign')->assertStatus(400)->assertSessionHas('error');
$this->assertDatabaseMissing('sessions', ['id' => 'own']);
$this->assertDatabaseHas('sessions', ['id' => 'foreign']);
}
public function test_bulk_delete_removes_sessions(): void
{
$admin = $this->adminWithoutPermissions();
$this->storedSession($admin, 'a');
$this->storedSession($admin, 'b');
$this->actingAs($admin)->post('/admin/setting-profile/sessions-bulk-delete', ['ids' => ['a', 'b']])->assertOk();
$this->assertDatabaseCount('sessions', 0);
}
public function test_change_password_updates_password_when_current_matches(): void
{
$admin = $this->adminWithoutPermissions();
$admin->update(['password' => 'oldsecret1']);
$this->actingAs($admin)->post('/admin/setting-profile/change-password', ['password' => 'oldsecret1', 'newPassword' => 'newsecret1', 'newPassword_confirmation' => 'newsecret1'])->assertOk()->assertSessionHas('success');
$this->assertTrue(Hash::check('newsecret1', $admin->fresh()->password));
}
public function test_change_password_rejects_wrong_current_password(): void
{
$admin = $this->adminWithoutPermissions();
$admin->update(['password' => 'oldsecret1']);
$this->actingAs($admin)->postJson('/admin/setting-profile/change-password', ['password' => 'wrongpass1', 'newPassword' => 'newsecret1', 'newPassword_confirmation' => 'newsecret1'])->assertUnauthorized();
$this->assertTrue(Hash::check('oldsecret1', $admin->fresh()->password));
}
public function test_change_password_validates_input(): void
{
$this->actingAs($this->adminWithoutPermissions())->postJson('/admin/setting-profile/change-password', ['password' => 'short', 'newPassword' => 'newsecret1', 'newPassword_confirmation' => 'other'])
->assertUnprocessable()
->assertJsonValidationErrors(['password', 'newPassword']);
}
}
@@ -0,0 +1,126 @@
<?php
namespace Tests\Feature\Http\Admin;
use App\Enums\ShiftDayEnum;
use App\Enums\UserEnum;
use App\Models\Shift;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Inertia\Testing\AssertableInertia as Assert;
use Tests\Concerns\InteractsWithAdmin;
use Tests\TestCase;
class ShiftControllerTest extends TestCase
{
use InteractsWithAdmin, RefreshDatabase;
protected function payload(array $overrides = []): array
{
return array_merge([
'day_of_week' => ShiftDayEnum::Monday->value,
'start_time' => '08:00',
'end_time' => '16:00',
'is_active' => true,
], $overrides);
}
public function test_guest_is_redirected(): void
{
$this->get(route('admin.shifts.index'))->assertRedirect(route('login'));
}
public function test_admin_without_permission_is_forbidden(): void
{
$admin = $this->adminWithoutPermissions();
$shift = Shift::factory()->create();
$this->actingAs($admin)->get(route('admin.shifts.index'))->assertForbidden();
$this->actingAs($admin)->get(route('admin.shifts.create'))->assertForbidden();
$this->actingAs($admin)->post(route('admin.shifts.store'), $this->payload())->assertForbidden();
$this->actingAs($admin)->get(route('admin.shifts.edit', $shift))->assertForbidden();
$this->actingAs($admin)->put(route('admin.shifts.update', $shift), $this->payload())->assertForbidden();
$this->actingAs($admin)->delete(route('admin.shifts.destroy', $shift))->assertForbidden();
$this->actingAs($admin)->post('/admin/shifts-bulk-delete', ['ids' => [$shift->id]])->assertForbidden();
$this->actingAs($admin)->get('/admin/shifts-calendar-data')->assertForbidden();
}
public function test_index_renders_with_filters(): void
{
$admin = $this->adminWithAllPermissions();
$doctor = User::factory()->create(['level' => UserEnum::Doctor->value, 'fullname' => 'دکتر الف']);
Shift::factory()->weekly(ShiftDayEnum::Monday)->forDoctor($doctor)->create();
Shift::factory()->weekly(ShiftDayEnum::Tuesday)->create();
Shift::factory()->weekly(ShiftDayEnum::Tuesday)->inactive()->create();
$this->actingAs($admin)->get(route('admin.shifts.index'))
->assertOk()
->assertInertia(fn (Assert $page) => $page->component('Shifts/index')->has('shifts.data', 3)->where('permissions_edit', false)->where('permissions_delete', false));
$this->actingAs($admin)->get(route('admin.shifts.index', ['day_of_week' => ShiftDayEnum::Tuesday->value]))->assertInertia(fn (Assert $page) => $page->has('shifts.data', 2));
$this->actingAs($admin)->get(route('admin.shifts.index', ['status' => 'فعال']))->assertInertia(fn (Assert $page) => $page->has('shifts.data', 2));
$this->actingAs($admin)->get(route('admin.shifts.index', ['occupancy' => 'آزاد']))->assertInertia(fn (Assert $page) => $page->has('shifts.data', 2));
$this->actingAs($admin)->get(route('admin.shifts.index', ['search' => 'الف']))->assertInertia(fn (Assert $page) => $page->has('shifts.data', 1));
}
public function test_create_and_edit_pages_render(): void
{
$admin = $this->adminWithAllPermissions();
$shift = Shift::factory()->create();
$this->actingAs($admin)->get(route('admin.shifts.create'))->assertInertia(fn (Assert $page) => $page->component('Shifts/create_and_edit')->missing('is_edit'));
$this->actingAs($admin)->get(route('admin.shifts.edit', $shift))->assertInertia(fn (Assert $page) => $page->component('Shifts/create_and_edit')->where('is_edit', true)->where('shift.id', $shift->id));
}
public function test_store_creates_shift_with_creator(): void
{
$admin = $this->adminWithAllPermissions();
$doctor = User::factory()->create(['level' => UserEnum::Doctor->value]);
$this->actingAs($admin)->post(route('admin.shifts.store'), $this->payload(['user_id' => $doctor->id]))->assertOk()->assertSessionHas('success');
$this->assertDatabaseHas('shifts', ['day_of_week' => ShiftDayEnum::Monday->value, 'start_time' => '08:00', 'end_time' => '16:00', 'user_id' => $doctor->id, 'created_by' => $admin->id, 'is_active' => 1]);
}
public function test_store_rejects_conflicting_shift(): void
{
$admin = $this->adminWithAllPermissions();
$doctor = User::factory()->create(['level' => UserEnum::Doctor->value]);
Shift::factory()->weekly(ShiftDayEnum::Monday)->between('08:00', '12:00')->forDoctor($doctor)->create();
$this->actingAs($admin)->postJson(route('admin.shifts.store'), $this->payload(['user_id' => $doctor->id, 'start_time' => '10:00', 'end_time' => '14:00']))
->assertUnprocessable()
->assertJsonValidationErrors('start_time');
}
public function test_update_changes_shift(): void
{
$admin = $this->adminWithAllPermissions();
$shift = Shift::factory()->weekly(ShiftDayEnum::Monday)->create();
$this->actingAs($admin)->put(route('admin.shifts.update', $shift), $this->payload(['day_of_week' => null, 'date' => '2026-09-21', 'is_active' => false]))->assertOk();
$shift->refresh();
$this->assertNull($shift->day_of_week);
$this->assertSame('2026-09-21', $shift->date->toDateString());
$this->assertFalse($shift->is_active);
}
public function test_destroy_and_bulk_delete_remove_shifts(): void
{
$admin = $this->adminWithAllPermissions();
$shifts = Shift::factory()->count(3)->create();
$this->actingAs($admin)->delete(route('admin.shifts.destroy', $shifts[0]))->assertOk();
$this->actingAs($admin)->post('/admin/shifts-bulk-delete', ['ids' => [$shifts[1]->id, $shifts[2]->id]])->assertOk();
$this->assertDatabaseCount('shifts', 0);
}
public function test_calendar_data_returns_all_shifts(): void
{
Shift::factory()->count(2)->create();
$this->actingAs($this->adminWithAllPermissions())->getJson('/admin/shifts-calendar-data')->assertOk()->assertJsonPath('success', true)->assertJsonCount(2, 'data');
}
}
@@ -0,0 +1,155 @@
<?php
namespace Tests\Feature\Http\Admin;
use App\Enums\UserEnum;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Hash;
use Inertia\Testing\AssertableInertia as Assert;
use Maatwebsite\Excel\Facades\Excel;
use Spatie\Permission\Models\Role;
use Tests\Concerns\InteractsWithAdmin;
use Tests\TestCase;
class UserControllerTest extends TestCase
{
use InteractsWithAdmin, RefreshDatabase;
protected function payload(array $overrides = []): array
{
return array_merge([
'username' => 'newuser',
'phone' => '09120000000',
'fullname' => 'کاربر جدید',
'email' => 'new@example.com',
'password' => 'secret123',
'password_confirmation' => 'secret123',
'level' => UserEnum::Doctor->value,
'national_code' => '0012345678',
], $overrides);
}
public function test_guest_is_redirected(): void
{
$this->get(route('admin.users.index'))->assertRedirect(route('login'));
}
public function test_admin_without_permission_is_forbidden(): void
{
$admin = $this->adminWithoutPermissions();
$this->actingAs($admin)->get(route('admin.users.index'))->assertForbidden();
$this->actingAs($admin)->post(route('admin.users.store'), $this->payload())->assertForbidden();
$this->actingAs($admin)->post('/admin/users-bulk-delete', ['ids' => []])->assertForbidden();
$this->actingAs($admin)->get('/admin/doctors-options')->assertForbidden();
}
public function test_index_renders_with_filters(): void
{
$admin = $this->adminWithAllPermissions();
User::factory()->create(['level' => UserEnum::Doctor->value, 'fullname' => 'دکتر الف']);
User::factory()->create(['level' => UserEnum::Vet->value]);
$this->actingAs($admin)->get(route('admin.users.index'))->assertOk()->assertInertia(fn (Assert $page) => $page->component('Users/index')->has('users.data', 3));
$this->actingAs($admin)->get(route('admin.users.index', ['level' => UserEnum::Doctor->value]))->assertInertia(fn (Assert $page) => $page->has('users.data', 1));
$this->actingAs($admin)->get(route('admin.users.index', ['search' => 'الف']))->assertInertia(fn (Assert $page) => $page->has('users.data', 1));
}
public function test_create_and_edit_pages_render_with_roles(): void
{
$admin = $this->adminWithAllPermissions();
$user = User::factory()->create();
$this->actingAs($admin)->get(route('admin.users.create'))->assertInertia(fn (Assert $page) => $page->component('Users/create_and_edit')->has('roles', 1));
$this->actingAs($admin)->get(route('admin.users.edit', $user))->assertInertia(fn (Assert $page) => $page->component('Users/create_and_edit')->where('is_edit', true)->where('user.id', $user->id)->where('user.role', []));
}
public function test_store_creates_user_with_hashed_password_and_roles(): void
{
$admin = $this->adminWithAllPermissions();
$role = Role::first();
$this->actingAs($admin)->post(route('admin.users.store'), $this->payload(['role' => [$role->id]]))->assertOk()->assertSessionHas('success');
$user = User::where('username', 'newuser')->first();
$this->assertNotNull($user);
$this->assertTrue(Hash::check('secret123', $user->password));
$this->assertSame(UserEnum::Doctor->value, $user->level);
$this->assertTrue($user->hasRole($role));
}
public function test_store_validates_input(): void
{
$this->actingAs($this->adminWithAllPermissions())->postJson(route('admin.users.store'), ['level' => UserEnum::Vet->value])
->assertUnprocessable()
->assertJsonValidationErrors(['phone', 'password', 'level']);
}
public function test_update_changes_user_and_syncs_roles(): void
{
$admin = $this->adminWithAllPermissions();
$role = Role::first();
$user = User::factory()->create(['level' => UserEnum::Doctor->value]);
$user->assignRole($role);
$originalPassword = $user->password;
$this->actingAs($admin)->put(route('admin.users.update', $user), $this->payload([
'username' => 'renamed',
'level' => UserEnum::Supervisor->value,
'password' => null,
'password_confirmation' => null,
]))->assertOk();
$user->refresh();
$this->assertSame('renamed', $user->username);
$this->assertSame(UserEnum::Supervisor->value, $user->level);
$this->assertSame($originalPassword, $user->password);
$this->assertFalse($user->hasRole($role));
}
public function test_update_changes_password_when_provided(): void
{
$user = User::factory()->create();
$this->actingAs($this->adminWithAllPermissions())->put(route('admin.users.update', $user), $this->payload([
'password' => 'newsecret1',
'password_confirmation' => 'newsecret1',
]))->assertOk();
$this->assertTrue(Hash::check('newsecret1', $user->fresh()->password));
}
public function test_destroy_and_bulk_delete_soft_delete_users(): void
{
$admin = $this->adminWithAllPermissions();
$users = User::factory()->count(3)->create();
$this->actingAs($admin)->delete(route('admin.users.destroy', $users[0]))->assertOk();
$this->actingAs($admin)->post('/admin/users-bulk-delete', ['ids' => [$users[1]->id, $users[2]->id]])->assertOk();
foreach ($users as $user) {
$this->assertDatabaseMissing('users', ['id' => $user->id]);
}
}
public function test_get_doctors_options_returns_only_doctors(): void
{
$doctor = User::factory()->create(['level' => UserEnum::Doctor->value, 'fullname' => 'دکتر ب']);
User::factory()->create(['level' => UserEnum::Vet->value, 'fullname' => 'دکتر ب']);
$this->actingAs($this->adminWithAllPermissions())->getJson('/admin/doctors-options?search=ب')
->assertOk()
->assertJsonCount(1, 'data')
->assertJsonPath('data.0.value', $doctor->id);
}
public function test_export_data_downloads_excel(): void
{
Excel::fake();
$this->actingAs($this->adminWithAllPermissions())->get('/admin/users-export-data')->assertOk();
Excel::assertDownloaded('users.xlsx');
}
}
@@ -0,0 +1,104 @@
<?php
namespace Tests\Feature\Http\Admin;
use App\Enums\UserEnum;
use App\Enums\WithdrawRequestStatusEnum;
use App\Models\User;
use App\Models\Wallet;
use App\Models\WithdrawRequest;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Inertia\Testing\AssertableInertia as Assert;
use Tests\Concerns\InteractsWithAdmin;
use Tests\TestCase;
class WithdrawRequestControllerTest extends TestCase
{
use InteractsWithAdmin, RefreshDatabase;
public function test_routes_are_protected_from_guests_and_non_admins(): void
{
$withdrawRequest = WithdrawRequest::factory()->create();
$this->get(route('admin.withdraw-requests.index'))->assertRedirect(route('login'));
$this->actingAs(User::factory()->create(['level' => UserEnum::Doctor->value]))->post(route('admin.withdraw-requests.approve', $withdrawRequest))->assertNotFound();
}
public function test_index_renders_requests_with_stats_and_filters(): void
{
$admin = $this->adminWithoutPermissions();
Wallet::factory()->system()->create(['balance' => 900]);
WithdrawRequest::factory()->create(['amount' => 100000]);
WithdrawRequest::factory()->create(['amount' => 250000]);
WithdrawRequest::factory()->approved()->create(['amount' => 500000]);
$this->actingAs($admin)->get(route('admin.withdraw-requests.index'))
->assertOk()
->assertInertia(fn (Assert $page) => $page
->component('WithdrawRequests/index')
->has('withdrawRequests.data', 3)
->where('stats.pending_count', 2)
->where('stats.pending_amount', 350000)
->where('stats.system_wallet_balance', 900));
$this->actingAs($admin)->get(route('admin.withdraw-requests.index', ['status' => WithdrawRequestStatusEnum::Approved->value]))
->assertInertia(fn (Assert $page) => $page->has('withdrawRequests.data', 1));
}
public function test_index_searches_by_user_fullname(): void
{
$admin = $this->adminWithoutPermissions();
$user = User::factory()->create(['fullname' => 'کاربر خاص']);
WithdrawRequest::factory()->create(['user_id' => $user->id]);
WithdrawRequest::factory()->create();
$this->actingAs($admin)->get(route('admin.withdraw-requests.index', ['search' => 'خاص']))
->assertInertia(fn (Assert $page) => $page->has('withdrawRequests.data', 1));
}
public function test_approve_moves_funds_and_marks_request_approved(): void
{
$admin = $this->adminWithoutPermissions();
$wallet = Wallet::factory()->create(['balance' => 500000]);
$withdrawRequest = WithdrawRequest::factory()->create(['user_id' => $wallet->user_id, 'amount' => 200000]);
$this->actingAs($admin)->post(route('admin.withdraw-requests.approve', $withdrawRequest))->assertOk()->assertSessionHas('success');
$withdrawRequest->refresh();
$this->assertSame(WithdrawRequestStatusEnum::Approved, $withdrawRequest->status);
$this->assertSame($admin->id, $withdrawRequest->reviewed_by);
$this->assertSame(300000, $wallet->fresh()->balance);
$this->assertDatabaseHas('wallets', ['user_id' => null, 'balance' => 200000]);
}
public function test_approve_returns_validation_error_for_reviewed_request(): void
{
$withdrawRequest = WithdrawRequest::factory()->rejected()->create();
$this->actingAs($this->adminWithoutPermissions())->postJson(route('admin.withdraw-requests.approve', $withdrawRequest))
->assertUnprocessable()
->assertJsonValidationErrors('withdraw_request');
}
public function test_reject_marks_request_rejected_with_note(): void
{
$admin = $this->adminWithoutPermissions();
$withdrawRequest = WithdrawRequest::factory()->create();
$this->actingAs($admin)->post(route('admin.withdraw-requests.reject', $withdrawRequest), ['note' => 'کارت نامعتبر'])->assertOk();
$withdrawRequest->refresh();
$this->assertSame(WithdrawRequestStatusEnum::Rejected, $withdrawRequest->status);
$this->assertSame('کارت نامعتبر', $withdrawRequest->admin_note);
$this->assertSame($admin->id, $withdrawRequest->reviewed_by);
}
public function test_reject_validates_note_length(): void
{
$withdrawRequest = WithdrawRequest::factory()->create();
$this->actingAs($this->adminWithoutPermissions())->postJson(route('admin.withdraw-requests.reject', $withdrawRequest), ['note' => str_repeat('a', 1001)])
->assertUnprocessable()
->assertJsonValidationErrors('note');
}
}