diff --git a/tests/framework/data/ActiveDataFilterTest.php b/tests/framework/data/ActiveDataFilterTest.php index 598fcca8547..d4e2feb1c45 100644 --- a/tests/framework/data/ActiveDataFilterTest.php +++ b/tests/framework/data/ActiveDataFilterTest.php @@ -183,6 +183,45 @@ public function testBuild($filter, $expectedResult): void $this->assertEquals($expectedResult, $builder->build()); } + public function testBuildWithQueryOperatorMap(): void + { + $builder = new ActiveDataFilter(); + $builder->queryOperatorMap = [ + 'AND' => 'MY_AND', + 'NOT' => 'MY_NOT', + '>' => 'MY_GT', + ]; + $searchModel = (new DynamicModel(['name' => null, 'number' => null])) + ->addRule('name', 'string') + ->addRule('number', 'integer'); + $builder->setSearchModel($searchModel); + + $builder->filter = ['and' => [['name' => 'a'], ['name' => 'b']]]; + $result = $builder->build(); + $this->assertSame(['MY_AND', ['name' => 'a'], ['name' => 'b']], $result); + + $builder->filter = ['not' => ['name' => 'a']]; + $result = $builder->build(); + $this->assertSame(['MY_NOT', ['name' => 'a']], $result); + + $builder->filter = ['number' => ['gt' => 5]]; + $result = $builder->build(); + $this->assertSame(['MY_GT', 'number', 5], $result); + } + + public function testBuildAttributeConditionWithoutConditionBuilder(): void + { + $builder = new ActiveDataFilter(); + $searchModel = (new DynamicModel(['number' => null])) + ->addRule('number', 'integer'); + $builder->setSearchModel($searchModel); + + unset($builder->conditionBuilders['>']); + $builder->filter = ['number' => ['gt' => 5]]; + $result = $builder->build(); + $this->assertSame(['>', 'number', 5], $result); + } + /** * @depends testBuild */ diff --git a/tests/framework/data/ArrayDataProviderTest.php b/tests/framework/data/ArrayDataProviderTest.php index 3c747a54864..0e291db27fd 100644 --- a/tests/framework/data/ArrayDataProviderTest.php +++ b/tests/framework/data/ArrayDataProviderTest.php @@ -205,6 +205,13 @@ public function testGetKeys(): void $this->assertEquals(['key1', 'key2'], $dataProvider->getKeys()); } + public function testPrepareModelsWithNullAllModels(): void + { + $dataProvider = new ArrayDataProvider(); + $this->assertSame([], $dataProvider->getModels()); + $this->assertSame(0, $dataProvider->getTotalCount()); + } + public function testSortFlags(): void { $simpleArray = [['sortField' => 1], ['sortField' => 2], ['sortField' => 11]]; diff --git a/tests/framework/data/BaseDataProviderTest.php b/tests/framework/data/BaseDataProviderTest.php index c7468e9db35..d089317a235 100644 --- a/tests/framework/data/BaseDataProviderTest.php +++ b/tests/framework/data/BaseDataProviderTest.php @@ -9,7 +9,10 @@ namespace yiiunit\framework\data; use ReflectionClass; +use yii\base\InvalidArgumentException; use yii\data\BaseDataProvider; +use yii\data\Pagination; +use yii\data\Sort; use yiiunit\TestCase; /** @@ -17,6 +20,12 @@ */ class BaseDataProviderTest extends TestCase { + protected function setUp(): void + { + parent::setUp(); + $this->mockApplication(); + } + public function testGenerateId(): void { $rc = new ReflectionClass(BaseDataProvider::class); @@ -33,6 +42,143 @@ public function testGenerateId(): void $this->assertNull((new ConcreteDataProvider())->id); $this->assertNotNull((new ConcreteDataProvider())->id); } + + public function testPrepareAndGetModels(): void + { + $provider = new ConcreteDataProvider(); + $this->assertSame([], $provider->getModels()); + $this->assertSame([], $provider->getKeys()); + } + + public function testSetModels(): void + { + $provider = new ConcreteDataProvider(); + $provider->setModels(['a', 'b']); + $this->assertSame(['a', 'b'], $provider->getModels()); + } + + public function testSetKeys(): void + { + $provider = new ConcreteDataProvider(); + $provider->setKeys([1, 2]); + $this->assertSame([1, 2], $provider->getKeys()); + } + + public function testGetCount(): void + { + $provider = new ConcreteDataProvider(); + $provider->setModels(['a', 'b', 'c']); + $this->assertSame(3, $provider->getCount()); + } + + public function testGetTotalCountWithoutPagination(): void + { + $provider = new ConcreteDataProvider(['pagination' => false]); + $provider->setModels(['a', 'b']); + $this->assertSame(2, $provider->getTotalCount()); + } + + public function testGetTotalCountWithPagination(): void + { + $provider = new CountableDataProvider(); + $this->assertSame(42, $provider->getTotalCount()); + } + + public function testSetTotalCount(): void + { + $provider = new ConcreteDataProvider(); + $provider->setTotalCount(42); + $this->assertSame(42, $provider->getTotalCount()); + } + + public function testGetPaginationDefault(): void + { + $provider = new ConcreteDataProvider(); + $this->assertInstanceOf(Pagination::class, $provider->getPagination()); + } + + public function testSetPaginationWithId(): void + { + $provider = new ConcreteDataProvider(['id' => 'test']); + $pagination = $provider->getPagination(); + $this->assertSame('test-page', $pagination->pageParam); + $this->assertSame('test-per-page', $pagination->pageSizeParam); + } + + public function testSetPaginationInstance(): void + { + $pagination = new Pagination(); + $provider = new ConcreteDataProvider(); + $provider->setPagination($pagination); + $this->assertSame($pagination, $provider->getPagination()); + } + + public function testSetPaginationFalse(): void + { + $provider = new ConcreteDataProvider(); + $provider->setPagination(false); + $this->assertFalse($provider->getPagination()); + } + + public function testSetPaginationInvalid(): void + { + $provider = new ConcreteDataProvider(); + $this->expectException(InvalidArgumentException::class); + $provider->setPagination('invalid'); + } + + public function testGetSortDefault(): void + { + $provider = new ConcreteDataProvider(); + $this->assertInstanceOf(Sort::class, $provider->getSort()); + } + + public function testSetSortWithId(): void + { + $provider = new ConcreteDataProvider(['id' => 'test']); + $sort = $provider->getSort(); + $this->assertSame('test-sort', $sort->sortParam); + } + + public function testSetSortInstance(): void + { + $sort = new Sort(); + $provider = new ConcreteDataProvider(); + $provider->setSort($sort); + $this->assertSame($sort, $provider->getSort()); + } + + public function testSetSortFalse(): void + { + $provider = new ConcreteDataProvider(); + $provider->setSort(false); + $this->assertFalse($provider->getSort()); + } + + public function testSetSortInvalid(): void + { + $provider = new ConcreteDataProvider(); + $this->expectException(InvalidArgumentException::class); + $provider->setSort('invalid'); + } + + public function testRefresh(): void + { + $provider = new ConcreteDataProvider(); + $provider->getModels(); + $provider->setTotalCount(42); + $provider->refresh(); + $this->assertSame(0, $provider->getTotalCount()); + } + + public function testForcePrepare(): void + { + $provider = new ConcreteDataProvider(); + $provider->prepare(); + $provider->setModels(['overridden']); + $provider->prepare(true); + $this->assertSame([], $provider->getModels()); + } } /** @@ -64,3 +210,21 @@ protected function prepareTotalCount() return 0; } } + +class CountableDataProvider extends BaseDataProvider +{ + protected function prepareModels() + { + return ['prepared-model']; + } + + protected function prepareKeys($models) + { + return array_keys($models); + } + + protected function prepareTotalCount() + { + return 42; + } +} diff --git a/tests/framework/data/DataFilterTest.php b/tests/framework/data/DataFilterTest.php index cd75719124e..aa7cb9de2e1 100644 --- a/tests/framework/data/DataFilterTest.php +++ b/tests/framework/data/DataFilterTest.php @@ -459,6 +459,263 @@ public function testNormalizeNonDefaultNull(): void $this->assertEquals(['name' => null], $builder->normalize(false)); } + public function testSetSearchAttributeTypes(): void + { + $builder = new DataFilter(); + $types = ['name' => DataFilter::TYPE_STRING, 'number' => DataFilter::TYPE_INTEGER]; + $builder->setSearchAttributeTypes($types); + $this->assertSame($types, $builder->getSearchAttributeTypes()); + } + + public function testDetectSearchAttributeTypesBoolean(): void + { + $builder = new DataFilter(); + $model = (new DynamicModel(['active' => null])) + ->addRule('active', 'boolean'); + $builder->setSearchModel($model); + $types = $builder->getSearchAttributeTypes(); + $this->assertSame(DataFilter::TYPE_BOOLEAN, $types['active']); + } + + public function testDetectSearchAttributeTypeFallsBackToString(): void + { + $builder = new DataFilter(); + $model = (new DynamicModel(['name' => null])) + ->addRule('name', 'required'); + $builder->setSearchModel($model); + $types = $builder->getSearchAttributeTypes(); + $this->assertSame(DataFilter::TYPE_STRING, $types['name']); + } + + public function testGetSearchModelThrowsExceptionForNonModel(): void + { + $builder = new DataFilter(); + $builder->setSearchModel(function () { + return new stdClass(); + }); + $this->expectException('yii\base\InvalidConfigException'); + $builder->getSearchModel(); + } + + public function testParseErrorMessageFallback(): void + { + $builder = new DataFilter(); + $builder->setSearchModel((new DynamicModel(['name' => null]))->addRule('name', 'string')); + $builder->setErrorMessages([]); + $builder->filter = 'invalid'; + $builder->validate(); + $this->assertSame('The format of Filter is invalid.', $builder->getFirstError('filter')); + } + + public function testValidateMixedOperatorAndValue(): void + { + $builder = new DataFilter(); + $builder->setSearchModel( + (new DynamicModel(['number' => null])) + ->addRule('number', 'integer') + ); + $builder->filter = [ + 'number' => [ + 'gt' => 10, + 'invalid_key' => 20, + ], + ]; + $this->assertFalse($builder->validate()); + $this->assertSame( + ['Condition for "number" should be either a value or valid operator specification.'], + $builder->getErrors('filter') + ); + } + + public function testValidateUnsupportedOperatorType(): void + { + $builder = new DataFilter(); + $builder->setSearchModel( + (new DynamicModel(['name' => null])) + ->addRule('name', 'string') + ); + $builder->filter = [ + 'name' => [ + 'gt' => 'foo', + ], + ]; + $this->assertFalse($builder->validate()); + $this->assertSame( + ['"name" does not support operator "gt".'], + $builder->getErrors('filter') + ); + } + + public function testValidateMultiValueOperatorRequiresArray(): void + { + $builder = new DataFilter(); + $builder->setSearchModel( + (new DynamicModel(['number' => null])) + ->addRule('number', 'integer') + ); + $builder->filter = [ + 'number' => [ + 'in' => 'not-array', + ], + ]; + $this->assertFalse($builder->validate()); + $this->assertSame( + ['Operator "in" requires multiple operands.'], + $builder->getErrors('filter') + ); + } + + public function testBuildWithValidation(): void + { + $builder = new DataFilter(); + $builder->setSearchModel( + (new DynamicModel(['name' => null])) + ->addRule('name', 'string') + ); + $builder->filter = 'invalid'; + $this->assertFalse($builder->build()); + } + + public function testBuildSuccess(): void + { + $builder = new DataFilter(); + $builder->setSearchModel( + (new DynamicModel(['name' => null])) + ->addRule('name', 'string') + ); + $builder->filter = ['name' => 'test']; + $result = $builder->build(); + $this->assertSame(['name' => 'test'], $result); + } + + public function testNormalizeWithValidationFailure(): void + { + $builder = new DataFilter(); + $builder->setSearchModel( + (new DynamicModel(['name' => null])) + ->addRule('name', 'string') + ); + $builder->filter = 'invalid'; + $this->assertFalse($builder->normalize()); + } + + public function testMagicPropertyAccess(): void + { + $builder = new DataFilter(); + $this->assertTrue($builder->canGetProperty('filter')); + $this->assertTrue($builder->canSetProperty('filter')); + $builder->filter = ['name' => 'test']; + $this->assertSame(['name' => 'test'], $builder->filter); + $this->assertTrue(isset($builder->filter)); + unset($builder->filter); + $this->assertFalse(isset($builder->filter)); + } + + public function testMagicPropertyAccessNonFilter(): void + { + $builder = new DataFilter(); + $this->assertTrue($builder->canGetProperty('filterAttributeName')); + $this->assertTrue($builder->canSetProperty('filterAttributeName')); + } + + public function testMagicGetNonFilterThrows(): void + { + $builder = new DataFilter(); + $this->expectException('yii\base\UnknownPropertyException'); + $builder->__get('nonExistent'); + } + + public function testMagicSetNonFilterThrows(): void + { + $builder = new DataFilter(); + $this->expectException('yii\base\UnknownPropertyException'); + $builder->__set('nonExistent', 'value'); + } + + public function testMagicIssetNonFilter(): void + { + $builder = new DataFilter(); + $this->assertFalse(isset($builder->nonExistent)); + } + + public function testMagicUnsetNonFilterThrows(): void + { + $builder = new DataFilter(); + $this->expectException('yii\base\InvalidCallException'); + unset($builder->nonExistent); + } + + public function testDetectSearchAttributeTypeDateAndTime(): void + { + $builder = new DataFilter(); + $model = (new DynamicModel(['date' => null, 'time' => null])) + ->addRule('date', 'date', ['format' => 'php:Y-m-d']) + ->addRule('time', 'time', ['format' => 'php:H:i:s']); + $builder->setSearchModel($model); + $types = $builder->getSearchAttributeTypes(); + $this->assertSame(DataFilter::TYPE_DATE, $types['date']); + $this->assertSame(DataFilter::TYPE_TIME, $types['time']); + } + + public function testParseErrorMessageUnknownKey(): void + { + $builder = new DataFilter(); + $builder->setSearchModel( + (new DynamicModel(['name' => null])) + ->addRule('name', 'string') + ); + $ref = new \ReflectionProperty($builder, '_errorMessages'); + if (PHP_VERSION_ID < 80100) { + $ref->setAccessible(true); + } + $ref->setValue($builder, ['someKey' => 'some message']); + $builder->filter = 'invalid'; + $builder->validate(); + $this->assertSame('The format of Filter is invalid.', $builder->getFirstError('filter')); + } + + public function testGetErrorMessagesCallback(): void + { + $builder = new DataFilter(); + $ref = new \ReflectionProperty($builder, '_errorMessages'); + if (PHP_VERSION_ID < 80100) { + $ref->setAccessible(true); + } + $ref->setValue($builder, function () { + return ['unsupportedOperatorType' => 'Callback message']; + }); + $messages = $builder->getErrorMessages(); + $this->assertSame('Callback message', $messages['unsupportedOperatorType']); + $this->assertArrayHasKey('unknownAttribute', $messages); + } + + public function testValidateAttributeValueUnsafe(): void + { + $builder = new DataFilter(); + $model = (new DynamicModel(['name' => null])) + ->addRule('name', 'string'); + $builder->setSearchModel($model); + $builder->setSearchAttributeTypes(['name' => DataFilter::TYPE_STRING, 'fake' => DataFilter::TYPE_STRING]); + $builder->filter = ['fake' => 'value']; + $this->assertFalse($builder->validate()); + $this->assertStringContainsString('fake', (string) $builder->getFirstError('filter')); + } + + public function testAttributes(): void + { + $builder = new DataFilter(); + $this->assertSame(['filter'], $builder->attributes()); + + $builder->filterAttributeName = 'search'; + $this->assertSame(['search'], $builder->attributes()); + } + + public function testFormName(): void + { + $builder = new DataFilter(); + $this->assertSame('', $builder->formName()); + } + public function testSetupErrorMessages(): void { $builder = new DataFilter(); diff --git a/tests/framework/data/SortTest.php b/tests/framework/data/SortTest.php index e57f9d4c308..0a7ade9f8e3 100644 --- a/tests/framework/data/SortTest.php +++ b/tests/framework/data/SortTest.php @@ -336,6 +336,144 @@ public function testLinkWithoutParams($enableMultiSort, $defaultOrder, $link): v $this->assertEquals($link, $sort->link('age')); } + public function testNormalizeAttributesWithPartialConfig(): void + { + $sort = new Sort([ + 'attributes' => [ + 'age' => [ + 'label' => 'Age', + ], + ], + ]); + + $this->assertSame(SORT_ASC, $sort->attributes['age']['asc']['age']); + $this->assertSame(SORT_DESC, $sort->attributes['age']['desc']['age']); + $this->assertSame('Age', $sort->attributes['age']['label']); + } + + public function testLinkWithExistingCssClass(): void + { + $manager = new UrlManager([ + 'baseUrl' => '/', + 'scriptUrl' => '/index.php', + 'cache' => null, + ]); + + $sort = new Sort([ + 'attributes' => ['age'], + 'params' => ['sort' => 'age'], + 'urlManager' => $manager, + 'route' => 'site/index', + ]); + + $link = $sort->link('age', ['class' => 'custom']); + $this->assertSame('Age', $link); + } + + public function testLinkWithLabelOption(): void + { + $manager = new UrlManager([ + 'baseUrl' => '/', + 'scriptUrl' => '/index.php', + 'cache' => null, + ]); + + $sort = new Sort([ + 'attributes' => ['age'], + 'params' => ['sort' => 'age'], + 'urlManager' => $manager, + 'route' => 'site/index', + ]); + + $link = $sort->link('age', ['label' => 'Custom Label']); + $this->assertSame('Custom Label', $link); + } + + public function testLinkWithAttributeLabel(): void + { + $manager = new UrlManager([ + 'baseUrl' => '/', + 'scriptUrl' => '/index.php', + 'cache' => null, + ]); + + $sort = new Sort([ + 'attributes' => [ + 'age' => [ + 'asc' => ['age' => SORT_ASC], + 'desc' => ['age' => SORT_DESC], + 'label' => 'Custom Age', + ], + ], + 'urlManager' => $manager, + 'route' => 'site/index', + ]); + + $link = $sort->link('age'); + $this->assertSame('Custom Age', $link); + } + + public function testLinkWithModelClass(): void + { + $manager = new UrlManager([ + 'baseUrl' => '/', + 'scriptUrl' => '/index.php', + 'cache' => null, + ]); + + $sort = new Sort([ + 'attributes' => ['name'], + 'modelClass' => SortTestModel::class, + 'urlManager' => $manager, + 'route' => 'site/index', + ]); + + $link = $sort->link('name'); + $this->assertSame('Test Name', $link); + } + + public function testCreateAbsoluteUrl(): void + { + $manager = new UrlManager([ + 'baseUrl' => '/', + 'scriptUrl' => '/index.php', + 'cache' => null, + 'hostInfo' => 'http://example.com', + ]); + + $sort = new Sort([ + 'attributes' => ['age'], + 'params' => ['sort' => 'age'], + 'urlManager' => $manager, + 'route' => 'site/index', + ]); + + $url = $sort->createUrl('age', true); + $this->assertSame('http://example.com/index.php?r=site%2Findex&sort=-age', $url); + } + + public function testCreateSortParamWithUnknownAttribute(): void + { + $sort = new Sort([ + 'attributes' => ['age'], + 'route' => 'site/index', + ]); + + $this->expectException(\yii\base\InvalidConfigException::class); + $sort->createSortParam('unknown'); + } + + public function testHasAttribute(): void + { + $sort = new Sort([ + 'attributes' => ['age', 'name'], + ]); + + $this->assertTrue($sort->hasAttribute('age')); + $this->assertTrue($sort->hasAttribute('name')); + $this->assertFalse($sort->hasAttribute('unknown')); + } + public function testParseSortParam(): void { $sort = new CustomSort([ @@ -382,6 +520,16 @@ public function testGetExpressionOrders(): void } } +class SortTestModel extends \yii\base\Model +{ + public $name; + + public function attributeLabels() + { + return ['name' => 'Test Name']; + } +} + class CustomSort extends Sort { /**