Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 159 additions & 2 deletions tests/NestedSetsBehaviorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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();
}
}
6 changes: 3 additions & 3 deletions tests/support/model/MultipleTree.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
* @property int $tree
* @property string $name
*/
final class MultipleTree extends ActiveRecord
class MultipleTree extends ActiveRecord
{
public static function tableName(): string
{
Expand Down Expand Up @@ -50,10 +50,10 @@ public function transactions(): array
}

/**
* @phpstan-return MultipleTreeQuery<self>
* @phpstan-return MultipleTreeQuery<static>
*/
public static function find(): MultipleTreeQuery
{
return new MultipleTreeQuery(self::class);
return new MultipleTreeQuery(static::class);
}
}
9 changes: 9 additions & 0 deletions tests/support/model/MultipleTreeQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@
*/
final class MultipleTreeQuery extends ActiveQuery
{
/**
* @phpstan-param class-string<T> $modelClass
* @phpstan-param array<string, mixed> $config
*/
public function __construct(string $modelClass, array $config = [])
{
parent::__construct($modelClass, $config);
}

public function behaviors(): array
{
return [
Expand Down
15 changes: 12 additions & 3 deletions tests/support/model/Tree.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* @property int $depth
* @property string $name
*/
final class Tree extends ActiveRecord
class Tree extends ActiveRecord
{
public static function tableName(): string
{
Expand All @@ -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 [
Expand All @@ -46,10 +55,10 @@ public function transactions(): array
}

/**
* @phpstan-return TreeQuery<self>
* @phpstan-return TreeQuery<static>
*/
public static function find(): TreeQuery
{
return new TreeQuery(self::class);
return new TreeQuery(static::class);
}
}
9 changes: 9 additions & 0 deletions tests/support/model/TreeQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@
*/
final class TreeQuery extends ActiveQuery
{
/**
* @phpstan-param class-string<T> $modelClass
* @phpstan-param array<string, mixed> $config
*/
public function __construct(string $modelClass, array $config = [])
{
parent::__construct($modelClass, $config);
}

public function behaviors(): array
{
return [
Expand Down