From c72fa247c75f2f059ff44c8887cf325f952a5367 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 10 Jul 2023 13:00:27 +0900 Subject: [PATCH] fix: setValidationRule() cannot use with ruleGroup --- system/BaseModel.php | 19 +- system/Validation/Validation.php | 4 +- tests/_support/Config/Validation.php | 32 ++ tests/_support/Models/ValidModelRuleGroup.php | 27 + .../Models/ValidationModelRuleGroupTest.php | 476 ++++++++++++++++++ 5 files changed, 553 insertions(+), 5 deletions(-) create mode 100644 tests/_support/Config/Validation.php create mode 100644 tests/_support/Models/ValidModelRuleGroup.php create mode 100644 tests/system/Models/ValidationModelRuleGroupTest.php diff --git a/system/BaseModel.php b/system/BaseModel.php index 06b8190f037f..0f316e6ffa3f 100644 --- a/system/BaseModel.php +++ b/system/BaseModel.php @@ -1345,7 +1345,7 @@ public function skipValidation(bool $skip = true) } /** - * Allows to set validation messages. + * Allows to set (and reset) validation messages. * It could be used when you have to change default or override current validate messages. * * @param array $validationMessages Value @@ -1376,7 +1376,7 @@ public function setValidationMessage(string $field, array $fieldMessages) } /** - * Allows to set validation rules. + * Allows to set (and reset) validation rules. * It could be used when you have to change default or override current validate rules. * * @param array $validationRules Value @@ -1401,6 +1401,17 @@ public function setValidationRules(array $validationRules) */ public function setValidationRule(string $field, $fieldRules) { + $rules = $this->validationRules; + + // ValidationRules can be either a string, which is the group name, + // or an array of rules. + if (is_string($rules)) { + [$rules, $customErrors] = $this->validation->loadRuleGroup($rules); + + $this->validationRules = $rules; + $this->validationMessages = $this->validationMessages + $customErrors; + } + $this->validationRules[$field] = $fieldRules; return $this; @@ -1466,7 +1477,9 @@ public function getValidationRules(array $options = []): array // ValidationRules can be either a string, which is the group name, // or an array of rules. if (is_string($rules)) { - $rules = $this->validation->loadRuleGroup($rules); + [$rules, $customErrors] = $this->validation->loadRuleGroup($rules); + + $this->validationMessages = $this->validationMessages + $customErrors; } if (isset($options['except'])) { diff --git a/system/Validation/Validation.php b/system/Validation/Validation.php index cbfd396f325f..91e441356112 100644 --- a/system/Validation/Validation.php +++ b/system/Validation/Validation.php @@ -665,7 +665,7 @@ protected function loadRuleSets() * same format used with setRules(). Additionally, check * for {group}_errors for an array of custom error messages. * - * @return array + * @return array [rules, customErrors] * * @throws ValidationException */ @@ -693,7 +693,7 @@ public function loadRuleGroup(?string $group = null) $this->customErrors = $this->config->{$errorName}; } - return $this->rules; + return [$this->rules, $this->customErrors]; } /** diff --git a/tests/_support/Config/Validation.php b/tests/_support/Config/Validation.php new file mode 100644 index 000000000000..48be09084502 --- /dev/null +++ b/tests/_support/Config/Validation.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Tests\Support\Config; + +use Config\Validation as ValidationConfig; + +class Validation extends ValidationConfig +{ + public $signup = [ + 'id' => 'permit_empty|is_natural_no_zero', + 'name' => [ + 'required', + 'min_length[3]', + ], + 'token' => 'permit_empty|in_list[{id}]', + ]; + public $signup_errors = [ + 'name' => [ + 'required' => 'You forgot to name the baby.', + 'min_length' => 'Too short, man!', + ], + ]; +} diff --git a/tests/_support/Models/ValidModelRuleGroup.php b/tests/_support/Models/ValidModelRuleGroup.php new file mode 100644 index 000000000000..8b751b5f2409 --- /dev/null +++ b/tests/_support/Models/ValidModelRuleGroup.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Tests\Support\Models; + +use CodeIgniter\Model; + +class ValidModelRuleGroup extends Model +{ + protected $table = 'job'; + protected $returnType = 'object'; + protected $useSoftDeletes = false; + protected $dateFormat = 'int'; + protected $allowedFields = [ + 'name', + 'description', + ]; + protected $validationRules = 'signup'; +} diff --git a/tests/system/Models/ValidationModelRuleGroupTest.php b/tests/system/Models/ValidationModelRuleGroupTest.php new file mode 100644 index 000000000000..f25dfbbf9735 --- /dev/null +++ b/tests/system/Models/ValidationModelRuleGroupTest.php @@ -0,0 +1,476 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Models; + +use CodeIgniter\Database\BaseConnection; +use CodeIgniter\Model; +use Config\Services; +use stdClass; +use Tests\Support\Config\Validation; +use Tests\Support\Models\JobModel; +use Tests\Support\Models\SimpleEntity; +use Tests\Support\Models\ValidErrorsModel; +use Tests\Support\Models\ValidModelRuleGroup; + +/** + * @group DatabaseLive + * + * @internal + */ +final class ValidationModelRuleGroupTest extends LiveModelTestCase +{ + protected function setUp(): void + { + parent::setUp(); + + $this->createModel(ValidModelRuleGroup::class); + } + + protected function createModel(string $modelName, ?BaseConnection $db = null): Model + { + $config = new Validation(); + $validation = new \CodeIgniter\Validation\Validation($config, Services::renderer()); + + $this->db = $db ?? $this->db; + $this->model = new $modelName($this->db, $validation); + + return $this->model; + } + + public function testValid(): void + { + $data = [ + 'name' => 'some name', + 'description' => 'some great marketing stuff', + ]; + + $this->assertIsInt($this->model->insert($data)); + + $errors = $this->model->errors(); + $this->assertSame([], $errors); + } + + public function testValidationBasics(): void + { + $data = [ + 'name' => null, + 'description' => 'some great marketing stuff', + ]; + + $this->assertFalse($this->model->insert($data)); + + $errors = $this->model->errors(); + $this->assertSame('You forgot to name the baby.', $errors['name']); + } + + /** + * @see https://github.com/codeigniter4/CodeIgniter4/issues/5859 + */ + public function testValidationTwice(): void + { + $data = [ + 'name' => null, + 'description' => 'some great marketing stuff', + ]; + + $this->assertFalse($this->model->insert($data)); + + $errors = $this->model->errors(); + $this->assertSame('You forgot to name the baby.', $errors['name']); + + $data = [ + 'name' => 'some name', + 'description' => 'some great marketing stuff', + ]; + + $this->assertIsInt($this->model->insert($data)); + } + + public function testValidationWithSetValidationRule(): void + { + $data = [ + 'name' => 'some name', + 'description' => 'some great marketing stuff', + ]; + + $this->model->setValidationRule('description', [ + 'rules' => 'required|min_length[50]', + 'errors' => [ + 'min_length' => 'Description is too short baby.', + ], + ]); + $this->assertFalse($this->model->insert($data)); + + $errors = $this->model->errors(); + $this->assertSame('Description is too short baby.', $errors['description']); + } + + public function testValidationWithSetValidationRules(): void + { + $data = [ + 'name' => '', + 'description' => 'some great marketing stuff', + ]; + + $this->model->setValidationRules([ + 'name' => [ + 'rules' => 'required', + 'errors' => [ + 'required' => 'Give me a name baby.', + ], + ], + 'description' => [ + 'rules' => 'required|min_length[50]', + 'errors' => [ + 'min_length' => 'Description is too short baby.', + ], + ], + ]); + $this->assertFalse($this->model->insert($data)); + + $errors = $this->model->errors(); + $this->assertSame('Give me a name baby.', $errors['name']); + $this->assertSame('Description is too short baby.', $errors['description']); + } + + public function testValidationWithSetValidationMessage(): void + { + $data = [ + 'name' => null, + 'description' => 'some great marketing stuff', + ]; + + $this->model->setValidationMessage('name', [ + 'required' => 'Your baby name is missing.', + 'min_length' => 'Too short, man!', + ]); + $this->assertFalse($this->model->insert($data)); + + $errors = $this->model->errors(); + $this->assertSame('Your baby name is missing.', $errors['name']); + } + + public function testValidationPlaceholdersSuccess(): void + { + $data = [ + 'name' => 'abc', + 'id' => 13, + 'token' => 13, + ]; + + $this->assertTrue($this->model->validate($data)); + } + + public function testValidationPlaceholdersFail(): void + { + $data = [ + 'name' => 'abc', + 'id' => 13, + 'token' => 12, + ]; + + $this->assertFalse($this->model->validate($data)); + } + + public function testSkipValidation(): void + { + $data = [ + 'name' => '2', + 'description' => 'some great marketing stuff', + ]; + + $this->assertIsNumeric($this->model->skipValidation(true)->insert($data)); + } + + public function testCleanValidationRemovesAllWhenNoDataProvided(): void + { + $cleaner = $this->getPrivateMethodInvoker($this->model, 'cleanValidationRules'); + + $rules = [ + 'name' => 'required', + 'foo' => 'bar', + ]; + + $rules = $cleaner($rules, null); + $this->assertEmpty($rules); + } + + public function testCleanValidationRemovesOnlyForFieldsNotProvided(): void + { + $cleaner = $this->getPrivateMethodInvoker($this->model, 'cleanValidationRules'); + + $rules = [ + 'name' => 'required', + 'foo' => 'required', + ]; + + $data = [ + 'foo' => 'bar', + ]; + + $rules = $cleaner($rules, $data); + $this->assertArrayHasKey('foo', $rules); + $this->assertArrayNotHasKey('name', $rules); + } + + public function testCleanValidationReturnsAllWhenAllExist(): void + { + $cleaner = $this->getPrivateMethodInvoker($this->model, 'cleanValidationRules'); + + $rules = [ + 'name' => 'required', + 'foo' => 'required', + ]; + + $data = [ + 'foo' => 'bar', + 'name' => null, + ]; + + $rules = $cleaner($rules, $data); + $this->assertArrayHasKey('foo', $rules); + $this->assertArrayHasKey('name', $rules); + } + + public function testValidationPassesWithMissingFields(): void + { + $data = [ + 'foo' => 'bar', + ]; + + $result = $this->model->validate($data); + $this->assertTrue($result); + } + + /** + * @see https://github.com/codeigniter4/CodeIgniter4/issues/1584 + */ + public function testUpdateWithValidation(): void + { + $data = [ + 'description' => 'This is a first test!', + 'name' => 'valid', + 'id' => 42, + 'token' => 42, + ]; + + $id = $this->model->insert($data); + $this->assertTrue((bool) $id); + + $data['description'] = 'This is a second test!'; + unset($data['name']); + + $result = $this->model->update($id, $data); + $this->assertTrue($result); + } + + /** + * @see https://github.com/codeigniter4/CodeIgniter4/issues/1717 + */ + public function testRequiredWithValidationEmptyString(): void + { + $this->assertFalse($this->model->insert(['name' => ''])); + } + + /** + * @see https://github.com/codeigniter4/CodeIgniter4/issues/1717 + */ + public function testRequiredWithValidationNull(): void + { + $this->assertFalse($this->model->insert(['name' => null])); + } + + /** + * @see https://github.com/codeigniter4/CodeIgniter4/issues/1717 + */ + public function testRequiredWithValidationTrue(): void + { + $data = [ + 'name' => 'foobar', + 'description' => 'just because we have to', + ]; + + $this->assertNotFalse($this->model->insert($data)); + } + + /** + * @see https://github.com/codeigniter4/CodeIgniter4/issues/1574 + */ + public function testValidationIncludingErrors(): void + { + $data = [ + 'description' => 'This is a first test!', + 'name' => 'valid', + 'id' => 42, + 'token' => 42, + ]; + + $this->createModel(ValidErrorsModel::class); + + $id = $this->model->insert($data); + $this->assertFalse((bool) $id); + $this->assertSame('Minimum Length Error', $this->model->errors()['name']); + } + + public function testValidationByObject(): void + { + $data = new stdClass(); + + $data->name = 'abc'; + $data->id = '13'; + $data->token = '13'; + + $this->assertTrue($this->model->validate($data)); + } + + public function testGetValidationRules(): void + { + $this->createModel(JobModel::class); + $this->setPrivateProperty($this->model, 'validationRules', ['description' => 'required']); + + $rules = $this->model->getValidationRules(); + $this->assertSame('required', $rules['description']); + } + + public function testGetValidationMessages(): void + { + $jobData = [ + [ + 'name' => 'Comedian', + 'description' => null, + ], + ]; + + $this->createModel(JobModel::class); + $this->setPrivateProperty($this->model, 'validationRules', ['description' => 'required']); + $this->setPrivateProperty($this->model, 'validationMessages', ['description' => 'Description field is required.']); + + $this->assertFalse($this->model->insertBatch($jobData)); + + $error = $this->model->getValidationMessages(); + $this->assertSame('Description field is required.', $error['description']); + } + + /** + * @see https://github.com/codeigniter4/CodeIgniter4/issues/6577 + */ + public function testUpdateEntityWithPropertyCleanValidationRulesTrueAndCallingCleanRulesFalse() + { + $model = new class () extends Model { + protected $table = 'test'; + protected $allowedFields = ['field1', 'field2', 'field3', 'field4']; + protected $returnType = SimpleEntity::class; + protected $validationRules = [ + 'field1' => 'required_with[field2,field3,field4]', + 'field2' => 'permit_empty', + 'field3' => 'permit_empty', + 'field4' => 'permit_empty', + ]; + }; + + // Simulate to get the entity from the database. + $entity = new SimpleEntity(); + $entity->setAttributes([ + 'id' => '1', + 'field1' => 'value1', + 'field2' => 'value2', + 'field3' => '', + 'field4' => '', + ]); + + // Change field1 value. + $entity->field1 = ''; + + // Set $cleanValidationRules to false by cleanRules() + $model->cleanRules(false)->save($entity); + + $errors = $model->errors(); + $this->assertCount(1, $errors); + $this->assertSame( + $errors['field1'], + 'The field1 field is required when field2,field3,field4 is present.' + ); + } + + public function testUpdateEntityWithPropertyCleanValidationRulesFalse() + { + $model = new class () extends Model { + protected $table = 'test'; + protected $allowedFields = ['field1', 'field2', 'field3', 'field4']; + protected $returnType = SimpleEntity::class; + protected $validationRules = [ + 'field1' => 'required_with[field2,field3,field4]', + 'field2' => 'permit_empty', + 'field3' => 'permit_empty', + 'field4' => 'permit_empty', + ]; + + // Set to false. + protected $cleanValidationRules = false; + }; + + // Simulate to get the entity from the database. + $entity = new SimpleEntity(); + $entity->setAttributes([ + 'id' => '1', + 'field1' => 'value1', + 'field2' => 'value2', + 'field3' => '', + 'field4' => '', + ]); + + // Change field1 value. + $entity->field1 = ''; + + $model->save($entity); + + $errors = $model->errors(); + $this->assertCount(1, $errors); + $this->assertSame( + $errors['field1'], + 'The field1 field is required when field2,field3,field4 is present.' + ); + } + + public function testInsertEntityValidateEntireRules() + { + $model = new class () extends Model { + protected $table = 'test'; + protected $allowedFields = ['field1', 'field2', 'field3', 'field4']; + protected $returnType = SimpleEntity::class; + protected $validationRules = [ + 'field1' => 'required', + 'field2' => 'required', + 'field3' => 'permit_empty', + 'field4' => 'permit_empty', + ]; + }; + + $entity = new SimpleEntity(); + $entity->setAttributes([ + 'field1' => 'value1', + // field2 is missing + 'field3' => '', + 'field4' => '', + ]); + + // Insert ignores $cleanValidationRules value. + $model->insert($entity); + + $errors = $model->errors(); + $this->assertCount(1, $errors); + $this->assertSame( + $errors['field2'], + 'The field2 field is required.' + ); + } +}