Files
Leantime/tests/Unit/app/Domain/Install/UpdateSql30526Test.php

140 lines
4.9 KiB
PHP

<?php
namespace Unit\app\Domain\Install;
use Illuminate\Database\ConnectionInterface;
use Leantime\Domain\Install\Repositories\Install;
use Mockery;
use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration;
use Unit\TestCase;
/**
* Regression tests for migration update_sql_30526 — the hygiene pass that
* deletes goal↔milestone `tracked_by` edges violating the same-project product
* rule (promoted by the pre-guard 30524/30525 backfill) and edges orphaned by
* the pre-cascade generic ticket delete. Behavior pinned with a faked
* connection — no DB.
*
* The migration issues two pluck reads on the aliased edge table (cross-project
* first, then orphans) and one chunked whereIn-delete on the plain table; the
* fake serves them in that order.
*/
class UpdateSql30526Test extends TestCase
{
use MockeryPHPUnitIntegration;
/**
* @param int[] $crossProjectIds edge ids the cross-project read returns
* @param int[] $orphanIds edge ids the orphan read returns
* @return array{result: mixed, deleted: int[]}
*/
private function runMigration(array $crossProjectIds, array $orphanIds, bool $tablesExist = true): array
{
$deleted = [];
// The two aliased pluck reads arrive in a fixed order (cross-project,
// then orphans) — serve them from a queue shared across builder
// instances.
$pluckQueue = new \ArrayObject([$crossProjectIds, $orphanIds]);
$schema = Mockery::mock();
$schema->shouldReceive('hasTable')->andReturn($tablesExist);
$conn = Mockery::mock(ConnectionInterface::class);
$conn->shouldReceive('getSchemaBuilder')->andReturn($schema);
$conn->shouldReceive('table')->andReturnUsing(
function (string $table) use ($pluckQueue, &$deleted) {
return new class($pluckQueue, $deleted)
{
private array $whereInIds = [];
public function __construct(private \ArrayObject $pluckQueue, private array &$deleted) {}
public function join(...$a): static
{
return $this;
}
public function leftJoin(...$a): static
{
return $this;
}
public function where(...$a): static
{
return $this;
}
public function whereColumn(...$a): static
{
return $this;
}
public function whereIn($column, $ids): static
{
$this->whereInIds = $ids;
return $this;
}
public function pluck($column)
{
$sets = $this->pluckQueue->getArrayCopy();
$next = array_shift($sets);
$this->pluckQueue->exchangeArray($sets);
return collect($next ?? []);
}
public function delete(): int
{
foreach ($this->whereInIds as $id) {
$this->deleted[] = (int) $id;
}
return count($this->whereInIds);
}
};
}
);
$install = (new \ReflectionClass(Install::class))->newInstanceWithoutConstructor();
$prop = new \ReflectionProperty(Install::class, 'connection');
$prop->setAccessible(true);
$prop->setValue($install, $conn);
return ['result' => $install->update_sql_30526(), 'deleted' => $deleted];
}
public function test_deletes_cross_project_and_orphaned_edges(): void
{
$out = $this->runMigration(crossProjectIds: [11, 12], orphanIds: [13]);
$this->assertTrue($out['result']);
$this->assertSame([11, 12, 13], $out['deleted']);
}
public function test_deduplicates_an_edge_that_is_both_cross_project_and_orphaned(): void
{
$out = $this->runMigration(crossProjectIds: [11], orphanIds: [11, 12]);
$this->assertTrue($out['result']);
$this->assertSame([11, 12], $out['deleted'], 'an id in both sets is deleted once');
}
public function test_a_clean_graph_deletes_nothing(): void
{
$out = $this->runMigration(crossProjectIds: [], orphanIds: []);
$this->assertTrue($out['result']);
$this->assertSame([], $out['deleted']);
}
public function test_is_a_no_op_when_the_required_tables_are_absent(): void
{
$out = $this->runMigration(crossProjectIds: [11], orphanIds: [12], tablesExist: false);
$this->assertTrue($out['result']);
$this->assertSame([], $out['deleted'], 'missing schema short-circuits before any delete');
}
}