From 7b37d581d1d6bec1c6138d35ea932ce272bf98b0 Mon Sep 17 00:00:00 2001 From: Wilmer Arambula Date: Sat, 28 Jun 2025 13:50:19 -0400 Subject: [PATCH] refactor: Raise code coverage 100%. --- tests/NestedSetsBehaviorTest.php | 161 +++++++++++++++++++++- tests/support/model/MultipleTree.php | 6 +- tests/support/model/MultipleTreeQuery.php | 9 ++ tests/support/model/Tree.php | 15 +- tests/support/model/TreeQuery.php | 9 ++ 5 files changed, 192 insertions(+), 8 deletions(-) diff --git a/tests/NestedSetsBehaviorTest.php b/tests/NestedSetsBehaviorTest.php index 96210e5..d311816 100644 --- a/tests/NestedSetsBehaviorTest.php +++ b/tests/NestedSetsBehaviorTest.php @@ -7,12 +7,14 @@ use LogicException; use Throwable; use yii\base\NotSupportedException; -use yii\db\Exception; -use yii\db\StaleObjectException; +use yii\db\{ActiveRecord, Exception, StaleObjectException}; use yii\helpers\ArrayHelper; use yii2\extensions\nestedsets\NestedSetsBehavior; use yii2\extensions\nestedsets\tests\support\model\{MultipleTree, Tree}; +use function get_class; +use function sprintf; + final class NestedSetsBehaviorTest extends TestCase { public function testReturnTrueAndMatchXmlAfterMakeRootNewForTreeAndMultipleTree(): void @@ -1463,4 +1465,159 @@ public function testThrowLogicExceptionWhenBehaviorIsDetachedFromOwner(): void $behavior->parents(); } + + public function testReturnAffectedRowsAndUpdateTreeAfterDeleteWithChildrenWhenManualTransactionIsUsed(): void + { + $this->generateFixtureTree(); + + $node = Tree::findOne(10); + + self::assertNotNull( + $node, + 'Node with ID \'10\' should exist before attempting to delete with children using manual transaction.', + ); + self::assertEquals( + 'Node 2.1', + $node->getAttribute('name'), + 'Node with ID \'10\' should have the name \'Node 2.1\' before deletion.', + ); + self::assertFalse( + $node->isTransactional(ActiveRecord::OP_DELETE), + 'Node with ID \'10\' should not use transactional delete (manual transaction expected).', + ); + + $initialCount = (int) Tree::find()->count(); + $toDeleteCount = (int) Tree::find() + ->andWhere(['>=', 'lft', $node->getAttribute('lft')]) + ->andWhere(['<=', 'rgt', $node->getAttribute('rgt')]) + ->count(); + + self::assertEquals( + 3, + $toDeleteCount, + 'Node \'2.1\' should have itself and 2 children (total \'3\' nodes to delete).', + ); + + $result = (int) $node->deleteWithChildren(); + + self::assertEquals( + $toDeleteCount, + $result, + '\'deleteWithChildren()\' should return the number of affected rows equal to the nodes deleted.', + ); + + $finalCount = (int) Tree::find()->count(); + + self::assertEquals( + $initialCount - $toDeleteCount, + $finalCount, + 'Tree node count after deletion should decrease by the number of deleted nodes.', + ); + self::assertNull( + Tree::findOne(10), + 'Node with ID \'10\' should not exist after deletion.', + ); + self::assertNull( + Tree::findOne(11), + 'Node with ID \'11\' should not exist after deletion.', + ); + self::assertNull( + Tree::findOne(12), + 'Node with ID \'12\' should not exist after deletion.', + ); + self::assertNotNull( + Tree::findOne(1), + 'Root node with ID \'1\' should still exist after deleting node \'10\' and its children.', + ); + } + + public function testReturnFalseWhenDeleteWithChildrenIsAbortedByBeforeDelete(): void + { + $this->createDatabase(); + + $node = $this->createPartialMock( + Tree::class, + [ + 'beforeDelete', + ], + ); + $node->setAttributes( + [ + 'id' => 1, + 'name' => 'Test Node', + 'lft' => 1, + 'rgt' => 2, + 'depth' => 0, + ], + ); + $node->setIsNewRecord(false); + $node->expects(self::once())->method('beforeDelete')->willReturn(false); + + self::assertFalse( + $node->isTransactional(ActiveRecord::OP_DELETE), + 'Node with ID \'1\' should not use transactional delete when \'beforeDelete()\' returns \'false\'.', + ); + + $result = $node->deleteWithChildren(); + + self::assertFalse( + $result, + '\'deleteWithChildren()\' should return \'false\' when \'beforeDelete()\' aborts the deletion process.', + ); + } + + public function testThrowExceptionWhenDeleteWithChildrenThrowsExceptionInTransaction(): void + { + $this->createDatabase(); + + $node = new Tree(['name' => 'Root']); + + $node->detachBehavior('nestedSetsBehavior'); + + self::assertNull( + $node->getBehavior('nestedSetsBehavior'), + 'Behavior must be detached before testing exception handling.', + ); + + $nestedSetsBehavior = $this->createMock(NestedSetsBehavior::class); + $nestedSetsBehavior->expects(self::once()) + ->method('deleteWithChildren') + ->willThrowException(new Exception('Simulated database error during deletion')); + + $node->attachBehavior('nestedSetsBehavior', $nestedSetsBehavior); + $behavior = $node->getBehavior('nestedSetsBehavior'); + + self::assertInstanceOf( + NestedSetsBehavior::class, + $behavior, + 'Behavior must be attached to the node before testing exception handling.', + ); + + $this->expectException(Exception::class); + $this->expectExceptionMessage('Simulated database error during deletion'); + + $behavior->deleteWithChildren(); + } + + public function testThrowExceptionWhenMakeRootIsCalledOnModelWithoutPrimaryKey(): void + { + $this->createDatabase(); + + $node = new class (['name' => 'Root without PK']) extends MultipleTree { + public static function primaryKey(): array + { + return []; + } + + public function makeRoot(): bool + { + return parent::makeRoot(); + } + }; + + $this->expectException(Exception::class); + $this->expectExceptionMessage(sprintf('"%s" must have a primary key.', get_class($node))); + + $node->makeRoot(); + } } diff --git a/tests/support/model/MultipleTree.php b/tests/support/model/MultipleTree.php index 7e5524b..7f462f6 100644 --- a/tests/support/model/MultipleTree.php +++ b/tests/support/model/MultipleTree.php @@ -15,7 +15,7 @@ * @property int $tree * @property string $name */ -final class MultipleTree extends ActiveRecord +class MultipleTree extends ActiveRecord { public static function tableName(): string { @@ -50,10 +50,10 @@ public function transactions(): array } /** - * @phpstan-return MultipleTreeQuery + * @phpstan-return MultipleTreeQuery */ public static function find(): MultipleTreeQuery { - return new MultipleTreeQuery(self::class); + return new MultipleTreeQuery(static::class); } } diff --git a/tests/support/model/MultipleTreeQuery.php b/tests/support/model/MultipleTreeQuery.php index cd20eb7..a41145b 100644 --- a/tests/support/model/MultipleTreeQuery.php +++ b/tests/support/model/MultipleTreeQuery.php @@ -14,6 +14,15 @@ */ final class MultipleTreeQuery extends ActiveQuery { + /** + * @phpstan-param class-string $modelClass + * @phpstan-param array $config + */ + public function __construct(string $modelClass, array $config = []) + { + parent::__construct($modelClass, $config); + } + public function behaviors(): array { return [ diff --git a/tests/support/model/Tree.php b/tests/support/model/Tree.php index 30a1763..4491fae 100644 --- a/tests/support/model/Tree.php +++ b/tests/support/model/Tree.php @@ -14,7 +14,7 @@ * @property int $depth * @property string $name */ -final class Tree extends ActiveRecord +class Tree extends ActiveRecord { public static function tableName(): string { @@ -28,6 +28,15 @@ public function behaviors(): array ]; } + public function isTransactional($operation): bool + { + if ($operation === ActiveRecord::OP_DELETE) { + return false; + } + + return parent::isTransactional($operation); + } + public function rules(): array { return [ @@ -46,10 +55,10 @@ public function transactions(): array } /** - * @phpstan-return TreeQuery + * @phpstan-return TreeQuery */ public static function find(): TreeQuery { - return new TreeQuery(self::class); + return new TreeQuery(static::class); } } diff --git a/tests/support/model/TreeQuery.php b/tests/support/model/TreeQuery.php index 2d1d2f0..3943077 100644 --- a/tests/support/model/TreeQuery.php +++ b/tests/support/model/TreeQuery.php @@ -14,6 +14,15 @@ */ final class TreeQuery extends ActiveQuery { + /** + * @phpstan-param class-string $modelClass + * @phpstan-param array $config + */ + public function __construct(string $modelClass, array $config = []) + { + parent::__construct($modelClass, $config); + } + public function behaviors(): array { return [