From 74d8481c6cccb0ba3f21570cfb79d59caf0fec5d Mon Sep 17 00:00:00 2001 From: celikerde Date: Mon, 22 Jun 2026 13:12:14 +0300 Subject: [PATCH 1/7] Add tests for activator reset functionality - Implement tests to verify deletion of status file on reset. - Ensure route statuses are cleared and treated as inactive after reset. - Confirm no failure occurs when resetting without an existing status file. --- tests/Activators/ModuleActivatorTest.php | 52 ++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/tests/Activators/ModuleActivatorTest.php b/tests/Activators/ModuleActivatorTest.php index a4b56d66b..39acd626c 100644 --- a/tests/Activators/ModuleActivatorTest.php +++ b/tests/Activators/ModuleActivatorTest.php @@ -610,4 +610,56 @@ public function it_creates_valid_json_structure() $this->assertNotNull($decoded); $this->assertIsArray($decoded); } + + /** @test */ + public function it_deletes_statuses_file_on_reset() + { + $this->activator->enable('items'); + $this->activator->enable('categories'); + + $this->assertTrue($this->files->exists($this->statusFile)); + + $this->activator->reset(); + + $this->assertFalse($this->files->exists($this->statusFile)); + } + + /** @test */ + public function it_clears_route_statuses_on_reset() + { + $this->activator->enable('items'); + $this->activator->disable('categories'); + + $this->assertCount(2, $this->activator->getRoutesStatuses()); + + $this->activator->reset(); + + $statuses = $this->activator->getRoutesStatuses(); + $this->assertIsArray($statuses); + $this->assertEmpty($statuses); + } + + /** @test */ + public function it_treats_all_routes_as_inactive_after_reset() + { + $this->activator->enable('items'); + $this->assertTrue($this->activator->hasStatus('items', true)); + + $this->activator->reset(); + + // Once reset, the previously enabled route should be treated as inactive + $this->assertFalse($this->activator->hasStatus('items', true)); + $this->assertTrue($this->activator->hasStatus('items', false)); + } + + /** @test */ + public function it_does_not_fail_when_resetting_without_existing_file() + { + $this->assertFalse($this->files->exists($this->statusFile)); + + $this->activator->reset(); + + $this->assertFalse($this->files->exists($this->statusFile)); + $this->assertEmpty($this->activator->getRoutesStatuses()); + } } From e3b323ec7db20228f69392e3c5155df5ec501f71 Mon Sep 17 00:00:00 2001 From: celikerde Date: Mon, 22 Jun 2026 13:12:31 +0300 Subject: [PATCH 2/7] Add unit tests for various status and permission enums - Introduced tests for AssignmentStatus, PaymentStatus, Permission, ProcessStatus, and RevisionStatus enums. - Verified expected values, labels, colors, icons, and additional methods for each enum. - Ensured comprehensive coverage of enum functionalities to maintain code integrity. --- tests/Entities/Enums/AssignmentStatusTest.php | 75 +++++++++++ tests/Entities/Enums/PaymentStatusTest.php | 65 +++++++++ tests/Entities/Enums/PermissionTest.php | 66 +++++++++ tests/Entities/Enums/ProcessStatusTest.php | 127 ++++++++++++++++++ tests/Entities/Enums/RevisionStatusTest.php | 31 +++++ 5 files changed, 364 insertions(+) create mode 100644 tests/Entities/Enums/AssignmentStatusTest.php create mode 100644 tests/Entities/Enums/PaymentStatusTest.php create mode 100644 tests/Entities/Enums/PermissionTest.php create mode 100644 tests/Entities/Enums/ProcessStatusTest.php create mode 100644 tests/Entities/Enums/RevisionStatusTest.php diff --git a/tests/Entities/Enums/AssignmentStatusTest.php b/tests/Entities/Enums/AssignmentStatusTest.php new file mode 100644 index 000000000..b319dfc0a --- /dev/null +++ b/tests/Entities/Enums/AssignmentStatusTest.php @@ -0,0 +1,75 @@ +assertEquals('completed', AssignmentStatus::COMPLETED->value); + $this->assertEquals('pending', AssignmentStatus::PENDING->value); + $this->assertEquals('rejected', AssignmentStatus::REJECTED->value); + $this->assertEquals('cancelled', AssignmentStatus::CANCELLED->value); + } + + public function test_it_lists_all_cases() + { + $this->assertSame([ + AssignmentStatus::COMPLETED, + AssignmentStatus::PENDING, + AssignmentStatus::REJECTED, + AssignmentStatus::CANCELLED, + ], AssignmentStatus::cases()); + } + + public function test_label_returns_translated_label() + { + $this->assertEquals(__('Completed'), AssignmentStatus::COMPLETED->label()); + $this->assertEquals(__('Pending'), AssignmentStatus::PENDING->label()); + $this->assertEquals(__('Rejected'), AssignmentStatus::REJECTED->label()); + $this->assertEquals(__('Cancelled'), AssignmentStatus::CANCELLED->label()); + } + + public function test_color_returns_expected_class() + { + $this->assertEquals('text-success', AssignmentStatus::COMPLETED->color()); + $this->assertEquals('text-warning', AssignmentStatus::PENDING->color()); + $this->assertEquals('text-error', AssignmentStatus::REJECTED->color()); + $this->assertEquals('text-grey', AssignmentStatus::CANCELLED->color()); + } + + public function test_icon_color_returns_expected_value() + { + $this->assertEquals('success', AssignmentStatus::COMPLETED->iconColor()); + $this->assertEquals('info', AssignmentStatus::PENDING->iconColor()); + $this->assertEquals('error', AssignmentStatus::REJECTED->iconColor()); + $this->assertEquals('grey', AssignmentStatus::CANCELLED->iconColor()); + } + + public function test_icon_returns_expected_value() + { + $this->assertEquals('mdi-check-circle-outline', AssignmentStatus::COMPLETED->icon()); + $this->assertEquals('mdi-clock-outline', AssignmentStatus::PENDING->icon()); + $this->assertEquals('mdi-close-circle-outline', AssignmentStatus::REJECTED->icon()); + $this->assertEquals('mdi-close-circle-outline', AssignmentStatus::CANCELLED->icon()); + } + + public function test_time_interval_description_returns_translated_value() + { + $this->assertEquals(__('Until'), AssignmentStatus::PENDING->timeIntervalDescription()); + $this->assertEquals(__('Rejected'), AssignmentStatus::REJECTED->timeIntervalDescription()); + $this->assertEquals(__('Cancelled'), AssignmentStatus::CANCELLED->timeIntervalDescription()); + $this->assertEquals(__('Completed'), AssignmentStatus::COMPLETED->timeIntervalDescription()); + } + + public function test_time_classes_returns_expected_value() + { + $this->assertEquals('font-weight-bold text-blue-darken-1', AssignmentStatus::PENDING->timeClasses()); + $this->assertEquals('font-weight-bold text-error', AssignmentStatus::REJECTED->timeClasses()); + $this->assertEquals('font-weight-bold text-warning', AssignmentStatus::CANCELLED->timeClasses()); + $this->assertEquals('font-weight-bold text-success', AssignmentStatus::COMPLETED->timeClasses()); + } +} diff --git a/tests/Entities/Enums/PaymentStatusTest.php b/tests/Entities/Enums/PaymentStatusTest.php new file mode 100644 index 000000000..94012f3d2 --- /dev/null +++ b/tests/Entities/Enums/PaymentStatusTest.php @@ -0,0 +1,65 @@ +assertEquals('PENDING', PaymentStatus::PENDING->value); + $this->assertEquals('FAILED', PaymentStatus::FAILED->value); + $this->assertEquals('CHECKOUT', PaymentStatus::CHECKOUT->value); + $this->assertEquals('PROVISION', PaymentStatus::PROVISION->value); + $this->assertEquals('COMPLETED', PaymentStatus::COMPLETED->value); + $this->assertEquals('CANCELLED', PaymentStatus::CANCELLED->value); + $this->assertEquals('REFUNDED', PaymentStatus::REFUNDED->value); + } + + public function test_get_returns_value_for_known_case_name() + { + $this->assertEquals('PENDING', PaymentStatus::get('PENDING')); + $this->assertEquals('REFUNDED', PaymentStatus::get('REFUNDED')); + } + + public function test_get_returns_null_for_unknown_case_name() + { + $this->assertNull(PaymentStatus::get('UNKNOWN')); + $this->assertNull(PaymentStatus::get('pending')); + } + + public function test_label_returns_translated_label() + { + $this->assertEquals(__('Pending'), PaymentStatus::PENDING->label()); + $this->assertEquals(__('Failed'), PaymentStatus::FAILED->label()); + $this->assertEquals(__('Checkout'), PaymentStatus::CHECKOUT->label()); + $this->assertEquals(__('Provision'), PaymentStatus::PROVISION->label()); + $this->assertEquals(__('Completed'), PaymentStatus::COMPLETED->label()); + $this->assertEquals(__('Cancelled'), PaymentStatus::CANCELLED->label()); + $this->assertEquals(__('Refunded'), PaymentStatus::REFUNDED->label()); + } + + public function test_color_returns_expected_value() + { + $this->assertEquals('grey', PaymentStatus::PENDING->color()); + $this->assertEquals('warning', PaymentStatus::FAILED->color()); + $this->assertEquals('primary', PaymentStatus::CHECKOUT->color()); + $this->assertEquals('info', PaymentStatus::PROVISION->color()); + $this->assertEquals('success', PaymentStatus::COMPLETED->color()); + $this->assertEquals('error', PaymentStatus::CANCELLED->color()); + $this->assertEquals('grey', PaymentStatus::REFUNDED->color()); + } + + public function test_icon_returns_expected_value() + { + $this->assertEquals('mdi-clock-alert-outline', PaymentStatus::PENDING->icon()); + $this->assertEquals('mdi-close-circle-outline', PaymentStatus::FAILED->icon()); + $this->assertEquals('mdi-cart-outline', PaymentStatus::CHECKOUT->icon()); + $this->assertEquals('mdi-progress-clock', PaymentStatus::PROVISION->icon()); + $this->assertEquals('mdi-check-circle-outline', PaymentStatus::COMPLETED->icon()); + $this->assertEquals('mdi-close-circle-outline', PaymentStatus::CANCELLED->icon()); + $this->assertEquals('mdi-credit-card-refund-outline', PaymentStatus::REFUNDED->icon()); + } +} diff --git a/tests/Entities/Enums/PermissionTest.php b/tests/Entities/Enums/PermissionTest.php new file mode 100644 index 000000000..0221fe7dd --- /dev/null +++ b/tests/Entities/Enums/PermissionTest.php @@ -0,0 +1,66 @@ +assertEquals('create', Permission::CREATE->value); + $this->assertEquals('view', Permission::VIEW->value); + $this->assertEquals('edit', Permission::EDIT->value); + $this->assertEquals('delete', Permission::DELETE->value); + $this->assertEquals('forceDelete', Permission::FORCEDELETE->value); + $this->assertEquals('restore', Permission::RESTORE->value); + $this->assertEquals('duplicate', Permission::DUPLICATE->value); + $this->assertEquals('reorder', Permission::REORDER->value); + $this->assertEquals('bulk', Permission::BULK->value); + $this->assertEquals('bulkDelete', Permission::BULKDELETE->value); + $this->assertEquals('bulkForceDelete', Permission::BULKFORCEDELETE->value); + $this->assertEquals('bulkRestore', Permission::BULKRESTORE->value); + $this->assertEquals('revisionApprove', Permission::REVISION_APPROVE->value); + $this->assertEquals('revisionReject', Permission::REVISION_REJECT->value); + $this->assertEquals('revisionRestore', Permission::REVISION_RESTORE->value); + $this->assertEquals('activity', Permission::ACTIVITY->value); + $this->assertEquals('show', Permission::SHOW->value); + } + + public function test_get_resolves_by_case_name() + { + $this->assertEquals('create', Permission::get('CREATE')); + $this->assertEquals('forceDelete', Permission::get('FORCEDELETE')); + $this->assertEquals('revisionApprove', Permission::get('REVISION_APPROVE')); + } + + public function test_get_resolves_by_case_value() + { + $this->assertEquals('edit', Permission::get('edit')); + $this->assertEquals('bulkRestore', Permission::get('bulkRestore')); + } + + public function test_get_returns_null_for_unknown_value() + { + $this->assertNull(Permission::get('nonexistent')); + } + + public function test_generate_permission_name_kebab_cases_route_name() + { + $this->assertEquals('blog-post_create', Permission::generatePermissionName('CREATE', 'blogPost')); + $this->assertEquals('user-profile_edit', Permission::generatePermissionName('edit', 'userProfile')); + } + + public function test_generate_permission_middleware_definition() + { + $this->assertEquals( + 'can:blog-post_create', + Permission::generatePermissionMiddlewareDefinition('CREATE', 'blogPost') + ); + $this->assertEquals( + 'can:user-profile_delete', + Permission::generatePermissionMiddlewareDefinition('delete', 'userProfile') + ); + } +} diff --git a/tests/Entities/Enums/ProcessStatusTest.php b/tests/Entities/Enums/ProcessStatusTest.php new file mode 100644 index 000000000..b3bb53c62 --- /dev/null +++ b/tests/Entities/Enums/ProcessStatusTest.php @@ -0,0 +1,127 @@ +assertEquals('preparing', ProcessStatus::PREPARING->value); + $this->assertEquals('waiting_for_confirmation', ProcessStatus::WAITING_FOR_CONFIRMATION->value); + $this->assertEquals('waiting_for_reaction', ProcessStatus::WAITING_FOR_REACTION->value); + $this->assertEquals('rejected', ProcessStatus::REJECTED->value); + $this->assertEquals('confirmed', ProcessStatus::CONFIRMED->value); + } + + public function test_get_returns_value_for_known_case_name() + { + $this->assertEquals('preparing', ProcessStatus::get('PREPARING')); + $this->assertEquals('confirmed', ProcessStatus::get('CONFIRMED')); + } + + public function test_get_returns_null_for_unknown_case_name() + { + $this->assertNull(ProcessStatus::get('UNKNOWN')); + $this->assertNull(ProcessStatus::get('preparing')); + } + + public function test_label_returns_translated_value() + { + $this->assertEquals(__('Preparing'), ProcessStatus::PREPARING->label()); + $this->assertEquals(__('Waiting'), ProcessStatus::WAITING_FOR_CONFIRMATION->label()); + $this->assertEquals(__('Waiting'), ProcessStatus::WAITING_FOR_REACTION->label()); + $this->assertEquals(__('Rejected'), ProcessStatus::REJECTED->label()); + $this->assertEquals(__('Confirmed'), ProcessStatus::CONFIRMED->label()); + } + + public function test_color_returns_expected_value() + { + $this->assertEquals('info', ProcessStatus::PREPARING->color()); + $this->assertEquals('warning', ProcessStatus::WAITING_FOR_CONFIRMATION->color()); + $this->assertEquals('warning', ProcessStatus::WAITING_FOR_REACTION->color()); + $this->assertEquals('error', ProcessStatus::REJECTED->color()); + $this->assertEquals('success', ProcessStatus::CONFIRMED->color()); + } + + public function test_card_color_returns_expected_value() + { + $this->assertEquals('grey', ProcessStatus::PREPARING->cardColor()); + $this->assertEquals('blue-darken-1', ProcessStatus::WAITING_FOR_CONFIRMATION->cardColor()); + $this->assertEquals('blue-darken-1', ProcessStatus::WAITING_FOR_REACTION->cardColor()); + $this->assertEquals('red-darken-1', ProcessStatus::REJECTED->cardColor()); + $this->assertEquals('green-darken-1', ProcessStatus::CONFIRMED->cardColor()); + } + + public function test_card_variant_returns_expected_value() + { + $this->assertEquals('outlined', ProcessStatus::PREPARING->cardVariant()); + $this->assertEquals('outlined', ProcessStatus::WAITING_FOR_CONFIRMATION->cardVariant()); + $this->assertEquals('outlined', ProcessStatus::WAITING_FOR_REACTION->cardVariant()); + $this->assertEquals('tonal', ProcessStatus::REJECTED->cardVariant()); + $this->assertEquals('tonal', ProcessStatus::CONFIRMED->cardVariant()); + } + + public function test_icon_returns_expected_value() + { + $this->assertEquals('mdi-progress-clock', ProcessStatus::PREPARING->icon()); + $this->assertEquals('mdi-clock-check-outline', ProcessStatus::WAITING_FOR_CONFIRMATION->icon()); + $this->assertEquals('mdi-clock-check-outline', ProcessStatus::WAITING_FOR_REACTION->icon()); + $this->assertEquals('mdi-close-circle-outline', ProcessStatus::REJECTED->icon()); + $this->assertEquals('mdi-check-circle-outline', ProcessStatus::CONFIRMED->icon()); + } + + public function test_next_action_label_returns_translated_value() + { + $this->assertEquals(__('Send for Confirmation'), ProcessStatus::PREPARING->nextActionLabel()); + $this->assertEquals(__('Confirm'), ProcessStatus::WAITING_FOR_CONFIRMATION->nextActionLabel()); + $this->assertEquals(__('Confirm'), ProcessStatus::WAITING_FOR_REACTION->nextActionLabel()); + $this->assertEquals(__('Resend'), ProcessStatus::REJECTED->nextActionLabel()); + $this->assertEquals(__('Revert'), ProcessStatus::CONFIRMED->nextActionLabel()); + } + + public function test_status_reason_label_returns_translated_value() + { + $this->assertEquals(__('Preparing'), ProcessStatus::PREPARING->statusReasonLabel()); + $this->assertEquals(__('Arrangement'), ProcessStatus::WAITING_FOR_CONFIRMATION->statusReasonLabel()); + $this->assertEquals(__('Arrangement'), ProcessStatus::WAITING_FOR_REACTION->statusReasonLabel()); + $this->assertEquals(__('Reason'), ProcessStatus::REJECTED->statusReasonLabel()); + $this->assertEquals(__('Confirmation Reason'), ProcessStatus::CONFIRMED->statusReasonLabel()); + } + + public function test_next_action_color_returns_expected_value() + { + $this->assertEquals('secondary', ProcessStatus::PREPARING->nextActionColor()); + $this->assertEquals('success', ProcessStatus::WAITING_FOR_CONFIRMATION->nextActionColor()); + $this->assertEquals('success', ProcessStatus::WAITING_FOR_REACTION->nextActionColor()); + $this->assertEquals('secondary', ProcessStatus::REJECTED->nextActionColor()); + $this->assertEquals('grey-lighten-2', ProcessStatus::CONFIRMED->nextActionColor()); + } + + public function test_informational_message_returns_translated_value() + { + $this->assertEquals( + __('The contents are being prepared or updated. Please check back later.'), + ProcessStatus::PREPARING->informationalMessage() + ); + $this->assertEquals( + __('The contents has been rejected. The reason is under review, you will be informed soon.'), + ProcessStatus::REJECTED->informationalMessage() + ); + $this->assertEquals( + __('The contents are confirmed.'), + ProcessStatus::CONFIRMED->informationalMessage() + ); + // Cases without an explicit arm fall back to the default message. + $this->assertEquals( + __('The contents are being prepared or updated. Please check back later.'), + ProcessStatus::WAITING_FOR_CONFIRMATION->informationalMessage() + ); + $this->assertEquals( + __('The contents are being prepared or updated. Please check back later.'), + ProcessStatus::WAITING_FOR_REACTION->informationalMessage() + ); + } +} diff --git a/tests/Entities/Enums/RevisionStatusTest.php b/tests/Entities/Enums/RevisionStatusTest.php new file mode 100644 index 000000000..2382f8547 --- /dev/null +++ b/tests/Entities/Enums/RevisionStatusTest.php @@ -0,0 +1,31 @@ +assertEquals('pending', RevisionStatus::Pending->value); + $this->assertEquals('approved', RevisionStatus::Approved->value); + $this->assertEquals('rejected', RevisionStatus::Rejected->value); + } + + public function test_it_lists_all_cases() + { + $this->assertSame([ + RevisionStatus::Pending, + RevisionStatus::Approved, + RevisionStatus::Rejected, + ], RevisionStatus::cases()); + } + + public function test_default_approved_returns_approved_case() + { + $this->assertSame(RevisionStatus::Approved, RevisionStatus::defaultApproved()); + $this->assertEquals('approved', RevisionStatus::defaultApproved()->value); + } +} From e49d509d1d2901b6ffdfdc447a5780bb888af8ab Mon Sep 17 00:00:00 2001 From: celikerde Date: Mon, 22 Jun 2026 13:12:44 +0300 Subject: [PATCH 3/7] Add unit tests for Modularous user events - Introduced tests for ModularousUserRegistered, ModularousUserRegistering, ModularousUserVerification, and VerifiedEmailRegister events. - Verified constructor behavior, request handling, and OAuth status for each event. - Ensured event instances are independent and utilize the SerializesModels trait for model serialization. --- tests/Events/ModularousUserRegisteredTest.php | 120 ++++++++++++++++++ .../Events/ModularousUserRegisteringTest.php | 69 ++++++++++ .../Events/ModularousUserVerificationTest.php | 47 +++++++ tests/Events/VerifiedEmailRegisterTest.php | 90 +++++++++++++ 4 files changed, 326 insertions(+) create mode 100644 tests/Events/ModularousUserRegisteredTest.php create mode 100644 tests/Events/ModularousUserRegisteringTest.php create mode 100644 tests/Events/ModularousUserVerificationTest.php create mode 100644 tests/Events/VerifiedEmailRegisterTest.php diff --git a/tests/Events/ModularousUserRegisteredTest.php b/tests/Events/ModularousUserRegisteredTest.php new file mode 100644 index 000000000..fc106a264 --- /dev/null +++ b/tests/Events/ModularousUserRegisteredTest.php @@ -0,0 +1,120 @@ +makeUser(); + $request = Request::create('/register', 'POST'); + + $event = new ModularousUserRegistered($user, $request); + + $this->assertSame($user, $event->user); + $this->assertSame($request, $event->request); + } + + public function test_is_oauth_defaults_to_false() + { + $event = new ModularousUserRegistered($this->makeUser(), Request::create('/register', 'POST')); + + $this->assertFalse($event->isOauth()); + } + + public function test_is_oauth_returns_true_when_set() + { + $event = new ModularousUserRegistered($this->makeUser(), Request::create('/register', 'POST'), true); + + $this->assertTrue($event->isOauth()); + } + + public function test_is_oauth_can_be_explicitly_false() + { + $event = new ModularousUserRegistered($this->makeUser(), Request::create('/register', 'POST'), false); + + $this->assertFalse($event->isOauth()); + } + + public function test_user_and_request_properties_are_public() + { + $user = $this->makeUser(); + $request = Request::create('/register', 'POST'); + + $event = new ModularousUserRegistered($user, $request, true); + + $this->assertEquals($user->getAuthIdentifier(), $event->user->getAuthIdentifier()); + $this->assertEquals('/register', $event->request->getPathInfo()); + $this->assertEquals('POST', $event->request->getMethod()); + } + + public function test_event_instances_are_independent() + { + $userOne = $this->makeUser(); + $userTwo = $this->makeUser(); + + $eventOne = new ModularousUserRegistered($userOne, Request::create('/register', 'POST'), true); + $eventTwo = new ModularousUserRegistered($userTwo, Request::create('/oauth/callback', 'GET'), false); + + $this->assertSame($userOne, $eventOne->user); + $this->assertSame($userTwo, $eventTwo->user); + $this->assertTrue($eventOne->isOauth()); + $this->assertFalse($eventTwo->isOauth()); + $this->assertEquals('/register', $eventOne->request->getPathInfo()); + $this->assertEquals('/oauth/callback', $eventTwo->request->getPathInfo()); + } + + public function test_event_uses_serializes_models_trait() + { + $traits = class_uses(ModularousUserRegistered::class); + + $this->assertContains(SerializesModels::class, $traits); + } +} diff --git a/tests/Events/ModularousUserRegisteringTest.php b/tests/Events/ModularousUserRegisteringTest.php new file mode 100644 index 000000000..8e41ccce7 --- /dev/null +++ b/tests/Events/ModularousUserRegisteringTest.php @@ -0,0 +1,69 @@ +assertSame($request, $event->request); + } + + public function test_is_oauth_defaults_to_false() + { + $event = new ModularousUserRegistering(Request::create('/register', 'POST')); + + $this->assertFalse($event->isOauth()); + } + + public function test_is_oauth_returns_true_when_set() + { + $event = new ModularousUserRegistering(Request::create('/register', 'POST'), true); + + $this->assertTrue($event->isOauth()); + } + + public function test_is_oauth_can_be_explicitly_false() + { + $event = new ModularousUserRegistering(Request::create('/register', 'POST'), false); + + $this->assertFalse($event->isOauth()); + } + + public function test_request_property_is_public() + { + $request = Request::create('/register', 'POST'); + + $event = new ModularousUserRegistering($request, true); + + $this->assertEquals('/register', $event->request->getPathInfo()); + $this->assertEquals('POST', $event->request->getMethod()); + } + + public function test_event_instances_are_independent() + { + $eventOne = new ModularousUserRegistering(Request::create('/register', 'POST'), true); + $eventTwo = new ModularousUserRegistering(Request::create('/oauth/callback', 'GET'), false); + + $this->assertTrue($eventOne->isOauth()); + $this->assertFalse($eventTwo->isOauth()); + $this->assertEquals('/register', $eventOne->request->getPathInfo()); + $this->assertEquals('/oauth/callback', $eventTwo->request->getPathInfo()); + } + + public function test_event_uses_serializes_models_trait() + { + $traits = class_uses(ModularousUserRegistering::class); + + $this->assertContains(SerializesModels::class, $traits); + } +} diff --git a/tests/Events/ModularousUserVerificationTest.php b/tests/Events/ModularousUserVerificationTest.php new file mode 100644 index 000000000..80c0f3561 --- /dev/null +++ b/tests/Events/ModularousUserVerificationTest.php @@ -0,0 +1,47 @@ +assertSame($request, $event->request); + } + + public function test_request_property_is_public() + { + $request = Request::create('/verify', 'GET'); + + $event = new ModularousUserVerification($request); + + $this->assertEquals('/verify', $event->request->getPathInfo()); + $this->assertEquals('GET', $event->request->getMethod()); + } + + public function test_event_instances_are_independent() + { + $eventOne = new ModularousUserVerification(Request::create('/verify', 'GET')); + $eventTwo = new ModularousUserVerification(Request::create('/verify/confirm', 'POST')); + + $this->assertEquals('/verify', $eventOne->request->getPathInfo()); + $this->assertEquals('/verify/confirm', $eventTwo->request->getPathInfo()); + $this->assertNotSame($eventOne->request, $eventTwo->request); + } + + public function test_event_uses_serializes_models_trait() + { + $traits = class_uses(ModularousUserVerification::class); + + $this->assertContains(SerializesModels::class, $traits); + } +} diff --git a/tests/Events/VerifiedEmailRegisterTest.php b/tests/Events/VerifiedEmailRegisterTest.php new file mode 100644 index 000000000..96ffb7cfb --- /dev/null +++ b/tests/Events/VerifiedEmailRegisterTest.php @@ -0,0 +1,90 @@ +makeUser(); + + $event = new VerifiedEmailRegister($user); + + $this->assertSame($user, $event->user); + } + + public function test_user_property_is_public() + { + $user = $this->makeUser(); + + $event = new VerifiedEmailRegister($user); + + $this->assertEquals($user->getAuthIdentifier(), $event->user->getAuthIdentifier()); + } + + public function test_event_instances_are_independent() + { + $userOne = $this->makeUser(); + $userTwo = $this->makeUser(); + + $eventOne = new VerifiedEmailRegister($userOne); + $eventTwo = new VerifiedEmailRegister($userTwo); + + $this->assertSame($userOne, $eventOne->user); + $this->assertSame($userTwo, $eventTwo->user); + $this->assertNotSame($eventOne->user, $eventTwo->user); + } + + public function test_event_uses_serializes_models_trait() + { + $traits = class_uses(VerifiedEmailRegister::class); + + $this->assertContains(SerializesModels::class, $traits); + } +} From 9f190776279644d0c78abfe1221bf113d551fe91 Mon Sep 17 00:00:00 2001 From: celikerde Date: Mon, 22 Jun 2026 13:13:10 +0300 Subject: [PATCH 4/7] Add unit tests for authentication handler and validation exception - Introduced tests for the authentication handler to verify behavior when a user is already authenticated, when authentication throws an exception, and when session lookup fails. - Added tests for the ValidationException class to ensure correct JSON response formatting and error handling. - Ensured comprehensive coverage of both the handler and exception functionalities to maintain code integrity. --- tests/Exceptions/HandlerTest.php | 77 ++++++++++++++++++++ tests/Exceptions/ValidationExceptionTest.php | 72 ++++++++++++++++++ 2 files changed, 149 insertions(+) create mode 100644 tests/Exceptions/ValidationExceptionTest.php diff --git a/tests/Exceptions/HandlerTest.php b/tests/Exceptions/HandlerTest.php index a5ad2081e..0bbe05220 100644 --- a/tests/Exceptions/HandlerTest.php +++ b/tests/Exceptions/HandlerTest.php @@ -5,6 +5,7 @@ use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Log; +use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\View; use Symfony\Component\HttpKernel\Exception\HttpException; use Unusualify\Modularous\Entities\User; @@ -137,6 +138,82 @@ public function test_it_checks_remember_me_cookie() $this->assertTrue(modularousBaseKey() . '::errors.404' === $result || true); } + public function test_it_returns_true_when_guard_already_authenticated() + { + // Covers the early "return true;" branch (Handler line 54): + // when the modularous guard already reports an authenticated user. + $user = User::factory()->create(); + + $this->actingAs($user, 'modularous'); + + $this->assertTrue(Auth::guard('modularous')->check()); + $this->assertTrue($this->handler->exposeAttemptModularousAuthentication()); + } + + public function test_it_returns_false_when_authentication_throws() + { + // Covers the catch block (Handler lines 100-103): force an exception + // inside the try by making the guard resolution throw. + Auth::shouldReceive('guard') + ->andThrow(new \Exception('boom')); + + Log::shouldReceive('error') + ->once() + ->with(\Mockery::pattern('/Error in attemptModularousAuthentication: boom/')); + + $this->assertFalse($this->handler->exposeAttemptModularousAuthentication()); + } + + public function test_it_logs_and_returns_null_when_session_lookup_throws() + { + // Covers the catch block (Handler line 185): write a session file with + // valid login data, then drop the users table so the User::find() + // lookup throws a QueryException inside the try. + $sessionDir = storage_path('framework/sessions'); + if (! is_dir($sessionDir)) { + mkdir($sessionDir, 0777, true); + } + + $loginKey = 'login_modularous_' . sha1(User::class); + $sessionData = serialize([$loginKey => 1]); + $sessionFile = 'test_session_throws'; + file_put_contents($sessionDir . '/' . $sessionFile, $sessionData); + + Schema::drop((new User)->getTable()); + + Log::shouldReceive('error') + ->once() + ->with(\Mockery::pattern('/Error getting user data from session:/')); + + try { + $result = $this->handler->exposeGetUserDataFromSession($sessionFile); + + $this->assertNull($result); + } finally { + unlink($sessionDir . '/' . $sessionFile); + } + } + + public function test_it_runs_middleware_pipeline_to_completion() + { + // Covers the pipeline "then" closure (Handler line 125: return $request), + // which only executes when every middleware calls $next() and the + // pipeline finishes without throwing. LanguageMiddleware reads the + // authenticated user, so we authenticate one first. + $user = User::factory()->create(); + $this->actingAs($user, 'modularous'); + + Log::spy(); + + $this->handler->exposeRunModularousMiddleware(); + + // If the pipeline reached its closure without an exception, the catch + // block's error log is never emitted... + Log::shouldNotHaveReceived('error'); + // ...and LanguageMiddleware ran, setting the locale config. + $this->assertNotNull(config(modularousBaseKey() . '.locale')); + } + public function test_it_handles_missing_user_in_session() { // 1. Mock session file with non-existent user ID diff --git a/tests/Exceptions/ValidationExceptionTest.php b/tests/Exceptions/ValidationExceptionTest.php new file mode 100644 index 000000000..90dcdfab2 --- /dev/null +++ b/tests/Exceptions/ValidationExceptionTest.php @@ -0,0 +1,72 @@ + ['The email field is required.'], + ]); + + $this->assertInstanceOf(ValidationException::class, $exception); + $this->assertSame($exception, $exception->variant()); + } + + public function test_variant_sets_json_response_with_default_variant() + { + $exception = ValidationException::withMessages([ + 'email' => ['The email field is required.'], + ]); + + $response = $exception->variant()->response; + + $this->assertInstanceOf(JsonResponse::class, $response); + $this->assertEquals(422, $response->getStatusCode()); + + $data = json_decode($response->getContent(), true); + $this->assertEquals('The email field is required.', $data['message']); + $this->assertEquals(['email' => ['The email field is required.']], $data['errors']); + $this->assertEquals('error', $data['variant']); + } + + public function test_variant_accepts_custom_variant() + { + $exception = ValidationException::withMessages([ + 'name' => ['The name field is required.'], + ]); + + $data = json_decode($exception->variant('warning')->response->getContent(), true); + + $this->assertEquals('warning', $data['variant']); + } + + public function test_variant_uses_first_error_message_as_message() + { + $exception = ValidationException::withMessages([ + 'email' => ['First email error.', 'Second email error.'], + 'name' => ['Name error.'], + ]); + + $data = json_decode($exception->variant()->response->getContent(), true); + + $this->assertEquals('First email error.', $data['message']); + } + + public function test_variant_falls_back_to_default_message_when_no_errors() + { + // No messages means errors() is empty, so summarizeErrors() hits its + // default return branch. + $exception = ValidationException::withMessages([]); + + $data = json_decode($exception->variant()->response->getContent(), true); + + $this->assertEquals(__('The given data was invalid.'), $data['message']); + $this->assertEquals([], $data['errors']); + } +} From 89aaa9f1ba05cc06e0cacc189538f472a660778e Mon Sep 17 00:00:00 2001 From: celikerde Date: Mon, 22 Jun 2026 16:15:29 +0300 Subject: [PATCH 5/7] Add unit tests for HasPriceableMutators trait - Introduced comprehensive tests for the HasPriceableMutators trait, ensuring correct behavior of price-related attributes and methods. - Verified that the trait is properly used by the model and that default values are returned when no base price is set. - Included tests for formatted attributes to ensure they utilize the PriceService correctly with currency formatting. - Ensured coverage of scenarios involving discounts and VAT calculations to maintain code integrity. --- .../Mutators/HasPriceableMutatorsTest.php | 264 ++++++++++++++++++ 1 file changed, 264 insertions(+) create mode 100644 tests/Entities/Mutators/HasPriceableMutatorsTest.php diff --git a/tests/Entities/Mutators/HasPriceableMutatorsTest.php b/tests/Entities/Mutators/HasPriceableMutatorsTest.php new file mode 100644 index 000000000..508fa6af7 --- /dev/null +++ b/tests/Entities/Mutators/HasPriceableMutatorsTest.php @@ -0,0 +1,264 @@ +id(); + $table->string('name'); + $table->timestamps(); + }); + + $this->model = new TestMutatorModel(['name' => 'Test Mutator Model']); + $this->model->save(); + + $this->currency = PriceableCurrency::create([ + 'name' => 'US Dollar', + 'iso_4217' => 'USD', + 'symbol' => '$', + ]); + + $this->vatRate = VatRate::create([ + 'name' => 'Standard VAT', + 'slug' => 'standard-vat', + 'rate' => 20.0, + ]); + + $this->priceType = PriceType::create([ + 'name' => 'Standard Price Type', + ]); + + Config::set([ + 'priceable.defaults.currencies' => $this->currency->id, + 'priceable.defaults.vat_rates' => $this->vatRate->id, + 'priceable.defaults.price_type' => $this->priceType->id, + 'priceable.prices_are_including_vat' => false, + ]); + } + + protected function createBasePrice(array $overrides = []) + { + $priceSavingKey = Price::$priceSavingKey; + + $price = $this->model->prices()->create(array_merge([ + 'role' => 'base', + $priceSavingKey => 100.00, + 'currency_id' => $this->currency->id, + 'vat_rate_id' => $this->vatRate->id, + 'price_type_id' => $this->priceType->id, + ], $overrides)); + + $this->model->refresh(); + + return $price; + } + + protected function usdFormat($amount): string + { + return PriceService::formatAmount($amount, new MoneyCurrency('USD')); + } + + public function test_trait_is_used_by_model() + { + $this->assertContains( + HasPriceableMutators::class, + class_uses_recursive($this->model) + ); + } + + public function test_initialize_has_priceable_mutators_does_not_append_attributes() + { + // The initializer currently appends nothing (body is commented out). + $this->assertEmpty($this->model->getAppends()); + } + + public function test_has_language_based_price_always_returns_false() + { + // The accessor short-circuits with an early `return false;`. + $this->assertFalse($this->model->has_language_based_price); + + $this->createBasePrice(); + + $this->assertFalse($this->model->has_language_based_price); + } + + public function test_value_attributes_return_defaults_without_base_price() + { + $this->assertNull($this->model->base_price_vat_percentage); + $this->assertFalse($this->model->base_price_has_discount); + $this->assertNull($this->model->base_price_subtotal_amount); + $this->assertNull($this->model->base_price_raw_amount); + $this->assertNull($this->model->base_price_raw_discount_amount); + $this->assertNull($this->model->base_price_discounted_raw_amount); + $this->assertNull($this->model->base_price_vat_amount); + $this->assertNull($this->model->base_price_vat_discount_amount); + $this->assertNull($this->model->base_price_discounted_vat_amount); + $this->assertNull($this->model->base_price_total_discount_amount); + $this->assertNull($this->model->base_price_total_amount); + } + + public function test_formatted_attributes_return_defaults_without_base_price() + { + $this->assertSame('', $this->model->base_price_discount_percentage_formatted); + $this->assertSame('', $this->model->base_price_vat_percentage_formatted); + $this->assertNull($this->model->base_price_subtotal_amount_formatted); + $this->assertNull($this->model->base_price_raw_amount_formatted); + $this->assertNull($this->model->base_price_discounted_raw_amount_formatted); + $this->assertNull($this->model->base_price_vat_amount_formatted); + $this->assertNull($this->model->base_price_discounted_vat_amount_formatted); + $this->assertNull($this->model->base_price_raw_discount_amount_formatted); + $this->assertNull($this->model->base_price_vat_discount_amount_formatted); + $this->assertNull($this->model->base_price_total_discount_amount_formatted); + $this->assertNull($this->model->base_price_total_amount_formatted); + $this->assertNull($this->model->base_price_formatted); + } + + public function test_value_attributes_with_base_price_and_discount() + { + $this->createBasePrice([ + 'discount_percentage' => 10.0, + ]); + + $this->assertEquals(20.0, $this->model->base_price_vat_percentage); + $this->assertTrue($this->model->base_price_has_discount); + $this->assertEquals(10000, $this->model->base_price_subtotal_amount); + $this->assertEquals(10000, $this->model->base_price_raw_amount); + $this->assertEquals(2000, $this->model->base_price_vat_amount); + $this->assertEquals(10800, $this->model->base_price_total_amount); + + $this->assertEquals(1000, $this->model->base_price_raw_discount_amount); + $this->assertEquals(9000, $this->model->base_price_discounted_raw_amount); + $this->assertEquals(200, $this->model->base_price_vat_discount_amount); + $this->assertEquals(1800, $this->model->base_price_discounted_vat_amount); + $this->assertEquals(1200, $this->model->base_price_total_discount_amount); + } + + public function test_formatted_amount_attributes_use_price_service_with_currency() + { + $this->createBasePrice([ + 'discount_percentage' => 10.0, + ]); + + $this->assertEquals( + $this->usdFormat($this->model->base_price_subtotal_amount), + $this->model->base_price_subtotal_amount_formatted + ); + $this->assertEquals( + $this->usdFormat($this->model->base_price_raw_amount), + $this->model->base_price_raw_amount_formatted + ); + $this->assertEquals( + $this->usdFormat($this->model->base_price_discounted_raw_amount), + $this->model->base_price_discounted_raw_amount_formatted + ); + $this->assertEquals( + $this->usdFormat($this->model->base_price_vat_amount), + $this->model->base_price_vat_amount_formatted + ); + $this->assertEquals( + $this->usdFormat($this->model->base_price_discounted_vat_amount), + $this->model->base_price_discounted_vat_amount_formatted + ); + $this->assertEquals( + $this->usdFormat($this->model->base_price_raw_discount_amount), + $this->model->base_price_raw_discount_amount_formatted + ); + $this->assertEquals( + $this->usdFormat($this->model->base_price_vat_discount_amount), + $this->model->base_price_vat_discount_amount_formatted + ); + $this->assertEquals( + $this->usdFormat($this->model->base_price_total_discount_amount), + $this->model->base_price_total_discount_amount_formatted + ); + $this->assertEquals( + $this->usdFormat($this->model->base_price_total_amount), + $this->model->base_price_total_amount_formatted + ); + } + + public function test_percentage_formatted_attributes_with_values() + { + $this->createBasePrice([ + 'discount_percentage' => 10.0, + ]); + + $this->assertEquals('10%', $this->model->base_price_discount_percentage_formatted); + $this->assertEquals('20%', $this->model->base_price_vat_percentage_formatted); + } + + public function test_percentage_formatted_attributes_empty_when_no_discount() + { + $this->createBasePrice([ + 'discount_percentage' => 0.0, + ]); + + $this->assertSame('', $this->model->base_price_discount_percentage_formatted); + $this->assertEquals('20%', $this->model->base_price_vat_percentage_formatted); + } + + public function test_base_price_formatted_appends_vat_suffix_by_default() + { + $this->createBasePrice(); + + $expected = $this->usdFormat($this->model->base_price_raw_amount) . ' +' . __('VAT'); + + $this->assertEquals($expected, $this->model->base_price_formatted); + $this->assertStringContainsString('+' . __('VAT'), $this->model->base_price_formatted); + } + + public function test_base_price_formatted_omits_vat_suffix_when_prices_include_vat() + { + Config::set('priceable.prices_are_including_vat', true); + + $this->createBasePrice(); + + $expected = $this->usdFormat($this->model->base_price_raw_amount); + + $this->assertEquals($expected, $this->model->base_price_formatted); + $this->assertStringNotContainsString('+' . __('VAT'), $this->model->base_price_formatted); + } +} + +class TestMutatorModel extends Model +{ + use HasPriceable; + + public static $mutateHasPriceable = true; + + protected $table = 'test_mutator_models'; + + protected $fillable = ['name']; + + public static $priceSavingKey = 'price_value'; +} From c60c868750af396e387364f2b992ea28ef318a49 Mon Sep 17 00:00:00 2001 From: celikerde Date: Mon, 22 Jun 2026 16:15:39 +0300 Subject: [PATCH 6/7] Add unit tests for CacheObserver functionality - Introduced tests for the CacheObserver to ensure cache invalidation occurs correctly on model events: created, updated, deleted, restored, and forceDeleted. - Verified that caching behavior is properly handled when caching is enabled for the model's module. - Ensured comprehensive coverage of cache invalidation scenarios to maintain code integrity. --- .../Entities/Observers/CacheObserverTest.php | 122 ++++++++++++++++++ 1 file changed, 122 insertions(+) diff --git a/tests/Entities/Observers/CacheObserverTest.php b/tests/Entities/Observers/CacheObserverTest.php index 255f961df..7b4f78267 100644 --- a/tests/Entities/Observers/CacheObserverTest.php +++ b/tests/Entities/Observers/CacheObserverTest.php @@ -5,6 +5,8 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Facades\Config; use Unusualify\Modularous\Entities\Observers\CacheObserver; +use Unusualify\Modularous\Facades\ModularousCache; +use Unusualify\Modularous\Facades\RelationshipGraph; use Unusualify\Modularous\Tests\TestCase; class CacheObserverTest extends TestCase @@ -65,6 +67,126 @@ public function test_force_deleted_does_not_throw_when_cache_disabled() $this->assertTrue(true); } + public function test_created_invalidates_model_cache_when_enabled() + { + // Covers CacheObserver lines 37-39: when caching is enabled for the + // model's module, created() must invalidate the model's cache. + $model = $this->createTestModel(); + + // getCacheDependents() (used by shouldInvalidate) touches the graph. + RelationshipGraph::shouldReceive('getAffectedModuleRoutes')->andReturn([]); + RelationshipGraph::shouldReceive('getAffectedModuleRoutesByTable')->andReturn([]); + + // Enabled both globally (no args) and for the module/route (two args). + ModularousCache::shouldReceive('isEnabled')->andReturn(true); + // Granular invalidation "succeeds" so the dependent-module fallback is skipped. + ModularousCache::shouldReceive('invalidateByRelatedModel')->andReturn(true); + + // The assertion: line 38 runs exactly once. + ModularousCache::shouldReceive('invalidateForModel')->once(); + + $this->observer->created($model); + } + + public function test_updated_invalidates_refreshed_model_cache_when_enabled() + { + // Covers CacheObserver lines 59-63: when caching is enabled for the + // model's module, updated() clones + refreshes the model and invalidates + // the clone's cache. refresh() is a no-op here to avoid a DB round-trip. + $model = new class extends Model + { + protected $table = 'test_models'; + + public function getKey() + { + return 1; + } + + public function refresh() + { + return $this; + } + }; + $model->setRawAttributes(['id' => 1]); + + // getCacheDependents() (used by shouldInvalidate) touches the graph. + RelationshipGraph::shouldReceive('getAffectedModuleRoutes')->andReturn([]); + RelationshipGraph::shouldReceive('getAffectedModuleRoutesByTable')->andReturn([]); + + // Enabled both globally (no args) and for the module/route (two args). + ModularousCache::shouldReceive('isEnabled')->andReturn(true); + // Granular invalidation "succeeds" so the dependent-module fallback is skipped. + ModularousCache::shouldReceive('invalidateByRelatedModel')->andReturn(true); + + // The assertion: line 62 runs exactly once (on the refreshed clone). + ModularousCache::shouldReceive('invalidateForModel')->once(); + + $this->observer->updated($model); + } + + public function test_deleted_invalidates_model_cache_when_enabled() + { + // Covers CacheObserver lines 84-86: when caching is enabled for the + // model's module, deleted() must invalidate the model's cache. + $model = $this->createTestModel(); + + // getCacheDependents() (used by shouldInvalidate) touches the graph. + RelationshipGraph::shouldReceive('getAffectedModuleRoutes')->andReturn([]); + RelationshipGraph::shouldReceive('getAffectedModuleRoutesByTable')->andReturn([]); + + // Enabled both globally (no args) and for the module/route (two args). + ModularousCache::shouldReceive('isEnabled')->andReturn(true); + // Granular invalidation "succeeds" so the dependent-module fallback is skipped. + ModularousCache::shouldReceive('invalidateByRelatedModel')->andReturn(true); + + // The assertion: line 85 runs exactly once. + ModularousCache::shouldReceive('invalidateForModel')->once(); + + $this->observer->deleted($model); + } + + public function test_restored_invalidates_model_cache_when_enabled() + { + // Covers CacheObserver lines 106-108: when caching is enabled for the + // model's module, restored() must invalidate the model's cache. + $model = $this->createTestModel(); + + // getCacheDependents() (used by shouldInvalidate) touches the graph. + RelationshipGraph::shouldReceive('getAffectedModuleRoutes')->andReturn([]); + RelationshipGraph::shouldReceive('getAffectedModuleRoutesByTable')->andReturn([]); + + // Enabled both globally (no args) and for the module/route (two args). + ModularousCache::shouldReceive('isEnabled')->andReturn(true); + // Granular invalidation "succeeds" so the dependent-module fallback is skipped. + ModularousCache::shouldReceive('invalidateByRelatedModel')->andReturn(true); + + // The assertion: line 107 runs exactly once. + ModularousCache::shouldReceive('invalidateForModel')->once(); + + $this->observer->restored($model); + } + + public function test_force_deleted_invalidates_model_cache_when_enabled() + { + // Covers CacheObserver lines 125-127: when caching is enabled for the + // model's module, forceDeleted() must invalidate the model's cache. + $model = $this->createTestModel(); + + // getCacheDependents() (used by shouldInvalidate) touches the graph. + RelationshipGraph::shouldReceive('getAffectedModuleRoutes')->andReturn([]); + RelationshipGraph::shouldReceive('getAffectedModuleRoutesByTable')->andReturn([]); + + // Enabled both globally (no args) and for the module/route (two args). + ModularousCache::shouldReceive('isEnabled')->andReturn(true); + // Granular invalidation "succeeds" so the dependent-module fallback is skipped. + ModularousCache::shouldReceive('invalidateByRelatedModel')->andReturn(true); + + // The assertion: line 126 runs exactly once. + ModularousCache::shouldReceive('invalidateForModel')->once(); + + $this->observer->forceDeleted($model); + } + private function createTestModel(): Model { $model = new class extends Model From 86e1733474b1058a5d78d28e7c9d903075f979ab Mon Sep 17 00:00:00 2001 From: celikerde Date: Mon, 22 Jun 2026 16:15:49 +0300 Subject: [PATCH 7/7] Add unit tests for PriceableObserver functionality - Introduced comprehensive tests for the PriceableObserver to validate the correct handling of price calculations, including raw amounts and VAT amounts based on configuration settings for including or excluding VAT. - Verified behavior for various scenarios, including zero price values and discount percentages, ensuring accurate attribute management during model events. - Ensured thorough coverage of the saving, retrieved, and replicating methods to maintain code integrity. --- .../Observers/PriceableObserverTest.php | 154 ++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 tests/Entities/Observers/PriceableObserverTest.php diff --git a/tests/Entities/Observers/PriceableObserverTest.php b/tests/Entities/Observers/PriceableObserverTest.php new file mode 100644 index 000000000..d232c89f2 --- /dev/null +++ b/tests/Entities/Observers/PriceableObserverTest.php @@ -0,0 +1,154 @@ +observer = new PriceableObserver; + + Config::set('priceable.prices_are_including_vat', false); + } + + /** + * Build an unsaved Price with the given price_value and VAT rate. + * The vatRate relation is injected so saving() never hits the database. + */ + protected function makePrice(float $priceValue, float $vatRate, float $discountPercentage = 0): Price + { + $price = new Price; + $price->setRawAttributes([ + Price::$priceSavingKey => $priceValue, + 'discount_percentage' => $discountPercentage, + ]); + $price->setRelation('vatRate', new VatRate(['rate' => $vatRate])); + + return $price; + } + + // ------------------------------------------------------------------------- + // saving() + // ------------------------------------------------------------------------- + + public function test_saving_sets_raw_and_vat_amounts_when_excluding_vat() + { + // $price_value 100 → raw_amount 10000 cents; 20% VAT on top. + $price = $this->makePrice(100, 20.0); + + $this->observer->saving($price); + + $this->assertEquals(10000, $price->raw_amount); + $this->assertEquals(2000, $price->vat_amount); // 10000 * 0.20 + } + + public function test_saving_sets_raw_and_vat_amounts_when_including_vat() + { + Config::set('priceable.prices_are_including_vat', true); + + // $price_value 120 → newRawValue 12000 cents (gross incl. 20% VAT). + // raw_amount = 12000 / 1.20 = 10000; vat_amount = 12000 - 10000 = 2000. + $price = $this->makePrice(120, 20.0); + + $this->observer->saving($price); + + $this->assertEquals(10000, $price->raw_amount); + $this->assertEquals(2000, $price->vat_amount); + } + + public function test_saving_with_zero_price_value_produces_zero_amounts() + { + // Price constructor always injects price_value => 0.00 via defaultAttributes(), + // so isset($price->price_value) is always true. A zero price_value correctly + // results in raw_amount = 0 and vat_amount = 0. + $price = new Price; + $price->setRelation('vatRate', new VatRate(['rate' => 20.0])); + + $this->observer->saving($price); + + $this->assertEquals(0, $price->raw_amount); + $this->assertEquals(0, $price->vat_amount); + } + + public function test_saving_unsets_price_saving_key_attribute() + { + $price = $this->makePrice(100, 20.0); + + $this->observer->saving($price); + + $this->assertFalse($price->offsetExists(Price::$priceSavingKey)); + } + + public function test_saving_defaults_discount_percentage_to_zero_when_null() + { + $price = new Price; + $price->setRawAttributes([Price::$priceSavingKey => 100]); + $price->setRelation('vatRate', new VatRate(['rate' => 20.0])); + + $this->observer->saving($price); + + $this->assertEquals(0, $price->discount_percentage); + } + + public function test_saving_with_zero_vat_rate() + { + $price = $this->makePrice(50, 0.0); + + $this->observer->saving($price); + + $this->assertEquals(5000, $price->raw_amount); + $this->assertEquals(0, $price->vat_amount); + } + + // ------------------------------------------------------------------------- + // retrieved() + // ------------------------------------------------------------------------- + + public function test_retrieved_sets_price_saving_key_from_raw_amount_when_excluding_vat() + { + // Excluding VAT: price_value = raw_amount / 100. + $price = new Price; + $price->setRawAttributes(['raw_amount' => 10000, 'vat_amount' => 2000]); + + $this->observer->retrieved($price); + + $this->assertEquals(100.0, $price->getAttribute(Price::$priceSavingKey)); + } + + public function test_retrieved_sets_price_saving_key_from_total_amount_when_including_vat() + { + Config::set('priceable.prices_are_including_vat', true); + + // Including VAT: price_value = (raw_amount + vat_amount) / 100. + $price = new Price; + $price->setRawAttributes(['raw_amount' => 10000, 'vat_amount' => 2000]); + + $this->observer->retrieved($price); + + $this->assertEquals(120.0, $price->getAttribute(Price::$priceSavingKey)); + } + + // ------------------------------------------------------------------------- + // replicating() + // ------------------------------------------------------------------------- + + public function test_replicating_unsets_price_saving_key() + { + $price = new Price; + $price->setRawAttributes([Price::$priceSavingKey => 100]); + + $this->observer->replicating($price); + + $this->assertFalse($price->offsetExists(Price::$priceSavingKey)); + } +}