203 lines
8.8 KiB
PHP
203 lines
8.8 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace MeshVault\Tests\Integration;
|
|
|
|
use PDO;
|
|
use PDOException;
|
|
use PHPUnit\Framework\Attributes\DataProvider;
|
|
use PHPUnit\Framework\TestCase;
|
|
use RuntimeException;
|
|
|
|
final class CatalogMigrationTest extends TestCase
|
|
{
|
|
#[DataProvider('badPaths')]
|
|
public function testUpgradePreflightLeavesEverythingIntact(string $path, bool $duplicate): void
|
|
{
|
|
if ($duplicate) {
|
|
$this->pdo->exec("INSERT INTO files (id, model_id, folder_id, name, managed_path)
|
|
VALUES (31, 21, 10, 'other', 'parts/part.stl')");
|
|
} else {
|
|
$statement = $this->pdo->prepare('UPDATE files SET managed_path = ? WHERE id = 30');
|
|
$statement->execute([$path]);
|
|
}
|
|
$this->assertPreflightFails(false, $duplicate ? ['30', '31'] : ['30']);
|
|
}
|
|
|
|
public static function badPaths(): array
|
|
{
|
|
return [['parts/part.stl', true], ['a/../part.stl', false], ['a//part.stl', false], ['C:/part.stl', false]];
|
|
}
|
|
|
|
#[DataProvider('downgradeConflicts')]
|
|
public function testDowngradePreflightLeavesEverythingIntact(bool $unassigned): void
|
|
{
|
|
RepositoryDatabase::migrateTo($this->path, self::NEW);
|
|
if ($unassigned) {
|
|
$this->pdo->exec('UPDATE files SET model_id = NULL WHERE id = 30');
|
|
} else {
|
|
$this->pdo->exec("UPDATE models SET name = 'First' WHERE id = 21");
|
|
}
|
|
$this->assertPreflightFails(true, $unassigned ? ['30'] : ['20', '21']);
|
|
}
|
|
|
|
public static function downgradeConflicts(): array
|
|
{
|
|
return [[true], [false]];
|
|
}
|
|
|
|
private function assertPreflightFails(bool $down, array $ids): void
|
|
{
|
|
$before = $this->snapshot();
|
|
try {
|
|
if ($down) {
|
|
RepositoryDatabase::rollbackTo($this->path, self::OLD);
|
|
} else {
|
|
RepositoryDatabase::migrateTo($this->path, self::NEW);
|
|
}
|
|
self::fail('Expected preflight rejection');
|
|
} catch (RuntimeException $error) {
|
|
foreach ($ids as $id) {
|
|
self::assertStringContainsString($id, $error->getMessage());
|
|
}
|
|
self::assertStringContainsString('Resolve', $error->getMessage());
|
|
}
|
|
self::assertSame($before, $this->snapshot());
|
|
}
|
|
|
|
private function rows(): array
|
|
{
|
|
$rows = [];
|
|
$tables = ['folders', 'models', 'files', 'tags', 'model_tags', 'thumbnails', 'import_jobs', 'import_items'];
|
|
foreach ($tables as $table) {
|
|
$rows[$table] = $this->pdo->query('SELECT * FROM ' . $table . ' ORDER BY 1')->fetchAll(PDO::FETCH_ASSOC);
|
|
}
|
|
return $rows;
|
|
}
|
|
|
|
private function indexes(): array
|
|
{
|
|
$indexes = [];
|
|
if ($this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME) === 'sqlite') {
|
|
foreach (['files', 'models'] as $table) {
|
|
foreach ($this->pdo->query('PRAGMA index_list(' . $table . ')')->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
|
$indexes[$table][$row['name']] = (int) $row['unique'];
|
|
}
|
|
ksort($indexes[$table]);
|
|
}
|
|
}
|
|
return $indexes;
|
|
}
|
|
|
|
private function snapshot(): array
|
|
{
|
|
$snapshot = [$this->rows(), $this->pdo->query('SELECT * FROM phinxlog ORDER BY version')->fetchAll()];
|
|
if ($this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME) === 'sqlite') {
|
|
$snapshot[] = $this->pdo->query('SELECT * FROM sqlite_master ORDER BY type, name')->fetchAll();
|
|
foreach (['files', 'models'] as $table) {
|
|
$snapshot[] = $this->pdo->query('PRAGMA foreign_key_list(' . $table . ')')->fetchAll();
|
|
$snapshot[] = $this->pdo->query('PRAGMA index_list(' . $table . ')')->fetchAll();
|
|
}
|
|
}
|
|
return $snapshot;
|
|
}
|
|
|
|
private function assertRejected(string $sql): void
|
|
{
|
|
try {
|
|
$this->pdo->exec($sql);
|
|
self::fail('Expected database constraint rejection');
|
|
} catch (PDOException) {
|
|
self::assertTrue(true);
|
|
}
|
|
}
|
|
|
|
private const OLD = 20261011120006;
|
|
private const NEW = 20261012120001;
|
|
private string $directory;
|
|
private string $path;
|
|
private PDO $pdo;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
$this->directory = RepositoryDatabase::directory('catalog-migration');
|
|
$this->path = $this->directory . '/catalog.sqlite';
|
|
$this->pdo = RepositoryDatabase::migrate($this->path, self::OLD);
|
|
$this->pdo->exec("INSERT INTO folders (id, name, path) VALUES (10, 'root', '/')");
|
|
$this->pdo->exec("INSERT INTO models (id, folder_id, name, notes) VALUES
|
|
(20, 10, 'First', 'keep'), (21, 10, 'Second', 'also keep')");
|
|
$this->pdo->exec("INSERT INTO files (id, model_id, folder_id, name, managed_path, hash, size)
|
|
VALUES (30, 20, 10, 'part.stl', 'parts/part.stl', 'abc', 123)");
|
|
// Explicit fixture IDs do not advance PostgreSQL sequences.
|
|
if ($this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME) === 'pgsql') {
|
|
foreach (['folders' => 10, 'models' => 21, 'files' => 30] as $table => $id) {
|
|
$this->pdo->query("SELECT setval(pg_get_serial_sequence('$table', 'id'), $id, true)")->closeCursor();
|
|
}
|
|
}
|
|
$this->pdo->exec("INSERT INTO tags (id, name) VALUES (40, 'tag')");
|
|
$this->pdo->exec('INSERT INTO model_tags (model_id, tag_id) VALUES (20, 40)');
|
|
$this->pdo->exec("INSERT INTO thumbnails (model_id, managed_path, hash, size, mime_type)
|
|
VALUES (20, 'thumb.png', 'thumbhash', 12, 'image/png')");
|
|
$this->pdo->exec("INSERT INTO import_jobs (id, source_path) VALUES (50, 'source')");
|
|
$this->pdo->exec("INSERT INTO import_items (job_id, source_path, model_id) VALUES (50, 'part.stl', 20)");
|
|
}
|
|
|
|
protected function tearDown(): void
|
|
{
|
|
unset($this->pdo);
|
|
RepositoryDatabase::cleanup($this->path);
|
|
RepositoryDatabase::remove($this->directory);
|
|
}
|
|
|
|
public function testRoundTripPreservesRowsAndConstraints(): void
|
|
{
|
|
$before = $this->rows();
|
|
$oldIndexes = $this->indexes();
|
|
RepositoryDatabase::migrateTo($this->path, self::NEW);
|
|
$newIndexes = $this->indexes();
|
|
if ($newIndexes !== []) {
|
|
self::assertSame(1, $newIndexes['files']['idx_files_managed_path']);
|
|
self::assertSame(0, $newIndexes['files']['idx_files_model_id']);
|
|
self::assertSame(0, $newIndexes['models']['idx_models_folder_name']);
|
|
foreach (['idx_files_hash', 'idx_files_status', 'idx_files_folder_id'] as $index) {
|
|
self::assertSame($oldIndexes['files'][$index], $newIndexes['files'][$index]);
|
|
}
|
|
self::assertSame(
|
|
$oldIndexes['models']['idx_models_added_at'],
|
|
$newIndexes['models']['idx_models_added_at']
|
|
);
|
|
}
|
|
self::assertSame($before, $this->rows());
|
|
$this->assertRejected("INSERT INTO files (model_id, folder_id, name, managed_path)
|
|
VALUES (NULL, 10, 'duplicate', 'parts/part.stl')");
|
|
$this->assertRejected('UPDATE files SET model_id = 999 WHERE id = 30');
|
|
$this->assertRejected('DELETE FROM models WHERE id = 20');
|
|
$this->assertRejected('DELETE FROM folders WHERE id = 10');
|
|
$this->pdo->exec('UPDATE files SET model_id = NULL WHERE id = 30');
|
|
$this->assertRejected("INSERT INTO files (model_id, folder_id, name, managed_path)
|
|
VALUES (NULL, 10, 'duplicate', 'parts/part.stl')");
|
|
$this->pdo->exec('UPDATE files SET model_id = 20 WHERE id = 30');
|
|
RepositoryDatabase::rollbackTo($this->path, self::OLD);
|
|
self::assertSame($oldIndexes, $this->indexes());
|
|
self::assertSame($before, $this->rows());
|
|
$this->assertRejected('UPDATE files SET model_id = NULL WHERE id = 30');
|
|
$this->assertRejected("INSERT INTO models (folder_id, name) VALUES (10, 'First')");
|
|
RepositoryDatabase::migrateTo($this->path, self::NEW);
|
|
self::assertSame($before, $this->rows());
|
|
$model = $this->pdo->query("INSERT INTO models (folder_id, name) VALUES (10, 'First') RETURNING id");
|
|
self::assertGreaterThan(21, (int) $model->fetchColumn());
|
|
$model->closeCursor();
|
|
$file = $this->pdo->query("INSERT INTO files (model_id, folder_id, name, managed_path)
|
|
VALUES (NULL, 10, 'new', 'new.stl') RETURNING id");
|
|
self::assertGreaterThan(30, (int) $file->fetchColumn());
|
|
$file->closeCursor();
|
|
$this->pdo->exec('UPDATE files SET model_id = NULL WHERE id = 30');
|
|
$this->pdo->exec('DELETE FROM models WHERE id = 20');
|
|
self::assertSame(2, (int) $this->pdo->query('SELECT COUNT(*) FROM files')->fetchColumn());
|
|
if ($this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME) === 'sqlite') {
|
|
self::assertSame([], $this->pdo->query('PRAGMA foreign_key_check')->fetchAll());
|
|
}
|
|
}
|
|
}
|