Skip to content

Commit

Permalink
Update Resource Action Test
Browse files Browse the repository at this point in the history
  • Loading branch information
nasrulhazim committed Oct 31, 2024
1 parent de5b305 commit e47487d
Show file tree
Hide file tree
Showing 5 changed files with 106 additions and 62 deletions.
2 changes: 1 addition & 1 deletion .phpunit.cache/test-results
Original file line number Diff line number Diff line change
@@ -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}}
{"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}}
50 changes: 8 additions & 42 deletions src/ResourceAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [])
{
Expand All @@ -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
{
Expand All @@ -101,8 +83,6 @@ public function getRecord(): Model

/**
* Retrieve the inputs.
*
* @return array
*/
public function inputs(): array
{
Expand All @@ -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
{
Expand All @@ -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
{
Expand All @@ -170,8 +141,6 @@ public function model(): string

/**
* Preparation method for the action.
*
* @return void
*/
public function prepare(): void
{
Expand All @@ -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
{
Expand All @@ -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
Expand All @@ -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);
});
Expand Down
113 changes: 95 additions & 18 deletions tests/ResourceActionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
};

Expand All @@ -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' => '[email protected]',
'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' => '[email protected]',
'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('[email protected]');
});

// it applies constraints for update or create
it('applies constraints for update or create', function () {
// Arrange
$inputs = [
'name' => 'John Doe Updated',
'email' => '[email protected]', // 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' => '[email protected]', // 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
Expand Down Expand Up @@ -116,46 +195,44 @@
$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();

// Assert
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' => '[email protected]',
'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
});
2 changes: 1 addition & 1 deletion tests/Stubs/Models/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class User extends Authenticatable
* @var array
*/
protected $fillable = [
'name', 'email', 'password', 'ssn',
'name', 'email', 'password', 'ssn', 'security_answer',
];

/**
Expand Down
1 change: 1 addition & 0 deletions tests/database/migrations/create_users_table.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down

0 comments on commit e47487d

Please sign in to comment.