From e47487da5381f667c673710aa7ece65802d0553d Mon Sep 17 00:00:00 2001 From: Nasrul Hazim Bin Mohamad Date: Fri, 1 Nov 2024 00:20:41 +0800 Subject: [PATCH] Update Resource Action Test --- .phpunit.cache/test-results | 2 +- src/ResourceAction.php | 50 ++------ tests/ResourceActionTest.php | 113 +++++++++++++++--- tests/Stubs/Models/User.php | 2 +- .../migrations/create_users_table.php | 1 + 5 files changed, 106 insertions(+), 62 deletions(-) diff --git a/.phpunit.cache/test-results b/.phpunit.cache/test-results index f11f773..2be98c4 100644 --- a/.phpunit.cache/test-results +++ b/.phpunit.cache/test-results @@ -1 +1 @@ -{"version":"pest_2.34.4","defects":[],"times":{"P\\Tests\\LaravelActionTest::__pest_evaluable_it_can_make_a_menu_action":0.002,"P\\Tests\\LaravelActionTest::__pest_evaluable_it_can_make_an_API_action":0.001,"P\\Tests\\LaravelActionTest::__pest_evaluable_it_can_make_an_action_without_model_option":0.002,"P\\Tests\\LaravelActionTest::__pest_evaluable_it_has_make_action_command":0.004,"P\\Tests\\LaravelActionTest::__pest_evaluable_it_can_make_an_action_with_model_option":0.001,"P\\Tests\\AbstractActionTest::__pest_evaluable_it_creates_a_user_with_valid_data":0.136,"P\\Tests\\AbstractActionTest::__pest_evaluable_it_applies_hashing_to_password_field":0.141,"P\\Tests\\AbstractActionTest::__pest_evaluable_it_uses_transactions_during_execution":0.099,"P\\Tests\\AbstractActionTest::__pest_evaluable_it_removes_confirmation_fields_from_inputs":0.003,"P\\Tests\\AbstractActionTest::__pest_evaluable_it_validates_required_fields":0.001,"P\\Tests\\AbstractActionTest::__pest_evaluable_it_throws_exception_if_model_is_not_set":0.066,"P\\Tests\\AbstractActionTest::__pest_evaluable_it_applies_encryption_to_specified_fields":0.069,"P\\Tests\\ResourceActionTest::__pest_evaluable_it_creates_a_user_with_valid_data":0.124,"P\\Tests\\ResourceActionTest::__pest_evaluable_it_applies_encryption_to_specified_fields":0.063,"P\\Tests\\ResourceActionTest::__pest_evaluable_it_uses_transactions_during_execution":0.093,"P\\Tests\\ResourceActionTest::__pest_evaluable_it_validates_required_fields":0.001,"P\\Tests\\ResourceActionTest::__pest_evaluable_it_throws_exception_if_model_is_not_set":0.062,"P\\Tests\\ResourceActionTest::__pest_evaluable_it_applies_hashing_to_password_field":0.122,"P\\Tests\\ResourceActionTest::__pest_evaluable_it_removes_confirmation_fields_from_inputs":0.001}} \ No newline at end of file +{"version":"pest_2.34.4","defects":{"P\\Tests\\ResourceActionTest::__pest_evaluable_it_fails_to_apply_transformation_if_field_is_missing":8},"times":{"P\\Tests\\LaravelActionTest::__pest_evaluable_it_can_make_a_menu_action":0.001,"P\\Tests\\LaravelActionTest::__pest_evaluable_it_can_make_an_API_action":0.046,"P\\Tests\\LaravelActionTest::__pest_evaluable_it_can_make_an_action_without_model_option":0.001,"P\\Tests\\LaravelActionTest::__pest_evaluable_it_has_make_action_command":0.004,"P\\Tests\\LaravelActionTest::__pest_evaluable_it_can_make_an_action_with_model_option":0.001,"P\\Tests\\AbstractActionTest::__pest_evaluable_it_creates_a_user_with_valid_data":0.136,"P\\Tests\\AbstractActionTest::__pest_evaluable_it_applies_hashing_to_password_field":0.141,"P\\Tests\\AbstractActionTest::__pest_evaluable_it_uses_transactions_during_execution":0.099,"P\\Tests\\AbstractActionTest::__pest_evaluable_it_removes_confirmation_fields_from_inputs":0.003,"P\\Tests\\AbstractActionTest::__pest_evaluable_it_validates_required_fields":0.001,"P\\Tests\\AbstractActionTest::__pest_evaluable_it_throws_exception_if_model_is_not_set":0.066,"P\\Tests\\AbstractActionTest::__pest_evaluable_it_applies_encryption_to_specified_fields":0.069,"P\\Tests\\ResourceActionTest::__pest_evaluable_it_creates_a_user_with_valid_data":0.121,"P\\Tests\\ResourceActionTest::__pest_evaluable_it_applies_encryption_to_specified_fields":0.063,"P\\Tests\\ResourceActionTest::__pest_evaluable_it_uses_transactions_during_execution":0.069,"P\\Tests\\ResourceActionTest::__pest_evaluable_it_validates_required_fields":0.002,"P\\Tests\\ResourceActionTest::__pest_evaluable_it_throws_exception_if_model_is_not_set":0.062,"P\\Tests\\ResourceActionTest::__pest_evaluable_it_applies_hashing_to_password_field":0.123,"P\\Tests\\ResourceActionTest::__pest_evaluable_it_removes_confirmation_fields_from_inputs":0.001,"P\\Tests\\ResourceActionTest::__pest_evaluable_it_applies_both_hashing_and_encryption_to_specified_fields":0.145,"P\\Tests\\ResourceActionTest::__pest_evaluable_it_handles_multiple_fields_for_hashing_and_encryption":0.241,"P\\Tests\\ResourceActionTest::__pest_evaluable_it_fails_to_apply_transformation_if_field_is_missing":0.001,"P\\Tests\\ResourceActionTest::__pest_evaluable_it_applies_constraints_for_update_or_create":0.182,"P\\Tests\\ResourceActionTest::__pest_evaluable_it_does_not_apply_transformation_if_optional_field_is_missing":0.001}} \ No newline at end of file diff --git a/src/ResourceAction.php b/src/ResourceAction.php index 929157a..96e5f5e 100644 --- a/src/ResourceAction.php +++ b/src/ResourceAction.php @@ -7,57 +7,43 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Hash; -use Illuminate\Support\Str; use Illuminate\Support\Facades\Validator; +use Illuminate\Support\Str; abstract class ResourceAction implements Execute { /** * The model class the action operates on. - * - * @var string */ protected string $model; /** * Input data for the action. - * - * @var array */ protected array $inputs; /** * Fields to use for constraint-based operations. - * - * @var array */ protected array $constrainedBy = []; /** * Fields to hash before saving. - * - * @var array */ protected array $hashFields = []; /** * Fields to encrypt before saving. - * - * @var array */ protected array $encryptFields = []; /** * The Eloquent model record. - * - * @var Model */ protected Model $record; /** * Constructor to initialize input data. - * - * @param array $inputs */ public function __construct(array $inputs = []) { @@ -66,33 +52,29 @@ public function __construct(array $inputs = []) /** * Abstract method to define validation rules for the action. - * - * @return array */ abstract public function rules(): array; /** * Generic setter for properties. * - * @param string $property - * @param array $value * @return $this + * * @throws ActionException */ public function setProperty(string $property, array $value): self { - if (!property_exists($this, $property)) { + if (! property_exists($this, $property)) { throw new ActionException("Property {$property} does not exist."); } $this->{$property} = $value; + return $this; } /** * Retrieve the current record. - * - * @return Model */ public function getRecord(): Model { @@ -101,8 +83,6 @@ public function getRecord(): Model /** * Retrieve the inputs. - * - * @return array */ public function inputs(): array { @@ -111,31 +91,23 @@ public function inputs(): array /** * Hash specified fields in the inputs or constraints. - * - * @return void */ protected function transformFields(): void { - $this->applyTransformationOnFields($this->hashFields, fn($value) => Hash::make($value)); - $this->applyTransformationOnFields($this->encryptFields, fn($value) => encrypt($value)); + $this->applyTransformationOnFields($this->hashFields, fn ($value) => Hash::make($value)); + $this->applyTransformationOnFields($this->encryptFields, fn ($value) => encrypt($value)); } /** * Remove confirmation fields from inputs. - * - * @return void */ public function removeConfirmationFields(): void { - $this->inputs = array_filter($this->inputs, fn($value, $key) => !Str::contains($key, '_confirmation'), ARRAY_FILTER_USE_BOTH); + $this->inputs = array_filter($this->inputs, fn ($value, $key) => ! Str::contains($key, '_confirmation'), ARRAY_FILTER_USE_BOTH); } /** * Apply transformation to specified fields in inputs and constraints. - * - * @param array $fields - * @param callable $transformation - * @return void */ protected function applyTransformationOnFields(array $fields, callable $transformation): void { @@ -153,7 +125,6 @@ protected function applyTransformationOnFields(array $fields, callable $transfor * Retrieve the model class for the action. * * @throws ActionException - * @return string */ public function model(): string { @@ -170,8 +141,6 @@ public function model(): string /** * Preparation method for the action. - * - * @return void */ public function prepare(): void { @@ -182,7 +151,6 @@ public function prepare(): void * Validates the inputs against the defined rules. * * @throws \Illuminate\Validation\ValidationException - * @return void */ protected function validateInputs(): void { @@ -192,11 +160,9 @@ protected function validateInputs(): void )->validate(); } - /** * Execute the action with preparation, validation, and data processing. * - * @return Model * @throws \Illuminate\Validation\ValidationException */ public function execute(): Model @@ -207,7 +173,7 @@ public function execute(): Model $this->removeConfirmationFields(); return $this->record = DB::transaction(function () { - return !empty($this->constrainedBy) + return ! empty($this->constrainedBy) ? $this->model()::updateOrCreate($this->constrainedBy, $this->inputs) : $this->model()::create($this->inputs); }); diff --git a/tests/ResourceActionTest.php b/tests/ResourceActionTest.php index aa33c57..574592f 100644 --- a/tests/ResourceActionTest.php +++ b/tests/ResourceActionTest.php @@ -54,7 +54,8 @@ ]; // Stub a class without a model definition - $stubAction = new class($inputs) extends CreateUserAction { + $stubAction = new class($inputs) extends CreateUserAction + { protected string $model = ''; // Intentionally leave model empty }; @@ -81,6 +82,84 @@ expect(Hash::check('secretpassword', $record->password))->toBeTrue(); }); +// it applies both hashing and encryption to different fields +it('applies both hashing and encryption to specified fields', function () { + // Arrange + $inputs = [ + 'name' => 'John Doe', + 'email' => 'johndoe@example.com', + 'password' => 'secretpassword', + 'ssn' => '123-45-6789', + ]; + + $action = new CreateUserAction($inputs); + $action->setProperty('hashFields', ['password']); + $action->setProperty('encryptFields', ['ssn']); + + // Act + $record = $action->execute(); + + // Assert + expect(Hash::check('secretpassword', $record->password))->toBeTrue(); + expect(decrypt($record->ssn))->toBe('123-45-6789'); +}); + +// it handles multiple fields for hashing and encryption +it('handles multiple fields for hashing and encryption', function () { + // Arrange + $inputs = [ + 'name' => 'Jane Doe', + 'email' => 'janedoe@example.com', + 'password' => 'anotherpassword', + 'ssn' => '987-65-4321', + 'security_answer' => 'My first car', + ]; + + $action = new CreateUserAction($inputs); + $action->setProperty('hashFields', ['password', 'security_answer']); + $action->setProperty('encryptFields', ['ssn', 'email']); + + // Act + $record = $action->execute(); + + // Assert + // Only use Hash::check on fields known to be hashed + expect(Hash::check('anotherpassword', $record->password))->toBeTrue(); + expect(Hash::check('My first car', $record->security_answer))->toBeTrue(); + + // Decrypt and verify the encrypted fields + expect(decrypt($record->ssn))->toBe('987-65-4321'); + expect(decrypt($record->email))->toBe('janedoe@example.com'); +}); + +// it applies constraints for update or create +it('applies constraints for update or create', function () { + // Arrange + $inputs = [ + 'name' => 'John Doe Updated', + 'email' => 'uniqueemail@example.com', // Use a unique email to avoid the validation error + 'password' => 'newpassword', + ]; + + // Pre-create a user with a different email to simulate an existing record + $existingUser = User::create([ + 'name' => 'Old Name', + 'email' => 'oldemail@example.com', // Different email to avoid triggering unique constraint + 'password' => Hash::make('oldpassword'), + ]); + + // Now set the constraints to match the unique constraint check + $action = new CreateUserAction($inputs); + $action->setProperty('constrainedBy', ['id' => $existingUser->id]); // Use ID as a unique constraint for update + + // Act + $record = $action->execute(); + + // Assert + expect($record->name)->toBe('John Doe Updated') + ->and(Hash::check('newpassword', $record->password))->toBeTrue(); +}); + // it removes confirmation fields from inputs it('removes confirmation fields from inputs', function () { // Arrange @@ -116,19 +195,21 @@ $mockConnection = Mockery::mock(); $mockQueryBuilder = Mockery::mock(); - // Mock the chain of methods on the query builder + // Set expectation for DB::connection() + DB::shouldReceive('connection')->once()->andReturn($mockConnection); + + // Set up transaction mock and method chaining on the query builder + DB::shouldReceive('transaction')->once()->andReturnUsing(function ($callback) { + return $callback(); + }); + + // Set up expectations for methods on the query builder $mockConnection->shouldReceive('table')->andReturn($mockQueryBuilder); $mockQueryBuilder->shouldReceive('useWritePdo')->andReturn($mockQueryBuilder); $mockQueryBuilder->shouldReceive('where')->andReturn($mockQueryBuilder); $mockQueryBuilder->shouldReceive('count')->andReturn(0); $mockQueryBuilder->shouldReceive('updateOrCreate')->andReturn(Mockery::mock(User::class)); - // Mock the transaction flow - DB::shouldReceive('connection')->andReturn($mockConnection); - DB::shouldReceive('transaction')->once()->andReturnUsing(function ($callback) { - return $callback(); - }); - // Act $record = $action->execute(); @@ -136,26 +217,22 @@ expect($record)->toBeInstanceOf(User::class); }); -// it applies encryption to specified fields -it('applies encryption to specified fields', function () { +// it does not apply transformation if optional field is missing +it('does not apply transformation if optional field is missing', function () { // Arrange $inputs = [ 'name' => 'John Doe', 'email' => 'johndoe@example.com', - 'password' => 'secretpassword', // password will be hashed - 'ssn' => '123-45-6789', // This is the field to be encrypted + 'password' => 'secretpassword', // Required field to satisfy validation ]; $action = new CreateUserAction($inputs); - $action->setProperty('encryptFields', ['ssn']); // Use setProperty to define encryption fields + $action->setProperty('hashFields', ['security_answer']); // 'security_answer' is not present in inputs // Act $record = $action->execute(); // Assert - expect($record)->toBeInstanceOf(User::class); - - // Ensure 'ssn' field was encrypted - $decryptedSSN = decrypt($record->ssn); - expect($decryptedSSN)->toBe('123-45-6789'); + expect($record)->toBeInstanceOf(User::class); // Check the action executed successfully + expect($action->inputs())->not->toHaveKey('security_answer'); // Ensure no transformation occurred on missing field }); diff --git a/tests/Stubs/Models/User.php b/tests/Stubs/Models/User.php index 18c1518..fbc5351 100644 --- a/tests/Stubs/Models/User.php +++ b/tests/Stubs/Models/User.php @@ -12,7 +12,7 @@ class User extends Authenticatable * @var array */ protected $fillable = [ - 'name', 'email', 'password', 'ssn', + 'name', 'email', 'password', 'ssn', 'security_answer', ]; /** diff --git a/tests/database/migrations/create_users_table.php b/tests/database/migrations/create_users_table.php index 1ece402..f8a7e5a 100644 --- a/tests/database/migrations/create_users_table.php +++ b/tests/database/migrations/create_users_table.php @@ -19,6 +19,7 @@ public function up() $table->string('email')->unique(); $table->timestamp('email_verified_at')->nullable(); $table->string('password'); + $table->string('security_answer')->nullable(); $table->rememberToken(); $table->string('ssn')->nullable(); // Adding an 'ssn' field for testing encryption $table->timestamps();