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
+22
View File
@@ -0,0 +1,22 @@
<?php
namespace Tests\Concerns;
use App\Enums\UserEnum;
use App\Models\User;
use Database\Seeders\PermissionSeeder;
trait InteractsWithAdmin
{
protected function adminWithAllPermissions(): User
{
$this->seed(PermissionSeeder::class);
return User::where('email', 'admin@gmail.com')->firstOrFail();
}
protected function adminWithoutPermissions(): User
{
return User::factory()->create(['level' => UserEnum::Admin->value]);
}
}
@@ -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');
}
}
@@ -0,0 +1,256 @@
<?php
namespace Tests\Feature\Http\Client;
use App\Enums\UserEnum;
use App\Models\OtpCode;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Http;
use Inertia\Testing\AssertableInertia as Assert;
use Tests\TestCase;
class AuthControllerTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
Http::fake();
}
public function test_phone_page_renders(): void
{
$this->get(route('login'))->assertOk()->assertInertia(fn (Assert $page) => $page->component('Auth/Phone'));
}
public function test_authenticated_user_is_redirected_away_from_guest_routes(): void
{
$this->actingAs(User::factory()->create())->get(route('login'))->assertRedirect();
}
public function test_check_phone_redirects_to_password_when_phone_has_single_level(): void
{
User::factory()->create(['phone' => '+989120000000', 'level' => UserEnum::Doctor->value]);
$response = $this->post(route('checkPhone'), ['prefix' => '+98', 'phone' => '09120000000']);
$response->assertRedirect(route('password'));
$response->assertSessionHas('login_phone', '+989120000000');
$response->assertSessionHas('login_level', 'doctor');
}
public function test_check_phone_redirects_to_portal_when_phone_has_multiple_levels(): void
{
User::factory()->create(['phone' => '+989120000000', 'level' => UserEnum::Doctor->value]);
User::factory()->create(['phone' => '+989120000000', 'level' => UserEnum::Supervisor->value]);
$response = $this->post(route('checkPhone'), ['prefix' => '+98', 'phone' => '09120000000']);
$response->assertRedirect(route('portal'));
$response->assertSessionHas('available_levels');
}
public function test_check_phone_redirects_to_registration_for_unknown_phone(): void
{
$response = $this->post(route('checkPhone'), ['prefix' => '+98', 'phone' => '09120000000']);
$response->assertRedirect(route('register.chooseType'));
$response->assertSessionHas('register_phone', '+989120000000');
}
public function test_check_phone_validates_input(): void
{
$this->post(route('checkPhone'), ['phone' => 'abc'])->assertSessionHasErrors(['prefix', 'phone']);
}
public function test_choose_portal_requires_session_state(): void
{
$this->get(route('portal'))->assertRedirect(route('login'));
}
public function test_choose_portal_renders_available_levels(): void
{
$this->withSession(['login_phone' => '+989120000000', 'available_levels' => ['doctor', 'supervisor']])
->get(route('portal'))
->assertOk()
->assertInertia(fn (Assert $page) => $page->component('Auth/ChoosePortal')->where('availableLevels', ['doctor', 'supervisor']));
}
public function test_choose_portal_with_level_stores_it_and_redirects_to_password(): void
{
$this->withSession(['login_phone' => '+989120000000', 'available_levels' => ['doctor', 'supervisor']])
->get(route('portal', 'doctor'))
->assertRedirect(route('password'))
->assertSessionHas('login_level', 'doctor');
}
public function test_password_form_requires_session_state(): void
{
$this->get(route('password'))->assertRedirect(route('login'));
}
public function test_password_form_renders(): void
{
$this->withSession(['login_phone' => '+989120000000', 'login_level' => 'doctor'])
->get(route('password'))
->assertOk()
->assertInertia(fn (Assert $page) => $page->component('Auth/Password')->where('level', 'doctor'));
}
public function test_authenticate_logs_in_and_redirects_to_dashboard(): void
{
$user = User::factory()->create(['phone' => '+989120000000', 'level' => UserEnum::Doctor->value, 'password' => 'secret123']);
$this->withSession(['login_phone' => '+989120000000', 'login_level' => 'doctor'])
->post(route('authenticate'), ['password' => 'secret123'])
->assertRedirect(route('dashboard.index'));
$this->assertAuthenticatedAs($user);
}
public function test_authenticate_rejects_wrong_password(): void
{
User::factory()->create(['phone' => '+989120000000', 'level' => UserEnum::Doctor->value, 'password' => 'secret123']);
$this->withSession(['login_phone' => '+989120000000', 'login_level' => 'doctor'])
->from(route('password'))
->post(route('authenticate'), ['password' => 'wrong'])
->assertRedirect(route('password'))
->assertSessionHasErrors('password');
$this->assertGuest();
}
public function test_authenticate_without_session_redirects_to_login(): void
{
$this->post(route('authenticate'), ['password' => 'secret123'])->assertRedirect(route('login'));
}
public function test_send_otp_stores_code_and_redirects_to_verification(): void
{
$this->withSession(['login_phone' => '+989120000000'])
->post(route('sendOtp'))
->assertRedirect(route('otpVerification'));
$this->assertDatabaseHas('otp_codes', ['phone' => '+989120000000']);
}
public function test_send_otp_without_phone_redirects_to_login(): void
{
$this->post(route('sendOtp'))->assertRedirect(route('login'));
$this->assertDatabaseCount('otp_codes', 0);
}
public function test_otp_verification_page_renders_with_phone(): void
{
$this->withSession(['login_phone' => '+989120000000'])
->get(route('otpVerification'))
->assertOk()
->assertInertia(fn (Assert $page) => $page->component('Auth/OtpVerification')->where('phone', '+989120000000'));
}
public function test_verify_otp_marks_session_verified_and_redirects_to_reset(): void
{
OtpCode::factory()->create(['phone' => '+989120000000', 'code' => '1234']);
$this->withSession(['login_phone' => '+989120000000'])
->post(route('verifyOtp'), ['otp_code' => '1234'])
->assertRedirect(route('resetPassword'))
->assertSessionHas('otp_verified', true)
->assertSessionHas('verified_phone', '+989120000000');
$this->assertDatabaseCount('otp_codes', 0);
}
public function test_verify_otp_rejects_invalid_code(): void
{
OtpCode::factory()->create(['phone' => '+989120000000', 'code' => '1234']);
$this->withSession(['login_phone' => '+989120000000'])
->post(route('verifyOtp'), ['otp_code' => '0000'])
->assertSessionHasErrors('otp_code');
}
public function test_reset_password_page_requires_verified_session(): void
{
$this->get(route('resetPassword'))->assertRedirect(route('login'));
$this->withSession(['otp_verified' => true, 'verified_phone' => '+989120000000'])
->get(route('resetPassword'))
->assertOk()
->assertInertia(fn (Assert $page) => $page->component('Auth/ResetPassword'));
}
public function test_update_password_changes_password_and_flushes_session(): void
{
$user = User::factory()->create(['phone' => '+989120000000', 'level' => UserEnum::Doctor->value]);
$this->withSession(['otp_verified' => true, 'verified_phone' => '+989120000000', 'login_level' => 'doctor'])
->post(route('updatePassword'), ['password' => 'newsecret1', 'password_confirmation' => 'newsecret1'])
->assertRedirect(route('login'))
->assertSessionMissing('otp_verified');
$this->assertTrue(Hash::check('newsecret1', $user->fresh()->password));
}
public function test_update_password_validates_confirmation_and_length(): void
{
$this->withSession(['otp_verified' => true, 'verified_phone' => '+989120000000', 'login_level' => 'doctor'])
->post(route('updatePassword'), ['password' => 'short', 'password_confirmation' => 'other'])
->assertSessionHasErrors('password');
}
public function test_update_password_without_verified_session_redirects_to_login(): void
{
$this->post(route('updatePassword'), ['password' => 'newsecret1', 'password_confirmation' => 'newsecret1'])
->assertRedirect(route('login'));
}
public function test_update_password_for_unknown_user_redirects_to_login_with_error(): void
{
$this->withSession(['otp_verified' => true, 'verified_phone' => '+989120000000', 'login_level' => 'doctor'])
->post(route('updatePassword'), ['password' => 'newsecret1', 'password_confirmation' => 'newsecret1'])
->assertRedirect(route('login'))
->assertSessionHas('error');
}
public function test_register_pages_render_with_session_phone(): void
{
$session = ['register_phone' => '+989120000000'];
$this->withSession($session)->get(route('register.chooseType'))->assertInertia(fn (Assert $page) => $page->component('Auth/ChooseRegisterType')->where('phone', '+989120000000'));
$this->withSession($session)->get(route('register.supervisor'))->assertInertia(fn (Assert $page) => $page->component('Auth/RegisterSupervisor'));
$this->withSession($session)->get(route('register.veterinarian'))->assertInertia(fn (Assert $page) => $page->component('Auth/RegisterVet'));
}
public function test_store_registers_user_with_session_phone_and_logs_in(): void
{
$this->withSession(['register_phone' => '+989120000000'])
->post(route('register.store'), ['phone' => '09999999999', 'national_code' => '0012345678', 'level' => UserEnum::Vet->value])
->assertRedirect(route('dashboard.index'));
$this->assertAuthenticated();
$this->assertDatabaseHas('users', ['phone' => '+989120000000', 'national_code' => '0012345678']);
$this->assertDatabaseMissing('users', ['phone' => '09999999999']);
}
public function test_store_without_session_phone_redirects_to_login(): void
{
$this->post(route('register.store'), ['phone' => '09120000000', 'national_code' => '0012345678'])
->assertRedirect(route('login'));
$this->assertGuest();
$this->assertDatabaseCount('users', 0);
}
public function test_logout_ends_session(): void
{
$this->actingAs(User::factory()->create())->get(route('dashboard.logout'))->assertRedirect(route('login'));
$this->assertGuest();
}
}
@@ -0,0 +1,112 @@
<?php
namespace Tests\Feature\Http\Client\Doctor;
use App\Enums\DocumentStatusEnum;
use App\Enums\InvoiceStatusEnum;
use App\Enums\UserEnum;
use App\Models\Document;
use App\Models\DocumentAssignment;
use App\Models\Invoice;
use App\Models\User;
use App\Models\Wallet;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Inertia\Testing\AssertableInertia as Assert;
use Tests\TestCase;
class CaseControllerTest extends TestCase
{
use RefreshDatabase;
protected function doctor(): User
{
return User::factory()->create(['level' => UserEnum::Doctor->value]);
}
protected function assign(Document $document, User $doctor): void
{
DocumentAssignment::factory()->create(['document_id' => $document->id, 'user_id' => $doctor->id]);
}
public function test_routes_require_authentication(): void
{
$document = Document::factory()->create();
$this->get(route('dashboard.cases.index'))->assertRedirect(route('login'));
$this->get(route('dashboard.cases.show', $document))->assertRedirect(route('login'));
$this->post(route('dashboard.cases.reject', $document))->assertRedirect(route('login'));
}
public function test_index_paginates_pending_cases_of_user(): void
{
$user = User::factory()->create();
Document::factory()->count(12)->registered()->create(['requester_id' => $user->id]);
Document::factory()->completed()->create(['requester_id' => $user->id]);
$this->actingAs($user)->get(route('dashboard.cases.index'))
->assertOk()
->assertInertia(fn (Assert $page) => $page->component('Dashboard/Cases/Index')->has('cases.data', 10)->where('cases.meta.total', 12));
}
public function test_show_renders_case_for_requester_without_report_permission(): void
{
$document = Document::factory()->inProgress()->create();
$this->actingAs($document->requester)->get(route('dashboard.cases.show', $document))
->assertOk()
->assertInertia(fn (Assert $page) => $page->component('Dashboard/Cases/show')->where('case.id', $document->id)->where('canReport', false));
}
public function test_show_allows_assigned_doctor_to_report(): void
{
$doctor = $this->doctor();
$document = Document::factory()->inProgress()->create();
$this->assign($document, $doctor);
$this->actingAs($doctor)->get(route('dashboard.cases.show', $document))
->assertOk()
->assertInertia(fn (Assert $page) => $page->where('canReport', true));
}
public function test_show_forbids_unrelated_user(): void
{
$document = Document::factory()->create();
$this->actingAs(User::factory()->create())->get(route('dashboard.cases.show', $document))->assertForbidden();
}
public function test_reject_by_assigned_doctor_rejects_case_and_refunds(): void
{
$doctor = $this->doctor();
$document = Document::factory()->inProgress()->create();
$this->assign($document, $doctor);
$wallet = Wallet::factory()->create(['user_id' => $document->requester_id, 'balance' => 0]);
Invoice::factory()->paid()->create(['document_id' => $document->id, 'user_id' => $document->requester_id, 'amount' => 300000]);
$this->actingAs($doctor)->post(route('dashboard.cases.reject', $document), ['reason' => 'ناخوانا'])
->assertRedirect(route('dashboard.cases.index'))
->assertSessionHas('success');
$this->assertSame(DocumentStatusEnum::Rejected, $document->fresh()->status);
$this->assertSame(300000, $wallet->fresh()->balance);
$this->assertDatabaseHas('invoices', ['document_id' => $document->id, 'status' => InvoiceStatusEnum::Canceled->value]);
}
public function test_reject_validates_reason(): void
{
$doctor = $this->doctor();
$document = Document::factory()->inProgress()->create();
$this->assign($document, $doctor);
$this->actingAs($doctor)->post(route('dashboard.cases.reject', $document), [])->assertSessionHasErrors('reason');
}
public function test_reject_by_unassigned_user_returns_validation_error(): void
{
$document = Document::factory()->inProgress()->create();
$this->actingAs($this->doctor())->post(route('dashboard.cases.reject', $document), ['reason' => 'x'])->assertSessionHasErrors('document');
$this->assertSame(DocumentStatusEnum::InProgress, $document->fresh()->status);
}
}
@@ -0,0 +1,51 @@
<?php
namespace Tests\Feature\Http\Client\Doctor;
use App\Enums\UserEnum;
use App\Models\Document;
use App\Models\DocumentAssignment;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Inertia\Testing\AssertableInertia as Assert;
use Tests\TestCase;
class DashboardControllerTest extends TestCase
{
use RefreshDatabase;
public function test_index_requires_authentication(): void
{
$this->get(route('dashboard.index'))->assertRedirect(route('login'));
}
public function test_index_renders_requester_pending_cases_and_stats(): void
{
$user = User::factory()->create(['level' => UserEnum::Vet->value]);
Document::factory()->count(7)->registered()->create(['requester_id' => $user->id]);
Document::factory()->registered()->create();
$this->actingAs($user)->get(route('dashboard.index'))
->assertOk()
->assertInertia(fn (Assert $page) => $page
->component('Dashboard/Dashboard')
->has('cases.data', 5)
->where('stats.pending_cases', 7)
->where('stats.is_doctor', false));
}
public function test_index_renders_doctor_assigned_cases(): void
{
$doctor = User::factory()->create(['level' => UserEnum::Doctor->value]);
$document = Document::factory()->inProgress()->create();
DocumentAssignment::factory()->create(['document_id' => $document->id, 'user_id' => $doctor->id]);
Document::factory()->inProgress()->create();
$this->actingAs($doctor)->get(route('dashboard.index'))
->assertOk()
->assertInertia(fn (Assert $page) => $page
->has('cases.data', 1)
->where('cases.data.0.id', $document->id)
->where('stats.is_doctor', true));
}
}
@@ -0,0 +1,38 @@
<?php
namespace Tests\Feature\Http\Client\Doctor;
use App\Models\Document;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Inertia\Testing\AssertableInertia as Assert;
use Tests\TestCase;
class DocumentControllerTest extends TestCase
{
use RefreshDatabase;
public function test_routes_require_authentication(): void
{
$this->get(route('dashboard.documents.index'))->assertRedirect(route('login'));
}
public function test_index_lists_pending_documents_of_user(): void
{
$user = User::factory()->create();
Document::factory()->count(3)->registered()->create(['requester_id' => $user->id]);
Document::factory()->registered()->create();
$this->actingAs($user)->get(route('dashboard.documents.index'))
->assertOk()
->assertInertia(fn (Assert $page) => $page->component('Dashboard/Documents/Index')->has('documents.data', 3));
}
public function test_show_redirects_to_case_page(): void
{
$document = Document::factory()->create();
$this->actingAs($document->requester)->get(route('dashboard.documents.show', $document))
->assertRedirect(route('dashboard.cases.show', $document));
}
}
@@ -0,0 +1,97 @@
<?php
namespace Tests\Feature\Http\Client\Doctor;
use App\Enums\DocumentStatusEnum;
use App\Enums\UserEnum;
use App\Models\Document;
use App\Models\DocumentAssignment;
use App\Models\Report;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Facades\Storage;
use Inertia\Testing\AssertableInertia as Assert;
use Tests\TestCase;
class ReportControllerTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
Queue::fake();
Storage::fake('public');
}
protected function doctor(): User
{
return User::factory()->create(['level' => UserEnum::Doctor->value]);
}
public function test_routes_require_authentication(): void
{
$this->get(route('dashboard.reports.index'))->assertRedirect(route('login'));
$this->post(route('dashboard.reports.store'))->assertRedirect(route('login'));
}
public function test_index_shows_reports_written_by_doctor_and_pending_cases(): void
{
$doctor = $this->doctor();
Report::factory()->count(2)->create(['doctor_id' => $doctor->id]);
Report::factory()->create();
$pending = Document::factory()->inProgress()->create();
DocumentAssignment::factory()->create(['document_id' => $pending->id, 'user_id' => $doctor->id]);
$this->actingAs($doctor)->get(route('dashboard.reports.index'))
->assertOk()
->assertInertia(fn (Assert $page) => $page->component('Dashboard/Reports/Index')->has('reports.data', 2)->has('pendingCases.data', 1));
}
public function test_index_shows_reports_of_requesters_documents_without_pending_cases(): void
{
$requester = User::factory()->create(['level' => UserEnum::Vet->value]);
$document = Document::factory()->create(['requester_id' => $requester->id]);
Report::factory()->create(['document_id' => $document->id]);
Report::factory()->create();
$this->actingAs($requester)->get(route('dashboard.reports.index'))
->assertOk()
->assertInertia(fn (Assert $page) => $page->has('reports.data', 1)->where('pendingCases', null));
}
public function test_store_submits_report_and_completes_document(): void
{
$doctor = $this->doctor();
$document = Document::factory()->inProgress()->create();
DocumentAssignment::factory()->create(['document_id' => $document->id, 'user_id' => $doctor->id]);
$this->actingAs($doctor)->post(route('dashboard.reports.store'), [
'document_id' => $document->id,
'finding' => 'یافته',
'files' => [UploadedFile::fake()->image('a.jpg')],
])->assertRedirect(route('dashboard.cases.index'))->assertSessionHas('success');
$this->assertDatabaseHas('reports', ['document_id' => $document->id, 'doctor_id' => $doctor->id, 'finding' => 'یافته']);
$this->assertSame(DocumentStatusEnum::Completed, $document->fresh()->status);
$this->assertDatabaseCount('media', 1);
}
public function test_store_validates_input(): void
{
$this->actingAs($this->doctor())->post(route('dashboard.reports.store'), [])->assertSessionHasErrors(['document_id', 'finding']);
}
public function test_store_rejects_unassigned_doctor(): void
{
$document = Document::factory()->inProgress()->create();
$this->actingAs($this->doctor())->post(route('dashboard.reports.store'), ['document_id' => $document->id, 'finding' => 'x'])
->assertSessionHasErrors('document');
$this->assertDatabaseCount('reports', 0);
}
}
@@ -0,0 +1,99 @@
<?php
namespace Tests\Feature\Http\Client\Doctor;
use App\Enums\ShiftDayEnum;
use App\Enums\UserEnum;
use App\Models\Shift;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Carbon;
use Inertia\Testing\AssertableInertia as Assert;
use Tests\TestCase;
class ShiftControllerTest extends TestCase
{
use RefreshDatabase;
protected function doctor(): User
{
return User::factory()->create(['level' => UserEnum::Doctor->value]);
}
public function test_routes_require_authentication(): void
{
$shift = Shift::factory()->create();
$this->get(route('dashboard.shifts.index'))->assertRedirect(route('login'));
$this->put(route('dashboard.shifts.update', $shift))->assertRedirect(route('login'));
$this->delete(route('dashboard.shifts.destroy', $shift))->assertRedirect(route('login'));
}
public function test_routes_are_forbidden_for_non_doctors(): void
{
$vet = User::factory()->create(['level' => UserEnum::Vet->value]);
$shift = Shift::factory()->create();
$this->actingAs($vet)->get(route('dashboard.shifts.index'))->assertForbidden();
$this->actingAs($vet)->put(route('dashboard.shifts.update', $shift))->assertForbidden();
$this->actingAs($vet)->delete(route('dashboard.shifts.destroy', $shift))->assertForbidden();
}
public function test_index_lists_my_shifts_and_open_upcoming_shifts_sorted(): void
{
Carbon::setTestNow('2026-09-21 10:00:00');
$doctor = $this->doctor();
$mine = Shift::factory()->weekly(ShiftDayEnum::Monday)->forDoctor($doctor)->create();
Shift::factory()->weekly(ShiftDayEnum::Monday)->forDoctor()->create();
$openLater = Shift::factory()->onDate('2026-09-25')->create();
$openWeekly = Shift::factory()->weekly(ShiftDayEnum::Saturday)->create();
Shift::factory()->onDate('2026-09-20')->create();
Shift::factory()->weekly(ShiftDayEnum::Sunday)->inactive()->create();
$this->actingAs($doctor)->get(route('dashboard.shifts.index'))
->assertOk()
->assertInertia(fn (Assert $page) => $page
->component('Dashboard/Shifts/Index')
->has('myShifts.data', 1)
->where('myShifts.data.0.id', $mine->id)
->has('openShifts.data', 2)
->where('openShifts.data.0.id', $openWeekly->id)
->where('openShifts.data.1.id', $openLater->id));
}
public function test_update_claims_open_shift(): void
{
$doctor = $this->doctor();
$shift = Shift::factory()->onDate('2026-09-21')->create();
$this->actingAs($doctor)->put(route('dashboard.shifts.update', $shift))->assertOk();
$this->assertDatabaseHas('shifts', ['id' => $shift->id, 'user_id' => $doctor->id]);
}
public function test_update_rejects_taken_shift(): void
{
$shift = Shift::factory()->onDate('2026-09-21')->forDoctor()->create();
$this->actingAs($this->doctor())->putJson(route('dashboard.shifts.update', $shift))->assertUnprocessable()->assertJsonValidationErrors('shift');
}
public function test_destroy_releases_own_shift(): void
{
$doctor = $this->doctor();
$shift = Shift::factory()->onDate('2026-09-21')->forDoctor($doctor)->create();
$this->actingAs($doctor)->delete(route('dashboard.shifts.destroy', $shift))->assertOk();
$this->assertDatabaseHas('shifts', ['id' => $shift->id, 'user_id' => null]);
}
public function test_destroy_rejects_another_doctors_shift(): void
{
$shift = Shift::factory()->onDate('2026-09-21')->forDoctor()->create();
$this->actingAs($this->doctor())->deleteJson(route('dashboard.shifts.destroy', $shift))->assertUnprocessable();
$this->assertNotNull($shift->fresh()->user_id);
}
}
@@ -0,0 +1,92 @@
<?php
namespace Tests\Feature\Http\Client;
use App\Enums\DocumentModalityEnum;
use App\Enums\DocumentPriorityEnum;
use App\Enums\PetTypeEnum;
use App\Enums\UserEnum;
use App\Models\Pet;
use App\Models\Shift;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Facades\Storage;
use Tests\TestCase;
class FastRequestControllerTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
Queue::fake();
Storage::fake('public');
}
public function test_routes_require_authentication(): void
{
$this->get(route('dashboard.fastRequests.create'))->assertRedirect(route('login'));
$this->post(route('dashboard.fastRequests.store'))->assertRedirect(route('login'));
}
public function test_create_returns_own_pets_and_doctors_currently_on_shift(): void
{
Carbon::setTestNow('2026-09-21 10:00:00');
$user = User::factory()->create();
Pet::factory()->count(2)->create(['owner_id' => $user->id]);
Pet::factory()->create();
$onShift = User::factory()->create(['level' => UserEnum::Doctor->value]);
Shift::factory()->onDate('2026-09-21')->between('08:00', '16:00')->forDoctor($onShift)->create();
Shift::factory()->onDate('2026-09-22')->forDoctor()->create();
$this->actingAs($user)->getJson(route('dashboard.fastRequests.create'))
->assertOk()
->assertJsonCount(2, 'pets')
->assertJsonCount(1, 'doctors')
->assertJsonPath('doctors.0.id', $onShift->id);
}
public function test_store_creates_document_with_pet_invoice_and_files(): void
{
$user = User::factory()->create();
$response = $this->actingAs($user)->postJson(route('dashboard.fastRequests.store'), [
'modality' => DocumentModalityEnum::CtScan->value,
'priority' => DocumentPriorityEnum::Medium->name,
'pet' => ['name' => 'Rex', 'type' => PetTypeEnum::Dog->name],
'scan_files' => [UploadedFile::fake()->image('scan.jpg')],
'history_files' => [UploadedFile::fake()->image('history.jpg')],
]);
$response->assertOk()
->assertJsonPath('document.modality', DocumentModalityEnum::CtScan->value)
->assertJsonPath('document.pet.name', 'Rex')
->assertJsonPath('document.invoice.amount', DocumentModalityEnum::CtScan->price());
$this->assertDatabaseCount('documents', 1);
$this->assertDatabaseCount('invoices', 1);
$this->assertDatabaseCount('media', 2);
}
public function test_store_validates_payload(): void
{
$this->actingAs(User::factory()->create())->postJson(route('dashboard.fastRequests.store'), [])
->assertUnprocessable()
->assertJsonValidationErrors(['modality', 'priority', 'pet']);
}
public function test_store_rejects_pet_of_another_user(): void
{
$pet = Pet::factory()->create();
$this->actingAs(User::factory()->create())->postJson(route('dashboard.fastRequests.store'), [
'modality' => DocumentModalityEnum::CtScan->value,
'priority' => DocumentPriorityEnum::Medium->name,
'pet_id' => $pet->id,
])->assertUnprocessable()->assertJsonValidationErrors('pet_id');
}
}
@@ -0,0 +1,69 @@
<?php
namespace Tests\Feature\Http\Client;
use App\Enums\InvoiceStatusEnum;
use App\Models\Invoice;
use App\Models\User;
use App\Models\Wallet;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Queue;
use Inertia\Testing\AssertableInertia as Assert;
use Tests\TestCase;
class InvoiceControllerTest extends TestCase
{
use RefreshDatabase;
public function test_routes_require_authentication(): void
{
$this->get(route('dashboard.invoices.index'))->assertRedirect(route('login'));
$this->post(route('dashboard.invoices.payWallet', 1))->assertRedirect(route('login'));
}
public function test_index_lists_only_own_invoices(): void
{
$user = User::factory()->create();
Invoice::factory()->count(3)->create(['user_id' => $user->id]);
Invoice::factory()->create();
$this->actingAs($user)->get(route('dashboard.invoices.index'))
->assertOk()
->assertInertia(fn (Assert $page) => $page->component('Dashboard/Invoices/Index')->has('invoices.data', 3));
}
public function test_pay_with_wallet_pays_own_pending_invoice(): void
{
Queue::fake();
$wallet = Wallet::factory()->create(['balance' => 1000000]);
$invoice = Invoice::factory()->create(['user_id' => $wallet->user_id, 'amount' => 400000]);
$this->actingAs($wallet->user)->postJson(route('dashboard.invoices.payWallet', $invoice))
->assertOk()
->assertJsonPath('invoice.id', $invoice->id);
$this->assertSame(InvoiceStatusEnum::Paid, $invoice->fresh()->status);
$this->assertSame(600000, $wallet->fresh()->balance);
}
public function test_pay_with_wallet_forbids_another_users_invoice(): void
{
$invoice = Invoice::factory()->create();
$this->actingAs(User::factory()->create())->postJson(route('dashboard.invoices.payWallet', $invoice))->assertForbidden();
$this->assertSame(InvoiceStatusEnum::Pending, $invoice->fresh()->status);
}
public function test_pay_with_wallet_returns_validation_error_on_insufficient_balance(): void
{
$wallet = Wallet::factory()->create(['balance' => 1000]);
$invoice = Invoice::factory()->create(['user_id' => $wallet->user_id, 'amount' => 400000]);
$this->actingAs($wallet->user)->postJson(route('dashboard.invoices.payWallet', $invoice))
->assertUnprocessable()
->assertJsonValidationErrors('amount');
$this->assertSame(InvoiceStatusEnum::Pending, $invoice->fresh()->status);
}
}
@@ -0,0 +1,59 @@
<?php
namespace Tests\Feature\Http\Client;
use App\Enums\MediaCollectionEnum;
use App\Models\Document;
use App\Models\Medium;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Storage;
use Tests\TestCase;
class MediaControllerTest extends TestCase
{
use RefreshDatabase;
protected function privateMediumFor(Document $document): Medium
{
Storage::fake('private');
$medium = Medium::factory()->private()->create(['path' => 'documents/scan.jpg', 'filename' => 'scan.jpg']);
Storage::disk('private')->put($medium->path, 'content');
$document->media()->attach($medium->id, ['collection' => MediaCollectionEnum::Primary->value]);
return $medium;
}
public function test_download_requires_authentication(): void
{
$this->get(route('dashboard.media.download', Medium::factory()->create()))->assertRedirect(route('login'));
}
public function test_requester_can_download_document_file(): void
{
$document = Document::factory()->create();
$medium = $this->privateMediumFor($document);
$this->actingAs($document->requester)->get(route('dashboard.media.download', $medium))
->assertOk()
->assertDownload('scan.jpg');
}
public function test_unrelated_user_is_forbidden(): void
{
$document = Document::factory()->create();
$medium = $this->privateMediumFor($document);
$this->actingAs(User::factory()->create())->get(route('dashboard.media.download', $medium))->assertForbidden();
}
public function test_missing_file_returns_not_found(): void
{
Storage::fake('private');
$document = Document::factory()->create();
$medium = Medium::factory()->private()->create(['path' => 'documents/missing.jpg']);
$document->media()->attach($medium->id, ['collection' => MediaCollectionEnum::Primary->value]);
$this->actingAs($document->requester)->get(route('dashboard.media.download', $medium))->assertNotFound();
}
}
@@ -0,0 +1,146 @@
<?php
namespace Tests\Feature\Http\Client;
use App\Enums\InvoicePaymentMethodEnum;
use App\Enums\InvoiceStatusEnum;
use App\Enums\PaymentPurposeEnum;
use App\Enums\PaymentStatusEnum;
use App\Enums\WalletTransactionReasonEnum;
use App\Models\Invoice;
use App\Models\Payment;
use App\Models\User;
use App\Models\Wallet;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Queue;
use Tests\TestCase;
class PaymentControllerTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
Queue::fake();
}
public function test_store_requires_authentication(): void
{
$this->post(route('dashboard.payments.store'), ['invoice_id' => 1])->assertRedirect(route('login'));
}
public function test_store_creates_gateway_payment_for_own_pending_invoice(): void
{
Http::fake(['gateway.zibal.ir/v1/request' => Http::response(['result' => 100, 'trackId' => 777])]);
$invoice = Invoice::factory()->create(['amount' => 500000]);
$response = $this->actingAs($invoice->user)->postJson(route('dashboard.payments.store'), ['invoice_id' => $invoice->id]);
$response->assertOk()
->assertJsonPath('payment.track_id', '777')
->assertJsonPath('url', 'https://gateway.zibal.ir/start/777');
$this->assertDatabaseHas('payments', [
'track_id' => '777',
'amount' => 500000,
'invoice_id' => $invoice->id,
'payer_id' => $invoice->user_id,
'purpose' => PaymentPurposeEnum::Invoice->value,
]);
}
public function test_store_rejects_invoice_of_another_user(): void
{
Http::fake();
$invoice = Invoice::factory()->create();
$this->actingAs(User::factory()->create())->postJson(route('dashboard.payments.store'), ['invoice_id' => $invoice->id])->assertNotFound();
Http::assertNothingSent();
}
public function test_store_rejects_non_pending_invoice(): void
{
Http::fake();
$invoice = Invoice::factory()->paid()->create();
$this->actingAs($invoice->user)->postJson(route('dashboard.payments.store'), ['invoice_id' => $invoice->id])->assertNotFound();
}
public function test_store_validates_invoice_id(): void
{
$this->actingAs(User::factory()->create())->postJson(route('dashboard.payments.store'), [])->assertUnprocessable()->assertJsonValidationErrors('invoice_id');
}
public function test_callback_with_unknown_track_id_redirects_to_dashboard(): void
{
$this->get(route('dashboard.payments.callback', ['trackId' => 'nope', 'success' => 1]))->assertRedirect(route('dashboard.index'));
}
public function test_callback_settles_successful_invoice_payment(): void
{
Http::fake(['gateway.zibal.ir/v1/verify' => Http::response(['result' => 100, 'refNumber' => 'REF'])]);
$invoice = Invoice::factory()->create(['amount' => 500000]);
$payment = Payment::factory()->create(['invoice_id' => $invoice->id, 'payer_id' => $invoice->user_id, 'amount' => 500000, 'track_id' => '555']);
$response = $this->get(route('dashboard.payments.callback', ['trackId' => '555', 'success' => 1]));
$response->assertRedirect(route('dashboard.index'));
$response->assertSessionHas('payment_result', ['status' => 'paid', 'track_id' => '555', 'purpose' => 'Invoice']);
$this->assertSame(PaymentStatusEnum::Paid, $payment->fresh()->status);
$invoice->refresh();
$this->assertSame(InvoiceStatusEnum::Paid, $invoice->status);
$this->assertSame(InvoicePaymentMethodEnum::Gateway, $invoice->payment_method);
$this->assertDatabaseHas('wallet_transactions', ['invoice_id' => $invoice->id, 'payment_id' => $payment->id, 'reason' => WalletTransactionReasonEnum::InvoicePayment->value]);
}
public function test_callback_settles_successful_wallet_top_up(): void
{
Http::fake(['gateway.zibal.ir/v1/verify' => Http::response(['result' => 100])]);
$wallet = Wallet::factory()->create(['balance' => 1000]);
Payment::factory()->walletTopUp()->create(['payer_id' => $wallet->user_id, 'amount' => 250000, 'track_id' => '556']);
$this->get(route('dashboard.payments.callback', ['trackId' => '556', 'success' => 1]))->assertRedirect(route('dashboard.index'));
$this->assertSame(251000, $wallet->fresh()->balance);
$this->assertDatabaseHas('wallet_transactions', ['wallet_id' => $wallet->id, 'reason' => WalletTransactionReasonEnum::TopUp->value, 'amount' => 250000]);
}
public function test_callback_marks_payment_failed_when_gateway_reports_failure(): void
{
Http::fake();
$invoice = Invoice::factory()->create();
$payment = Payment::factory()->create(['invoice_id' => $invoice->id, 'payer_id' => $invoice->user_id, 'track_id' => '557']);
$response = $this->get(route('dashboard.payments.callback', ['trackId' => '557', 'success' => 0]));
$response->assertSessionHas('payment_result.status', 'failed');
$this->assertSame(PaymentStatusEnum::Failed, $payment->fresh()->status);
$this->assertSame(InvoiceStatusEnum::Pending, $invoice->fresh()->status);
Http::assertNothingSent();
}
public function test_callback_marks_payment_failed_when_verification_fails(): void
{
Http::fake(['gateway.zibal.ir/v1/verify' => Http::response(['result' => 202])]);
$payment = Payment::factory()->create(['track_id' => '558']);
$this->get(route('dashboard.payments.callback', ['trackId' => '558', 'success' => 1]));
$this->assertSame(PaymentStatusEnum::Failed, $payment->fresh()->status);
$this->assertDatabaseCount('wallet_transactions', 0);
}
public function test_callback_does_not_settle_twice_for_already_paid_payment(): void
{
Http::fake();
$wallet = Wallet::factory()->create(['balance' => 1000]);
Payment::factory()->paid()->walletTopUp()->create(['payer_id' => $wallet->user_id, 'amount' => 250000, 'track_id' => '559']);
$this->get(route('dashboard.payments.callback', ['trackId' => '559', 'success' => 1]))->assertSessionHas('payment_result.status', 'paid');
$this->assertSame(1000, $wallet->fresh()->balance);
$this->assertDatabaseCount('wallet_transactions', 0);
}
}
@@ -0,0 +1,80 @@
<?php
namespace Tests\Feature\Http\Client;
use App\Models\Medium;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Tests\TestCase;
class ProfileControllerTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
Storage::fake('public');
}
public function test_update_requires_authentication(): void
{
$this->post(route('dashboard.profile.update'))->assertRedirect(route('login'));
}
public function test_update_saves_profile_fields(): void
{
$user = User::factory()->create();
$this->actingAs($user)->from('/dashboard')->post(route('dashboard.profile.update'), [
'fullname' => 'نام جدید',
'national_code' => '0012345678',
'email' => 'new@example.com',
'city' => 'تهران',
])->assertRedirect('/dashboard')->assertSessionHas('success');
$this->assertDatabaseHas('users', ['id' => $user->id, 'fullname' => 'نام جدید', 'national_code' => '0012345678', 'email' => 'new@example.com', 'city' => 'تهران']);
}
public function test_update_stores_avatar_and_sets_profile_url(): void
{
$user = User::factory()->create(['profile_url' => null]);
$this->actingAs($user)->post(route('dashboard.profile.update'), [
'fullname' => 'n',
'national_code' => '1',
'avatar' => UploadedFile::fake()->image('me.png'),
]);
$medium = Medium::first();
$this->assertNotNull($medium);
$this->assertStringStartsWith('profile/', $medium->path);
$this->assertSame($medium->url, $user->fresh()->profile_url);
Storage::disk('public')->assertExists($medium->path);
}
public function test_update_replaces_previous_avatar(): void
{
$old = Medium::factory()->public()->create(['path' => 'profile/old.png', 'url' => 'http://example.test/storage/profile/old.png']);
Storage::disk('public')->put($old->path, 'old');
$user = User::factory()->create(['profile_url' => $old->url]);
$this->actingAs($user)->post(route('dashboard.profile.update'), [
'fullname' => 'n',
'national_code' => '1',
'avatar' => UploadedFile::fake()->image('new.png'),
]);
$this->assertDatabaseMissing('media', ['id' => $old->id]);
Storage::disk('public')->assertMissing('profile/old.png');
$this->assertNotSame($old->url, $user->fresh()->profile_url);
}
public function test_update_validates_required_fields(): void
{
$this->actingAs(User::factory()->create())->post(route('dashboard.profile.update'), [])->assertSessionHasErrors(['fullname', 'national_code']);
}
}
@@ -0,0 +1,143 @@
<?php
namespace Tests\Feature\Http\Client;
use App\Enums\PaymentPurposeEnum;
use App\Enums\WithdrawRequestStatusEnum;
use App\Models\BankCard;
use App\Models\User;
use App\Models\Wallet;
use App\Models\WalletTransaction;
use App\Models\WithdrawRequest;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Http;
use Inertia\Testing\AssertableInertia as Assert;
use Tests\TestCase;
class WalletControllerTest extends TestCase
{
use RefreshDatabase;
public function test_routes_require_authentication(): void
{
$this->get(route('dashboard.wallet.index'))->assertRedirect(route('login'));
$this->post(route('dashboard.wallet.deposit'))->assertRedirect(route('login'));
$this->post(route('dashboard.wallet.withdrawRequests.store'))->assertRedirect(route('login'));
$this->post(route('dashboard.wallet.bankCards.store'))->assertRedirect(route('login'));
}
public function test_index_renders_own_transactions_withdraw_requests_and_bank_cards(): void
{
$wallet = Wallet::factory()->create(['balance' => 5000]);
WalletTransaction::factory()->count(2)->create(['wallet_id' => $wallet->id]);
WalletTransaction::factory()->create();
$bankCard = BankCard::factory()->create(['user_id' => $wallet->user_id]);
WithdrawRequest::factory()->create(['user_id' => $wallet->user_id, 'bank_card_id' => $bankCard->id]);
WithdrawRequest::factory()->create();
$this->actingAs($wallet->user)->get(route('dashboard.wallet.index'))
->assertOk()
->assertInertia(fn (Assert $page) => $page
->component('Dashboard/Wallet/Index')
->has('transactions.data', 2)
->has('withdrawRequests', 1)
->has('bankCards', 1));
}
public function test_index_creates_wallet_for_user_without_one(): void
{
$user = User::factory()->create();
$this->actingAs($user)->get(route('dashboard.wallet.index'))->assertOk();
$this->assertDatabaseHas('wallets', ['user_id' => $user->id, 'balance' => 0]);
}
public function test_deposit_creates_top_up_payment_and_returns_gateway_url(): void
{
Http::fake(['gateway.zibal.ir/v1/request' => Http::response(['result' => 100, 'trackId' => 900])]);
$user = User::factory()->create();
$this->actingAs($user)->postJson(route('dashboard.wallet.deposit'), ['amount' => 100000])
->assertOk()
->assertJsonPath('url', 'https://gateway.zibal.ir/start/900');
$this->assertDatabaseHas('payments', ['track_id' => '900', 'amount' => 100000, 'payer_id' => $user->id, 'purpose' => PaymentPurposeEnum::WalletTopUp->value, 'invoice_id' => null]);
}
public function test_deposit_validates_amount(): void
{
$this->actingAs(User::factory()->create())->postJson(route('dashboard.wallet.deposit'), ['amount' => 100])->assertUnprocessable()->assertJsonValidationErrors('amount');
}
public function test_store_withdraw_request_creates_pending_request(): void
{
$wallet = Wallet::factory()->create(['balance' => 500000]);
$bankCard = BankCard::factory()->create(['user_id' => $wallet->user_id]);
$this->actingAs($wallet->user)
->post(route('dashboard.wallet.withdrawRequests.store'), ['amount' => 200000, 'bank_card_id' => $bankCard->id])
->assertRedirect(route('dashboard.wallet.index'))
->assertSessionHas('success');
$this->assertDatabaseHas('withdraw_requests', ['user_id' => $wallet->user_id, 'amount' => 200000, 'status' => WithdrawRequestStatusEnum::Pending->value]);
}
public function test_store_withdraw_request_rejects_insufficient_balance(): void
{
$wallet = Wallet::factory()->create(['balance' => 100000]);
$bankCard = BankCard::factory()->create(['user_id' => $wallet->user_id]);
$this->actingAs($wallet->user)
->post(route('dashboard.wallet.withdrawRequests.store'), ['amount' => 200000, 'bank_card_id' => $bankCard->id])
->assertSessionHasErrors('amount');
$this->assertDatabaseCount('withdraw_requests', 0);
}
public function test_store_withdraw_request_rejects_another_users_bank_card(): void
{
$wallet = Wallet::factory()->create(['balance' => 500000]);
$bankCard = BankCard::factory()->create();
$this->actingAs($wallet->user)
->post(route('dashboard.wallet.withdrawRequests.store'), ['amount' => 100000, 'bank_card_id' => $bankCard->id])
->assertSessionHasErrors('bank_card_id');
}
public function test_store_bank_card_creates_card_for_user(): void
{
$user = User::factory()->create();
$this->actingAs($user)
->post(route('dashboard.wallet.bankCards.store'), ['card_number' => '6037991234567890', 'holder_name' => 'علی'])
->assertRedirect(route('dashboard.wallet.index'));
$this->assertDatabaseHas('bank_cards', ['user_id' => $user->id, 'card_number' => '6037991234567890', 'holder_name' => 'علی']);
}
public function test_store_bank_card_validates_input(): void
{
$this->actingAs(User::factory()->create())
->post(route('dashboard.wallet.bankCards.store'), ['card_number' => '123'])
->assertSessionHasErrors(['card_number', 'holder_name']);
}
public function test_destroy_bank_card_deletes_own_card(): void
{
$bankCard = BankCard::factory()->create();
$this->actingAs($bankCard->user)->delete(route('dashboard.wallet.bankCards.destroy', $bankCard))->assertRedirect(route('dashboard.wallet.index'));
$this->assertDatabaseMissing('bank_cards', ['id' => $bankCard->id]);
}
public function test_destroy_bank_card_forbids_another_users_card(): void
{
$bankCard = BankCard::factory()->create();
$this->actingAs(User::factory()->create())->delete(route('dashboard.wallet.bankCards.destroy', $bankCard))->assertForbidden();
$this->assertDatabaseHas('bank_cards', ['id' => $bankCard->id]);
}
}
+7 -1
View File
@@ -6,5 +6,11 @@ use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
abstract class TestCase extends BaseTestCase
{
//
protected function setUp(): void
{
parent::setUp();
$this->withoutVite();
config(['inertia.testing.page_paths' => [resource_path('js/Client/Pages'), resource_path('js/Admin/Pages')]]);
}
}