diff --git a/app/Http/Requests/Admin/PetRequest.php b/app/Http/Requests/Admin/PetRequest.php index 50e071f..4af03e4 100644 --- a/app/Http/Requests/Admin/PetRequest.php +++ b/app/Http/Requests/Admin/PetRequest.php @@ -38,8 +38,10 @@ class PetRequest extends FormRequest public function prepareForValidation() { - $this->merge([ - 'weight' => PersianToEnglish::toEnglish($this->weight), - ]); + if ($this->filled('weight')) { + $this->merge([ + 'weight' => PersianToEnglish::toEnglish($this->weight), + ]); + } } } diff --git a/app/Http/Requests/Admin/UserRequest.php b/app/Http/Requests/Admin/UserRequest.php index ed12466..53a9abd 100644 --- a/app/Http/Requests/Admin/UserRequest.php +++ b/app/Http/Requests/Admin/UserRequest.php @@ -33,7 +33,7 @@ class UserRequest extends FormRequest 'phone' => ['required', 'max:255', new PhoneNumber], 'fullname' => 'nullable | max:255', 'role' => ['nullable', 'array', 'exists:roles,id'], - 'profile_url' => ['nullable', 'exists:medium,url'], + 'profile_url' => ['nullable', 'exists:media,url'], 'email' => ['nullable', 'email', 'max:255', isset($toBeEditedUser) ? Rule::unique('users')->ignore($toBeEditedUser->id) diff --git a/docs/TESTING.md b/docs/TESTING.md index 2b0ba4c..48b809c 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -65,8 +65,9 @@ app/Models/Shift.php → tests/Feature/Models/ShiftTes **Service** — happy path; every guard / exception branch; every side effect (DB rows, dispatched jobs, transactions written); for money code, balance invariants before and after. -**Form Request** — `authorize()` when it is not `return true`; each rule with an invalid input → -`assertSessionHasErrors('field')` / 422; one fully valid payload passes. +**Form Request** — use the `Tests\Concerns\ValidatesFormRequests` trait (`assertPasses`, `assertFailsOn`, +`formRequestErrors`); it runs `prepareForValidation`, `rules`, `withValidator` and route/user resolvers without HTTP. +Cover `authorize()` when it is not `return true`; each rule with an invalid input; one fully valid payload. **HTTP** — guest is redirected to login; wrong role or non-owner gets 403/404; happy path returns the expected status and persisted state; Inertia page tests assert the component name and key props. @@ -120,23 +121,23 @@ Legend: `☐` not written · `☑` written · `—` intentionally not tested | Priority | Source | Test | Status | |----------|--------|------|--------| -| P1 | `app/Http/Requests/Client/Auth/UserRegistrationRequest.php` | `tests/Feature/Requests/Client/Auth/UserRegistrationRequestTest.php` | ☐ | -| P1 | `app/Http/Requests/Client/Auth/VerifyOtpRequest.php` | `tests/Feature/Requests/Client/Auth/VerifyOtpRequestTest.php` | ☐ | -| P1 | `app/Http/Requests/Client/PaymentRequest.php` | `tests/Feature/Requests/Client/PaymentRequestTest.php` | ☐ | -| P1 | `app/Http/Requests/Client/WalletDepositRequest.php` | `tests/Feature/Requests/Client/WalletDepositRequestTest.php` | ☐ | -| P1 | `app/Http/Requests/Client/WithdrawRequestStoreRequest.php` | `tests/Feature/Requests/Client/WithdrawRequestStoreRequestTest.php` | ☐ | -| P1 | `app/Http/Requests/Client/BankCardStoreRequest.php` | `tests/Feature/Requests/Client/BankCardStoreRequestTest.php` | ☐ | -| P1 | `app/Http/Requests/Client/FastRequestRequest.php` | `tests/Feature/Requests/Client/FastRequestRequestTest.php` | ☐ | -| P2 | `app/Http/Requests/Client/CaseRejectRequest.php` | `tests/Feature/Requests/Client/CaseRejectRequestTest.php` | ☐ | -| P2 | `app/Http/Requests/Client/ReportSubmitRequest.php` | `tests/Feature/Requests/Client/ReportSubmitRequestTest.php` | ☐ | -| P2 | `app/Http/Requests/Client/ProfileUpdateRequest.php` | `tests/Feature/Requests/Client/ProfileUpdateRequestTest.php` | ☐ | -| P2 | `app/Http/Requests/Admin/WithdrawRejectRequest.php` | `tests/Feature/Requests/Admin/WithdrawRejectRequestTest.php` | ☐ | -| P2 | `app/Http/Requests/Admin/DocumentRequest.php` | `tests/Feature/Requests/Admin/DocumentRequestTest.php` | ☐ | -| P2 | `app/Http/Requests/Admin/ShiftRequest.php` | `tests/Feature/Requests/Admin/ShiftRequestTest.php` | ☐ | -| P2 | `app/Http/Requests/Admin/UserRequest.php` | `tests/Feature/Requests/Admin/UserRequestTest.php` | ☐ | -| P2 | `app/Http/Requests/Admin/PetRequest.php` | `tests/Feature/Requests/Admin/PetRequestTest.php` | ☐ | -| P2 | `app/Http/Requests/Admin/ReportRequest.php` | `tests/Feature/Requests/Admin/ReportRequestTest.php` | ☐ | -| P2 | `app/Http/Requests/Admin/SettingProfileRequest.php` | `tests/Feature/Requests/Admin/SettingProfileRequestTest.php` | ☐ | +| P1 | `app/Http/Requests/Client/Auth/UserRegistrationRequest.php` | `tests/Feature/Requests/Client/Auth/UserRegistrationRequestTest.php` | ☑ | +| P1 | `app/Http/Requests/Client/Auth/VerifyOtpRequest.php` | `tests/Feature/Requests/Client/Auth/VerifyOtpRequestTest.php` | ☑ | +| P1 | `app/Http/Requests/Client/PaymentRequest.php` | `tests/Feature/Requests/Client/PaymentRequestTest.php` | ☑ | +| P1 | `app/Http/Requests/Client/WalletDepositRequest.php` | `tests/Feature/Requests/Client/WalletDepositRequestTest.php` | ☑ | +| P1 | `app/Http/Requests/Client/WithdrawRequestStoreRequest.php` | `tests/Feature/Requests/Client/WithdrawRequestStoreRequestTest.php` | ☑ | +| P1 | `app/Http/Requests/Client/BankCardStoreRequest.php` | `tests/Feature/Requests/Client/BankCardStoreRequestTest.php` | ☑ | +| P1 | `app/Http/Requests/Client/FastRequestRequest.php` | `tests/Feature/Requests/Client/FastRequestRequestTest.php` | ☑ | +| P2 | `app/Http/Requests/Client/CaseRejectRequest.php` | `tests/Feature/Requests/Client/CaseRejectRequestTest.php` | ☑ | +| P2 | `app/Http/Requests/Client/ReportSubmitRequest.php` | `tests/Feature/Requests/Client/ReportSubmitRequestTest.php` | ☑ | +| P2 | `app/Http/Requests/Client/ProfileUpdateRequest.php` | `tests/Feature/Requests/Client/ProfileUpdateRequestTest.php` | ☑ | +| P2 | `app/Http/Requests/Admin/WithdrawRejectRequest.php` | `tests/Feature/Requests/Admin/WithdrawRejectRequestTest.php` | ☑ | +| P2 | `app/Http/Requests/Admin/DocumentRequest.php` | `tests/Feature/Requests/Admin/DocumentRequestTest.php` | ☑ | +| P2 | `app/Http/Requests/Admin/ShiftRequest.php` | `tests/Feature/Requests/Admin/ShiftRequestTest.php` | ☑ | +| P2 | `app/Http/Requests/Admin/UserRequest.php` | `tests/Feature/Requests/Admin/UserRequestTest.php` | ☑ | +| P2 | `app/Http/Requests/Admin/PetRequest.php` | `tests/Feature/Requests/Admin/PetRequestTest.php` | ☑ | +| P2 | `app/Http/Requests/Admin/ReportRequest.php` | `tests/Feature/Requests/Admin/ReportRequestTest.php` | ☑ | +| P2 | `app/Http/Requests/Admin/SettingProfileRequest.php` | `tests/Feature/Requests/Admin/SettingProfileRequestTest.php` | ☑ (known bug: validates `users.name`, `first_name`, `last_name` which do not exist; passes on sqlite only because of literal quoting) | ### HTTP diff --git a/tests/Concerns/ValidatesFormRequests.php b/tests/Concerns/ValidatesFormRequests.php new file mode 100644 index 0000000..267b5ce --- /dev/null +++ b/tests/Concerns/ValidatesFormRequests.php @@ -0,0 +1,40 @@ +setContainer(app())->setRedirector(app(Redirector::class)); + $request->setUserResolver(fn () => $user); + + $route = new Route('POST', '/', []); + $route->parameters = $routeParameters; + $request->setRouteResolver(fn () => $route); + + try { + $request->validateResolved(); + } catch (ValidationException $e) { + return $e->errors(); + } + + return []; + } + + protected function assertPasses(string $requestClass, array $data, array $files = [], ?User $user = null, array $routeParameters = []): void + { + $this->assertSame([], $this->formRequestErrors($requestClass, $data, $files, $user, $routeParameters)); + } + + protected function assertFailsOn(string $field, string $requestClass, array $data, array $files = [], ?User $user = null, array $routeParameters = []): void + { + $this->assertArrayHasKey($field, $this->formRequestErrors($requestClass, $data, $files, $user, $routeParameters)); + } +} diff --git a/tests/Feature/Requests/Admin/DocumentRequestTest.php b/tests/Feature/Requests/Admin/DocumentRequestTest.php new file mode 100644 index 0000000..c399021 --- /dev/null +++ b/tests/Feature/Requests/Admin/DocumentRequestTest.php @@ -0,0 +1,59 @@ +assertPasses(DocumentRequest::class, ['pet_id' => Pet::factory()->create()->id]); + } + + public function test_passes_with_all_optional_fields(): void + { + $this->assertPasses(DocumentRequest::class, [ + 'pet_id' => Pet::factory()->create()->id, + 'modality' => 'mri', + 'status' => DocumentStatusEnum::Registered->name, + 'priority' => DocumentPriorityEnum::High->name, + 'note' => 'n', + 'submitted_at' => '2026-09-21', + 'scan_date' => '2026-09-21 10:00:00', + 'processing_deadline' => '2026-09-22', + 'primary_files' => [], + 'history_files' => [], + ]); + } + + public function test_fails_without_pet_or_with_unknown_pet(): void + { + $this->assertFailsOn('pet_id', DocumentRequest::class, []); + $this->assertFailsOn('pet_id', DocumentRequest::class, ['pet_id' => 999]); + } + + public function test_fails_when_status_or_priority_is_a_value_instead_of_key(): void + { + $petId = Pet::factory()->create()->id; + + $this->assertFailsOn('status', DocumentRequest::class, ['pet_id' => $petId, 'status' => DocumentStatusEnum::Registered->value]); + $this->assertFailsOn('priority', DocumentRequest::class, ['pet_id' => $petId, 'priority' => DocumentPriorityEnum::High->value]); + } + + public function test_fails_with_invalid_dates_or_non_array_files(): void + { + $petId = Pet::factory()->create()->id; + + $this->assertFailsOn('submitted_at', DocumentRequest::class, ['pet_id' => $petId, 'submitted_at' => 'x']); + $this->assertFailsOn('primary_files', DocumentRequest::class, ['pet_id' => $petId, 'primary_files' => 'x']); + } +} diff --git a/tests/Feature/Requests/Admin/PetRequestTest.php b/tests/Feature/Requests/Admin/PetRequestTest.php new file mode 100644 index 0000000..7304274 --- /dev/null +++ b/tests/Feature/Requests/Admin/PetRequestTest.php @@ -0,0 +1,68 @@ + 'Rex', + 'type' => PetTypeEnum::Dog->value, + 'gender' => PetGenderEnum::Male->value, + 'breed' => 'ژرمن', + 'weight' => '20', + 'sterilized' => true, + 'birthday' => '2020-01-01', + ], $overrides); + } + + public function test_passes_with_valid_payload(): void + { + $this->assertPasses(PetRequest::class, $this->valid()); + } + + public function test_converts_persian_digits_in_weight(): void + { + $this->assertPasses(PetRequest::class, $this->valid(['weight' => '۲۰'])); + } + + public function test_fails_without_required_fields(): void + { + $errors = $this->formRequestErrors(PetRequest::class, ['weight' => '1']); + + foreach (['name', 'type', 'gender', 'breed', 'sterilized', 'birthday'] as $field) { + $this->assertArrayHasKey($field, $errors); + } + } + + public function test_fails_without_weight(): void + { + $this->assertFailsOn('weight', PetRequest::class, $this->valid(['weight' => null])); + } + + public function test_fails_when_type_or_gender_is_a_key_instead_of_value(): void + { + $this->assertFailsOn('type', PetRequest::class, $this->valid(['type' => PetTypeEnum::Dog->name])); + $this->assertFailsOn('gender', PetRequest::class, $this->valid(['gender' => PetGenderEnum::Male->name])); + } + + public function test_fails_with_non_integer_weight_or_invalid_birthday(): void + { + $this->assertFailsOn('weight', PetRequest::class, $this->valid(['weight' => 'heavy'])); + $this->assertFailsOn('birthday', PetRequest::class, $this->valid(['birthday' => 'x'])); + } + + public function test_fails_with_non_boolean_sterilized(): void + { + $this->assertFailsOn('sterilized', PetRequest::class, $this->valid(['sterilized' => 'yes'])); + } +} diff --git a/tests/Feature/Requests/Admin/ReportRequestTest.php b/tests/Feature/Requests/Admin/ReportRequestTest.php new file mode 100644 index 0000000..4930794 --- /dev/null +++ b/tests/Feature/Requests/Admin/ReportRequestTest.php @@ -0,0 +1,64 @@ + 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_passes_with_valid_payload(): void + { + $this->assertPasses(ReportRequest::class, $this->valid()); + } + + public function test_passes_with_existing_medium(): void + { + $this->assertPasses(ReportRequest::class, $this->valid(['medium_id' => Medium::factory()->create()->id])); + } + + public function test_fails_without_required_fields(): void + { + $errors = $this->formRequestErrors(ReportRequest::class, []); + + foreach (['document_id', 'doctor_id', 'status', 'body', 'submitted_at'] as $field) { + $this->assertArrayHasKey($field, $errors); + } + } + + public function test_fails_with_unknown_document_doctor_or_medium(): void + { + $this->assertFailsOn('document_id', ReportRequest::class, $this->valid(['document_id' => 999])); + $this->assertFailsOn('doctor_id', ReportRequest::class, $this->valid(['doctor_id' => 999])); + $this->assertFailsOn('medium_id', ReportRequest::class, $this->valid(['medium_id' => 999])); + } + + public function test_fails_when_status_is_a_value_instead_of_key(): void + { + $this->assertFailsOn('status', ReportRequest::class, $this->valid(['status' => ReportStatusEnum::Approved->value])); + } + + public function test_fails_with_invalid_submitted_at(): void + { + $this->assertFailsOn('submitted_at', ReportRequest::class, $this->valid(['submitted_at' => 'x'])); + } +} diff --git a/tests/Feature/Requests/Admin/SettingProfileRequestTest.php b/tests/Feature/Requests/Admin/SettingProfileRequestTest.php new file mode 100644 index 0000000..cff4420 --- /dev/null +++ b/tests/Feature/Requests/Admin/SettingProfileRequestTest.php @@ -0,0 +1,34 @@ +create(); + + $this->assertPasses(SettingProfileRequest::class, ['name' => 'new-name'], [], null, ['user' => $user]); + } + + public function test_fails_without_name(): void + { + $this->assertFailsOn('name', SettingProfileRequest::class, [], [], null, ['user' => User::factory()->create()]); + } + + public function test_fails_when_fields_exceed_limit(): void + { + $user = User::factory()->create(); + + $this->assertFailsOn('name', SettingProfileRequest::class, ['name' => str_repeat('a', 256)], [], null, ['user' => $user]); + $this->assertFailsOn('first_name', SettingProfileRequest::class, ['name' => 'n', 'first_name' => str_repeat('a', 256)], [], null, ['user' => $user]); + } +} diff --git a/tests/Feature/Requests/Admin/ShiftRequestTest.php b/tests/Feature/Requests/Admin/ShiftRequestTest.php new file mode 100644 index 0000000..be7ddaa --- /dev/null +++ b/tests/Feature/Requests/Admin/ShiftRequestTest.php @@ -0,0 +1,96 @@ + ShiftDayEnum::Monday->value, + 'start_time' => '08:00', + 'end_time' => '16:00', + 'is_active' => true, + ], $overrides); + } + + public function test_passes_with_weekly_shift(): void + { + $this->assertPasses(ShiftRequest::class, $this->valid()); + } + + public function test_passes_with_dated_shift_and_doctor(): void + { + $doctor = User::factory()->create(['level' => UserEnum::Doctor->value]); + + $this->assertPasses(ShiftRequest::class, $this->valid(['day_of_week' => null, 'date' => '2026-09-21', 'user_id' => $doctor->id])); + } + + public function test_passes_with_overnight_shift(): void + { + $this->assertPasses(ShiftRequest::class, $this->valid(['start_time' => '22:00', 'end_time' => '02:00'])); + } + + public function test_fails_without_day_of_week_or_date(): void + { + $errors = $this->formRequestErrors(ShiftRequest::class, $this->valid(['day_of_week' => null])); + + $this->assertArrayHasKey('day_of_week', $errors); + $this->assertArrayHasKey('date', $errors); + } + + public function test_fails_without_times_or_is_active(): void + { + $errors = $this->formRequestErrors(ShiftRequest::class, ['day_of_week' => ShiftDayEnum::Monday->value]); + + $this->assertArrayHasKey('start_time', $errors); + $this->assertArrayHasKey('end_time', $errors); + $this->assertArrayHasKey('is_active', $errors); + } + + public function test_fails_with_invalid_day_time_format_or_equal_times(): void + { + $this->assertFailsOn('day_of_week', ShiftRequest::class, $this->valid(['day_of_week' => 'Monday'])); + $this->assertFailsOn('start_time', ShiftRequest::class, $this->valid(['start_time' => '8am'])); + $this->assertFailsOn('end_time', ShiftRequest::class, $this->valid(['end_time' => '08:00'])); + } + + public function test_fails_when_user_is_not_a_doctor(): void + { + $vet = User::factory()->create(['level' => UserEnum::Vet->value]); + + $this->assertFailsOn('user_id', ShiftRequest::class, $this->valid(['user_id' => $vet->id])); + } + + public function test_fails_when_shift_conflicts_with_doctors_existing_shift(): void + { + $doctor = User::factory()->create(['level' => UserEnum::Doctor->value]); + Shift::factory()->weekly(ShiftDayEnum::Monday)->between('08:00', '12:00')->forDoctor($doctor)->create(); + + $this->assertFailsOn('start_time', ShiftRequest::class, $this->valid(['user_id' => $doctor->id, 'start_time' => '10:00', 'end_time' => '14:00'])); + } + + public function test_conflict_check_ignores_shift_being_edited(): void + { + $doctor = User::factory()->create(['level' => UserEnum::Doctor->value]); + $shift = Shift::factory()->weekly(ShiftDayEnum::Monday)->between('08:00', '12:00')->forDoctor($doctor)->create(); + + $this->assertPasses(ShiftRequest::class, $this->valid(['user_id' => $doctor->id, 'start_time' => '09:00', 'end_time' => '13:00']), [], null, ['shift' => $shift]); + } + + public function test_date_takes_precedence_and_clears_day_of_week(): void + { + $this->assertPasses(ShiftRequest::class, $this->valid(['day_of_week' => 'invalid-day', 'date' => '2026-09-21'])); + } +} diff --git a/tests/Feature/Requests/Admin/UserRequestTest.php b/tests/Feature/Requests/Admin/UserRequestTest.php new file mode 100644 index 0000000..c064f16 --- /dev/null +++ b/tests/Feature/Requests/Admin/UserRequestTest.php @@ -0,0 +1,106 @@ + '09120000000', + 'password' => 'secret123', + 'password_confirmation' => 'secret123', + 'level' => UserEnum::Doctor->value, + ], $overrides); + } + + public function test_passes_with_required_fields_on_create(): void + { + $this->assertPasses(UserRequest::class, $this->valid()); + } + + public function test_passes_with_optional_fields_and_roles(): void + { + $role = Role::create(['name' => 'editor']); + + $this->assertPasses(UserRequest::class, $this->valid([ + 'username' => 'ali', + 'fullname' => 'علی', + 'email' => 'ali@example.com', + 'role' => [$role->id], + ])); + } + + public function test_fails_without_phone_password_or_level(): void + { + $errors = $this->formRequestErrors(UserRequest::class, []); + + $this->assertArrayHasKey('phone', $errors); + $this->assertArrayHasKey('password', $errors); + $this->assertArrayHasKey('level', $errors); + } + + public function test_fails_with_short_or_unconfirmed_password(): void + { + $this->assertFailsOn('password', UserRequest::class, $this->valid(['password' => 'short', 'password_confirmation' => 'short'])); + $this->assertFailsOn('password', UserRequest::class, $this->valid(['password_confirmation' => 'different'])); + } + + public function test_fails_with_level_outside_admin_list(): void + { + $this->assertFailsOn('level', UserRequest::class, $this->valid(['level' => UserEnum::Vet->value])); + } + + public function test_fails_with_duplicate_username_or_email(): void + { + User::factory()->create(['username' => 'taken', 'email' => 'taken@example.com']); + + $this->assertFailsOn('username', UserRequest::class, $this->valid(['username' => 'taken'])); + $this->assertFailsOn('email', UserRequest::class, $this->valid(['email' => 'taken@example.com'])); + } + + public function test_profile_url_must_match_existing_medium(): void + { + $medium = Medium::factory()->create(['url' => 'http://example.test/a.png']); + + $this->assertPasses(UserRequest::class, $this->valid(['profile_url' => $medium->url])); + $this->assertFailsOn('profile_url', UserRequest::class, $this->valid(['profile_url' => 'http://example.test/missing.png'])); + } + + public function test_fails_with_invalid_phone_or_unknown_role(): void + { + $this->assertFailsOn('phone', UserRequest::class, $this->valid(['phone' => 'abc'])); + $this->assertFailsOn('role', UserRequest::class, $this->valid(['role' => [999]])); + } + + public function test_update_allows_own_username_and_email_and_optional_password(): void + { + $user = User::factory()->create(['username' => 'me', 'email' => 'me@example.com']); + + $this->assertPasses(UserRequest::class, [ + 'username' => 'me', + 'email' => 'me@example.com', + 'phone' => '09120000000', + 'level' => UserEnum::Doctor->value, + ], [], null, ['user' => $user]); + } + + public function test_update_still_rejects_another_users_username(): void + { + $user = User::factory()->create(); + User::factory()->create(['username' => 'other']); + + $this->assertFailsOn('username', UserRequest::class, $this->valid(['username' => 'other']), [], null, ['user' => $user]); + } +} diff --git a/tests/Feature/Requests/Admin/WithdrawRejectRequestTest.php b/tests/Feature/Requests/Admin/WithdrawRejectRequestTest.php new file mode 100644 index 0000000..e549a0b --- /dev/null +++ b/tests/Feature/Requests/Admin/WithdrawRejectRequestTest.php @@ -0,0 +1,28 @@ +assertPasses(WithdrawRejectRequest::class, []); + $this->assertPasses(WithdrawRejectRequest::class, ['note' => null]); + } + + public function test_passes_with_note(): void + { + $this->assertPasses(WithdrawRejectRequest::class, ['note' => 'اطلاعات کارت ناقص است']); + } + + public function test_fails_when_note_exceeds_limit(): void + { + $this->assertFailsOn('note', WithdrawRejectRequest::class, ['note' => str_repeat('a', 1001)]); + } +} diff --git a/tests/Feature/Requests/Client/Auth/UserRegistrationRequestTest.php b/tests/Feature/Requests/Client/Auth/UserRegistrationRequestTest.php new file mode 100644 index 0000000..76e9799 --- /dev/null +++ b/tests/Feature/Requests/Client/Auth/UserRegistrationRequestTest.php @@ -0,0 +1,55 @@ + '09120000000', 'national_code' => '0012345678'], $overrides); + } + + public function test_passes_with_phone_and_national_code(): void + { + $this->assertPasses(UserRegistrationRequest::class, $this->valid()); + } + + public function test_passes_with_all_optional_fields(): void + { + $this->assertPasses(UserRegistrationRequest::class, $this->valid([ + 'fullname' => 'f', 'specialty' => 's', 'clinic_name' => 'c', 'clinic_phone' => '021', 'province' => 'p', 'city' => 'c', 'address' => 'a', 'level' => 'دانپزشک', + ])); + } + + public function test_accepts_international_and_persian_digit_phones(): void + { + $this->assertPasses(UserRegistrationRequest::class, $this->valid(['phone' => '+989120000000'])); + $this->assertPasses(UserRegistrationRequest::class, $this->valid(['phone' => '۰۹۱۲۰۰۰۰۰۰۰'])); + } + + public function test_fails_without_phone_or_national_code(): void + { + $errors = $this->formRequestErrors(UserRegistrationRequest::class, []); + + $this->assertArrayHasKey('phone', $errors); + $this->assertArrayHasKey('national_code', $errors); + } + + public function test_fails_with_invalid_phone(): void + { + $this->assertFailsOn('phone', UserRegistrationRequest::class, $this->valid(['phone' => 'abc'])); + $this->assertFailsOn('phone', UserRegistrationRequest::class, $this->valid(['phone' => '123'])); + $this->assertFailsOn('phone', UserRegistrationRequest::class, $this->valid(['phone' => '+9999120000000'])); + } + + public function test_fails_when_level_exceeds_limit(): void + { + $this->assertFailsOn('level', UserRegistrationRequest::class, $this->valid(['level' => str_repeat('a', 51)])); + } +} diff --git a/tests/Feature/Requests/Client/Auth/VerifyOtpRequestTest.php b/tests/Feature/Requests/Client/Auth/VerifyOtpRequestTest.php new file mode 100644 index 0000000..af31567 --- /dev/null +++ b/tests/Feature/Requests/Client/Auth/VerifyOtpRequestTest.php @@ -0,0 +1,62 @@ + '09120000000']); + } + + public function test_passes_with_valid_code_for_session_phone(): void + { + OtpCode::factory()->create(['phone' => '09120000000', 'code' => '1234']); + + $this->assertPasses(VerifyOtpRequest::class, ['otp_code' => '1234']); + } + + public function test_fails_without_code(): void + { + $this->assertFailsOn('otp_code', VerifyOtpRequest::class, []); + } + + public function test_fails_when_code_is_not_four_digits(): void + { + OtpCode::factory()->create(['phone' => '09120000000', 'code' => '123']); + + $this->assertFailsOn('otp_code', VerifyOtpRequest::class, ['otp_code' => '123']); + $this->assertFailsOn('otp_code', VerifyOtpRequest::class, ['otp_code' => 'abcd']); + } + + public function test_fails_with_wrong_code(): void + { + OtpCode::factory()->create(['phone' => '09120000000', 'code' => '1234']); + + $this->assertFailsOn('otp_code', VerifyOtpRequest::class, ['otp_code' => '0000']); + } + + public function test_fails_with_expired_code(): void + { + OtpCode::factory()->expired()->create(['phone' => '09120000000', 'code' => '1234']); + + $this->assertFailsOn('otp_code', VerifyOtpRequest::class, ['otp_code' => '1234']); + } + + public function test_fails_when_code_belongs_to_another_phone(): void + { + OtpCode::factory()->create(['phone' => '09130000000', 'code' => '1234']); + + $this->assertFailsOn('otp_code', VerifyOtpRequest::class, ['otp_code' => '1234']); + } +} diff --git a/tests/Feature/Requests/Client/BankCardStoreRequestTest.php b/tests/Feature/Requests/Client/BankCardStoreRequestTest.php new file mode 100644 index 0000000..724eedb --- /dev/null +++ b/tests/Feature/Requests/Client/BankCardStoreRequestTest.php @@ -0,0 +1,37 @@ +assertPasses(BankCardStoreRequest::class, ['card_number' => '6037991234567890', 'holder_name' => 'علی رضایی']); + } + + public function test_fails_without_required_fields(): void + { + $errors = $this->formRequestErrors(BankCardStoreRequest::class, []); + + $this->assertArrayHasKey('card_number', $errors); + $this->assertArrayHasKey('holder_name', $errors); + } + + public function test_fails_when_card_number_is_not_sixteen_digits(): void + { + $this->assertFailsOn('card_number', BankCardStoreRequest::class, ['card_number' => '603799123456789', 'holder_name' => 'x']); + $this->assertFailsOn('card_number', BankCardStoreRequest::class, ['card_number' => '60379912345678901', 'holder_name' => 'x']); + $this->assertFailsOn('card_number', BankCardStoreRequest::class, ['card_number' => '6037-9912-3456-7890', 'holder_name' => 'x']); + } + + public function test_fails_when_holder_name_is_too_long(): void + { + $this->assertFailsOn('holder_name', BankCardStoreRequest::class, ['card_number' => '6037991234567890', 'holder_name' => str_repeat('a', 256)]); + } +} diff --git a/tests/Feature/Requests/Client/CaseRejectRequestTest.php b/tests/Feature/Requests/Client/CaseRejectRequestTest.php new file mode 100644 index 0000000..f068c54 --- /dev/null +++ b/tests/Feature/Requests/Client/CaseRejectRequestTest.php @@ -0,0 +1,28 @@ +assertPasses(CaseRejectRequest::class, ['reason' => 'تصاویر ناخوانا هستند']); + } + + public function test_fails_without_reason(): void + { + $this->assertFailsOn('reason', CaseRejectRequest::class, []); + $this->assertFailsOn('reason', CaseRejectRequest::class, ['reason' => '']); + } + + public function test_fails_when_reason_exceeds_limit(): void + { + $this->assertFailsOn('reason', CaseRejectRequest::class, ['reason' => str_repeat('a', 1001)]); + } +} diff --git a/tests/Feature/Requests/Client/FastRequestRequestTest.php b/tests/Feature/Requests/Client/FastRequestRequestTest.php new file mode 100644 index 0000000..ad42452 --- /dev/null +++ b/tests/Feature/Requests/Client/FastRequestRequestTest.php @@ -0,0 +1,114 @@ +user = User::factory()->create(); + } + + protected function valid(array $overrides = []): array + { + return array_merge([ + 'modality' => DocumentModalityEnum::Mri->value, + 'priority' => DocumentPriorityEnum::High->name, + 'pet' => ['name' => 'Rex', 'type' => PetTypeEnum::Dog->name], + ], $overrides); + } + + public function test_passes_with_new_pet(): void + { + $this->assertPasses(FastRequestRequest::class, $this->valid([ + 'note' => 'n', + 'pet' => ['name' => 'Rex', 'type' => PetTypeEnum::Dog->name, 'gender' => PetGenderEnum::Male->name, 'breed' => 'b', 'age' => 3, 'weight' => 20, 'sterilized' => true], + ]), [], $this->user); + } + + public function test_passes_with_own_existing_pet_and_no_pet_payload(): void + { + $pet = Pet::factory()->create(['owner_id' => $this->user->id]); + + $this->assertPasses(FastRequestRequest::class, ['modality' => DocumentModalityEnum::Mri->value, 'priority' => DocumentPriorityEnum::Medium->name, 'pet_id' => $pet->id], [], $this->user); + } + + public function test_passes_with_doctor_and_image_files(): void + { + $doctor = User::factory()->create(['level' => UserEnum::Doctor->value]); + + $this->assertPasses(FastRequestRequest::class, $this->valid(['doctor_id' => $doctor->id]), [ + 'history_files' => [UploadedFile::fake()->image('h.jpg')], + 'scan_files' => [UploadedFile::fake()->image('s.png')], + ], $this->user); + } + + public function test_fails_without_modality_priority_or_pet(): void + { + $errors = $this->formRequestErrors(FastRequestRequest::class, [], [], $this->user); + + $this->assertArrayHasKey('modality', $errors); + $this->assertArrayHasKey('priority', $errors); + $this->assertArrayHasKey('pet', $errors); + $this->assertArrayHasKey('pet.name', $errors); + $this->assertArrayHasKey('pet.type', $errors); + } + + public function test_fails_with_invalid_modality_or_priority(): void + { + $this->assertFailsOn('modality', FastRequestRequest::class, $this->valid(['modality' => 'xray']), [], $this->user); + $this->assertFailsOn('priority', FastRequestRequest::class, $this->valid(['priority' => DocumentPriorityEnum::High->value]), [], $this->user); + } + + public function test_fails_with_pet_of_another_user(): void + { + $pet = Pet::factory()->create(); + + $this->assertFailsOn('pet_id', FastRequestRequest::class, $this->valid(['pet_id' => $pet->id]), [], $this->user); + } + + public function test_fails_with_invalid_pet_type_gender_age_or_weight(): void + { + $this->assertFailsOn('pet.type', FastRequestRequest::class, $this->valid(['pet' => ['name' => 'x', 'type' => PetTypeEnum::Dog->value]]), [], $this->user); + $this->assertFailsOn('pet.gender', FastRequestRequest::class, $this->valid(['pet' => ['name' => 'x', 'type' => 'Dog', 'gender' => 'Alien']]), [], $this->user); + $this->assertFailsOn('pet.age', FastRequestRequest::class, $this->valid(['pet' => ['name' => 'x', 'type' => 'Dog', 'age' => 101]]), [], $this->user); + $this->assertFailsOn('pet.weight', FastRequestRequest::class, $this->valid(['pet' => ['name' => 'x', 'type' => 'Dog', 'weight' => -1]]), [], $this->user); + } + + public function test_fails_with_non_doctor_as_doctor_id(): void + { + $vet = User::factory()->create(['level' => UserEnum::Vet->value]); + + $this->assertFailsOn('doctor_id', FastRequestRequest::class, $this->valid(['doctor_id' => $vet->id]), [], $this->user); + } + + public function test_fails_with_non_image_or_oversized_files(): void + { + $this->assertFailsOn('history_files.0', FastRequestRequest::class, $this->valid(), ['history_files' => [UploadedFile::fake()->create('a.pdf', 10, 'application/pdf')]], $this->user); + $this->assertFailsOn('scan_files.0', FastRequestRequest::class, $this->valid(), ['scan_files' => [UploadedFile::fake()->image('a.jpg')->size(10241)]], $this->user); + } + + public function test_fails_when_note_exceeds_limit(): void + { + $this->assertFailsOn('note', FastRequestRequest::class, $this->valid(['note' => str_repeat('a', 1001)]), [], $this->user); + } +} diff --git a/tests/Feature/Requests/Client/PaymentRequestTest.php b/tests/Feature/Requests/Client/PaymentRequestTest.php new file mode 100644 index 0000000..7fa44f5 --- /dev/null +++ b/tests/Feature/Requests/Client/PaymentRequestTest.php @@ -0,0 +1,36 @@ +create(); + + $this->assertPasses(PaymentRequest::class, ['invoice_id' => $invoice->id]); + } + + public function test_fails_without_invoice_id(): void + { + $this->assertFailsOn('invoice_id', PaymentRequest::class, []); + } + + public function test_fails_with_non_integer_invoice_id(): void + { + $this->assertFailsOn('invoice_id', PaymentRequest::class, ['invoice_id' => 'abc']); + } + + public function test_fails_with_unknown_invoice_id(): void + { + $this->assertFailsOn('invoice_id', PaymentRequest::class, ['invoice_id' => 999]); + } +} diff --git a/tests/Feature/Requests/Client/ProfileUpdateRequestTest.php b/tests/Feature/Requests/Client/ProfileUpdateRequestTest.php new file mode 100644 index 0000000..1573803 --- /dev/null +++ b/tests/Feature/Requests/Client/ProfileUpdateRequestTest.php @@ -0,0 +1,65 @@ + 'علی رضایی', 'national_code' => '0012345678'], $overrides); + } + + public function test_passes_with_required_fields_only(): void + { + $this->assertPasses(ProfileUpdateRequest::class, $this->valid()); + } + + public function test_passes_with_all_optional_fields_and_png_avatar(): void + { + $this->assertPasses(ProfileUpdateRequest::class, $this->valid([ + 'birthdate' => '1990-01-01', + 'email' => 'a@b.com', + 'province' => 'تهران', + 'city' => 'تهران', + 'address' => 'خیابان', + ]), ['avatar' => UploadedFile::fake()->image('a.png')]); + } + + public function test_fails_without_fullname_or_national_code(): void + { + $errors = $this->formRequestErrors(ProfileUpdateRequest::class, []); + + $this->assertArrayHasKey('fullname', $errors); + $this->assertArrayHasKey('national_code', $errors); + } + + public function test_fails_with_invalid_birthdate_or_email(): void + { + $this->assertFailsOn('birthdate', ProfileUpdateRequest::class, $this->valid(['birthdate' => 'not-a-date'])); + $this->assertFailsOn('email', ProfileUpdateRequest::class, $this->valid(['email' => 'not-an-email'])); + } + + public function test_fails_when_national_code_or_address_too_long(): void + { + $this->assertFailsOn('national_code', ProfileUpdateRequest::class, $this->valid(['national_code' => str_repeat('1', 21)])); + $this->assertFailsOn('address', ProfileUpdateRequest::class, $this->valid(['address' => str_repeat('a', 501)])); + } + + public function test_fails_with_non_image_or_disallowed_avatar_type(): void + { + $this->assertFailsOn('avatar', ProfileUpdateRequest::class, $this->valid(), ['avatar' => UploadedFile::fake()->create('a.pdf', 10, 'application/pdf')]); + $this->assertFailsOn('avatar', ProfileUpdateRequest::class, $this->valid(), ['avatar' => UploadedFile::fake()->image('a.gif')]); + } + + public function test_fails_with_avatar_over_ten_megabytes(): void + { + $this->assertFailsOn('avatar', ProfileUpdateRequest::class, $this->valid(), ['avatar' => UploadedFile::fake()->image('a.jpg')->size(10241)]); + } +} diff --git a/tests/Feature/Requests/Client/ReportSubmitRequestTest.php b/tests/Feature/Requests/Client/ReportSubmitRequestTest.php new file mode 100644 index 0000000..0922e8c --- /dev/null +++ b/tests/Feature/Requests/Client/ReportSubmitRequestTest.php @@ -0,0 +1,62 @@ + Document::factory()->create()->id, 'finding' => 'یافته‌ها'], $overrides); + } + + public function test_passes_with_document_and_finding(): void + { + $this->assertPasses(ReportSubmitRequest::class, $this->valid()); + } + + public function test_passes_with_optional_sections_and_image_files(): void + { + $this->assertPasses(ReportSubmitRequest::class, $this->valid([ + 'technique' => 't', + 'diagnose' => 'd', + 'recommendation' => 'r', + ]), ['files' => [UploadedFile::fake()->image('a.jpg'), UploadedFile::fake()->image('b.png')]]); + } + + public function test_fails_without_document_or_finding(): void + { + $errors = $this->formRequestErrors(ReportSubmitRequest::class, []); + + $this->assertArrayHasKey('document_id', $errors); + $this->assertArrayHasKey('finding', $errors); + } + + public function test_fails_with_unknown_document(): void + { + $this->assertFailsOn('document_id', ReportSubmitRequest::class, $this->valid(['document_id' => 999])); + } + + public function test_fails_when_section_exceeds_limit(): void + { + $this->assertFailsOn('finding', ReportSubmitRequest::class, $this->valid(['finding' => str_repeat('a', 10001)])); + } + + public function test_fails_with_non_image_file(): void + { + $this->assertFailsOn('files.0', ReportSubmitRequest::class, $this->valid(), ['files' => [UploadedFile::fake()->create('a.pdf', 10, 'application/pdf')]]); + } + + public function test_fails_with_file_over_ten_megabytes(): void + { + $this->assertFailsOn('files.0', ReportSubmitRequest::class, $this->valid(), ['files' => [UploadedFile::fake()->image('a.jpg')->size(10241)]]); + } +} diff --git a/tests/Feature/Requests/Client/WalletDepositRequestTest.php b/tests/Feature/Requests/Client/WalletDepositRequestTest.php new file mode 100644 index 0000000..468e5ab --- /dev/null +++ b/tests/Feature/Requests/Client/WalletDepositRequestTest.php @@ -0,0 +1,39 @@ +assertPasses(WalletDepositRequest::class, ['amount' => 10000]); + $this->assertPasses(WalletDepositRequest::class, ['amount' => 500000000]); + } + + public function test_fails_without_amount(): void + { + $this->assertFailsOn('amount', WalletDepositRequest::class, []); + } + + public function test_fails_with_non_integer_amount(): void + { + $this->assertFailsOn('amount', WalletDepositRequest::class, ['amount' => '10000.5']); + } + + public function test_fails_below_minimum(): void + { + $this->assertFailsOn('amount', WalletDepositRequest::class, ['amount' => 9999]); + } + + public function test_fails_above_maximum(): void + { + $this->assertFailsOn('amount', WalletDepositRequest::class, ['amount' => 500000001]); + } +} diff --git a/tests/Feature/Requests/Client/WithdrawRequestStoreRequestTest.php b/tests/Feature/Requests/Client/WithdrawRequestStoreRequestTest.php new file mode 100644 index 0000000..c9217b0 --- /dev/null +++ b/tests/Feature/Requests/Client/WithdrawRequestStoreRequestTest.php @@ -0,0 +1,42 @@ +create(); + + $this->assertPasses(WithdrawRequestStoreRequest::class, ['amount' => 50000, 'bank_card_id' => $bankCard->id]); + } + + public function test_fails_without_required_fields(): void + { + $errors = $this->formRequestErrors(WithdrawRequestStoreRequest::class, []); + + $this->assertArrayHasKey('amount', $errors); + $this->assertArrayHasKey('bank_card_id', $errors); + } + + public function test_fails_with_amount_out_of_range(): void + { + $bankCard = BankCard::factory()->create(); + + $this->assertFailsOn('amount', WithdrawRequestStoreRequest::class, ['amount' => 9999, 'bank_card_id' => $bankCard->id]); + $this->assertFailsOn('amount', WithdrawRequestStoreRequest::class, ['amount' => 500000001, 'bank_card_id' => $bankCard->id]); + } + + public function test_fails_with_unknown_bank_card(): void + { + $this->assertFailsOn('bank_card_id', WithdrawRequestStoreRequest::class, ['amount' => 50000, 'bank_card_id' => 999]); + } +}