Enable Discovery debug logging for production troubleshooting
- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
This commit is contained in:
314
tests/Framework/Database/Migration/MigrationCollectionTest.php
Normal file
314
tests/Framework/Database/Migration/MigrationCollectionTest.php
Normal file
@@ -0,0 +1,314 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Framework\Database\Migration;
|
||||
|
||||
use App\Framework\Database\Migration\Migration;
|
||||
use App\Framework\Database\Migration\MigrationCollection;
|
||||
use App\Framework\Database\Migration\MigrationVersion;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class MigrationCollectionTest extends TestCase
|
||||
{
|
||||
private Migration $migration1;
|
||||
|
||||
private Migration $migration2;
|
||||
|
||||
private Migration $migration3;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->migration1 = $this->createMockMigration('2024_01_01_120000', 'First migration');
|
||||
$this->migration2 = $this->createMockMigration('2024_01_02_120000', 'Second migration');
|
||||
$this->migration3 = $this->createMockMigration('2024_01_03_120000', 'Third migration');
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function constructor_creates_empty_collection(): void
|
||||
{
|
||||
$collection = new MigrationCollection();
|
||||
|
||||
$this->assertTrue($collection->isEmpty());
|
||||
$this->assertSame(0, $collection->count());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function constructor_accepts_variadic_migrations(): void
|
||||
{
|
||||
$collection = new MigrationCollection(
|
||||
$this->migration1,
|
||||
$this->migration2,
|
||||
$this->migration3
|
||||
);
|
||||
|
||||
$this->assertFalse($collection->isEmpty());
|
||||
$this->assertSame(3, $collection->count());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function fromArray_creates_collection_from_array(): void
|
||||
{
|
||||
$migrations = [$this->migration1, $this->migration2, $this->migration3];
|
||||
$collection = MigrationCollection::fromArray($migrations);
|
||||
|
||||
$this->assertSame(3, $collection->count());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function getIterator_allows_foreach_iteration(): void
|
||||
{
|
||||
$collection = new MigrationCollection($this->migration1, $this->migration2);
|
||||
|
||||
$iterations = 0;
|
||||
foreach ($collection as $migration) {
|
||||
$this->assertInstanceOf(Migration::class, $migration);
|
||||
$iterations++;
|
||||
}
|
||||
|
||||
$this->assertSame(2, $iterations);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function getByVersion_returns_migration_with_matching_version(): void
|
||||
{
|
||||
$collection = new MigrationCollection($this->migration1, $this->migration2);
|
||||
|
||||
$version = MigrationVersion::fromTimestamp('2024_01_01_120000');
|
||||
$result = $collection->getByVersion($version);
|
||||
|
||||
$this->assertSame($this->migration1, $result);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function getByVersion_returns_null_when_not_found(): void
|
||||
{
|
||||
$collection = new MigrationCollection($this->migration1);
|
||||
|
||||
$version = MigrationVersion::fromTimestamp('2024_01_99_999999');
|
||||
$result = $collection->getByVersion($version);
|
||||
|
||||
$this->assertNull($result);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function getByVersionString_returns_migration_with_matching_version_string(): void
|
||||
{
|
||||
$collection = new MigrationCollection($this->migration1, $this->migration2);
|
||||
|
||||
$result = $collection->getByVersionString('2024_01_02_120000');
|
||||
|
||||
$this->assertSame($this->migration2, $result);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function getByVersionString_returns_null_when_not_found(): void
|
||||
{
|
||||
$collection = new MigrationCollection($this->migration1);
|
||||
|
||||
$result = $collection->getByVersionString('2024_01_99_999999');
|
||||
|
||||
$this->assertNull($result);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function hasVersion_returns_true_when_version_exists(): void
|
||||
{
|
||||
$collection = new MigrationCollection($this->migration1, $this->migration2);
|
||||
|
||||
$version = MigrationVersion::fromTimestamp('2024_01_01_120000');
|
||||
$result = $collection->hasVersion($version);
|
||||
|
||||
$this->assertTrue($result);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function hasVersion_returns_false_when_version_does_not_exist(): void
|
||||
{
|
||||
$collection = new MigrationCollection($this->migration1);
|
||||
|
||||
$version = MigrationVersion::fromTimestamp('2024_01_99_999999');
|
||||
$result = $collection->hasVersion($version);
|
||||
|
||||
$this->assertFalse($result);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function hasVersionString_returns_true_when_version_string_exists(): void
|
||||
{
|
||||
$collection = new MigrationCollection($this->migration1, $this->migration2);
|
||||
|
||||
$result = $collection->hasVersionString('2024_01_02_120000');
|
||||
|
||||
$this->assertTrue($result);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function hasVersionString_returns_false_when_version_string_does_not_exist(): void
|
||||
{
|
||||
$collection = new MigrationCollection($this->migration1);
|
||||
|
||||
$result = $collection->hasVersionString('2024_01_99_999999');
|
||||
|
||||
$this->assertFalse($result);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function getVersions_returns_migration_version_collection(): void
|
||||
{
|
||||
$collection = new MigrationCollection($this->migration1, $this->migration2);
|
||||
|
||||
$versions = $collection->getVersions();
|
||||
|
||||
$this->assertSame(2, $versions->count());
|
||||
$this->assertTrue($versions->containsString('2024_01_01_120000'));
|
||||
$this->assertTrue($versions->containsString('2024_01_02_120000'));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function sorted_returns_collection_sorted_by_version_ascending(): void
|
||||
{
|
||||
// Create collection in reverse order
|
||||
$collection = new MigrationCollection($this->migration3, $this->migration1, $this->migration2);
|
||||
|
||||
$sorted = $collection->sorted();
|
||||
$migrations = $sorted->toArray();
|
||||
|
||||
$this->assertSame($this->migration1, $migrations[0]);
|
||||
$this->assertSame($this->migration2, $migrations[1]);
|
||||
$this->assertSame($this->migration3, $migrations[2]);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function sortedDescending_returns_collection_sorted_by_version_descending(): void
|
||||
{
|
||||
$collection = new MigrationCollection($this->migration1, $this->migration2, $this->migration3);
|
||||
|
||||
$sorted = $collection->sortedDescending();
|
||||
$migrations = $sorted->toArray();
|
||||
|
||||
$this->assertSame($this->migration3, $migrations[0]);
|
||||
$this->assertSame($this->migration2, $migrations[1]);
|
||||
$this->assertSame($this->migration1, $migrations[2]);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function filter_returns_collection_with_filtered_migrations(): void
|
||||
{
|
||||
$collection = new MigrationCollection($this->migration1, $this->migration2, $this->migration3);
|
||||
|
||||
$filtered = $collection->filter(function (Migration $migration) {
|
||||
return str_contains($migration->getDescription(), 'Second');
|
||||
});
|
||||
|
||||
$this->assertSame(1, $filtered->count());
|
||||
$this->assertSame($this->migration2, $filtered->toArray()[0]);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function after_returns_migrations_after_specified_version(): void
|
||||
{
|
||||
$collection = new MigrationCollection($this->migration1, $this->migration2, $this->migration3);
|
||||
|
||||
$version = MigrationVersion::fromTimestamp('2024_01_01_120000');
|
||||
$filtered = $collection->after($version);
|
||||
|
||||
$this->assertSame(2, $filtered->count());
|
||||
$migrations = $filtered->toArray();
|
||||
$this->assertSame($this->migration2, $migrations[0]);
|
||||
$this->assertSame($this->migration3, $migrations[1]);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function upTo_returns_migrations_up_to_and_including_specified_version(): void
|
||||
{
|
||||
$collection = new MigrationCollection($this->migration1, $this->migration2, $this->migration3);
|
||||
|
||||
$version = MigrationVersion::fromTimestamp('2024_01_02_120000');
|
||||
$filtered = $collection->upTo($version);
|
||||
|
||||
$this->assertSame(2, $filtered->count());
|
||||
$migrations = $filtered->toArray();
|
||||
$this->assertSame($this->migration1, $migrations[0]);
|
||||
$this->assertSame($this->migration2, $migrations[1]);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function first_returns_first_migration_by_version(): void
|
||||
{
|
||||
$collection = new MigrationCollection($this->migration3, $this->migration1, $this->migration2);
|
||||
|
||||
$first = $collection->first();
|
||||
|
||||
$this->assertSame($this->migration1, $first);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function first_returns_null_for_empty_collection(): void
|
||||
{
|
||||
$collection = new MigrationCollection();
|
||||
|
||||
$first = $collection->first();
|
||||
|
||||
$this->assertNull($first);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function last_returns_last_migration_by_version(): void
|
||||
{
|
||||
$collection = new MigrationCollection($this->migration1, $this->migration3, $this->migration2);
|
||||
|
||||
$last = $collection->last();
|
||||
|
||||
$this->assertSame($this->migration3, $last);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function last_returns_null_for_empty_collection(): void
|
||||
{
|
||||
$collection = new MigrationCollection();
|
||||
|
||||
$last = $collection->last();
|
||||
|
||||
$this->assertNull($last);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function toArray_returns_migrations_as_array(): void
|
||||
{
|
||||
$collection = new MigrationCollection($this->migration1, $this->migration2);
|
||||
|
||||
$array = $collection->toArray();
|
||||
|
||||
$this->assertIsArray($array);
|
||||
$this->assertSame(2, count($array));
|
||||
$this->assertSame($this->migration1, $array[0]);
|
||||
$this->assertSame($this->migration2, $array[1]);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function collection_is_immutable(): void
|
||||
{
|
||||
$collection = new MigrationCollection($this->migration1);
|
||||
|
||||
$filtered = $collection->filter(fn () => true);
|
||||
$sorted = $collection->sorted();
|
||||
|
||||
// Original collection should be unchanged
|
||||
$this->assertSame(1, $collection->count());
|
||||
$this->assertNotSame($collection, $filtered);
|
||||
$this->assertNotSame($collection, $sorted);
|
||||
}
|
||||
|
||||
private function createMockMigration(string $version, string $description): Migration
|
||||
{
|
||||
$migration = $this->createMock(Migration::class);
|
||||
$migration->method('getVersion')
|
||||
->willReturn(MigrationVersion::fromTimestamp($version));
|
||||
$migration->method('getDescription')
|
||||
->willReturn($description);
|
||||
|
||||
return $migration;
|
||||
}
|
||||
}
|
||||
179
tests/Framework/Database/Migration/MigrationLoaderTest.php
Normal file
179
tests/Framework/Database/Migration/MigrationLoaderTest.php
Normal file
@@ -0,0 +1,179 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Framework\Database\Migration;
|
||||
|
||||
use App\Framework\Core\ValueObjects\ClassName;
|
||||
use App\Framework\Database\Migration\Migration;
|
||||
use App\Framework\Database\Migration\MigrationCollection;
|
||||
use App\Framework\Database\Migration\MigrationLoader;
|
||||
use App\Framework\Database\Migration\MigrationVersion;
|
||||
use App\Framework\DI\Container;
|
||||
use App\Framework\Discovery\Results\AttributeRegistry;
|
||||
use App\Framework\Discovery\Results\DiscoveryRegistry;
|
||||
use App\Framework\Discovery\Results\InterfaceRegistry;
|
||||
use App\Framework\Discovery\Results\RouteRegistry;
|
||||
use App\Framework\Discovery\Results\TemplateRegistry;
|
||||
use App\Framework\Discovery\ValueObjects\InterfaceMapping;
|
||||
use App\Framework\Filesystem\FilePath;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class MigrationLoaderTest extends TestCase
|
||||
{
|
||||
private Container $container;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->container = $this->createMock(Container::class);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function loadMigrations_returns_empty_collection_when_no_migrations_found(): void
|
||||
{
|
||||
// Setup interface registry with no migrations
|
||||
$interfaceRegistry = new InterfaceRegistry();
|
||||
|
||||
$discoveryRegistry = new DiscoveryRegistry(
|
||||
new AttributeRegistry(),
|
||||
$interfaceRegistry,
|
||||
new RouteRegistry(),
|
||||
new TemplateRegistry()
|
||||
);
|
||||
|
||||
$loader = new MigrationLoader($discoveryRegistry, $this->container);
|
||||
|
||||
$result = $loader->loadMigrations();
|
||||
|
||||
$this->assertInstanceOf(MigrationCollection::class, $result);
|
||||
$this->assertTrue($result->isEmpty());
|
||||
$this->assertSame(0, $result->count());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function loadMigrations_returns_sorted_collection_of_migrations(): void
|
||||
{
|
||||
// Create mock migration instances (in unsorted order)
|
||||
$migration1 = $this->createMockMigration('2024_01_03_120000', 'Third migration');
|
||||
$migration2 = $this->createMockMigration('2024_01_01_120000', 'First migration');
|
||||
$migration3 = $this->createMockMigration('2024_01_02_120000', 'Second migration');
|
||||
|
||||
// Create interface mappings
|
||||
$mapping1 = new InterfaceMapping(
|
||||
ClassName::create(Migration::class),
|
||||
ClassName::create('App\\Migration1'),
|
||||
FilePath::create('/path/to/migration1.php')
|
||||
);
|
||||
$mapping2 = new InterfaceMapping(
|
||||
ClassName::create(Migration::class),
|
||||
ClassName::create('App\\Migration2'),
|
||||
FilePath::create('/path/to/migration2.php')
|
||||
);
|
||||
$mapping3 = new InterfaceMapping(
|
||||
ClassName::create(Migration::class),
|
||||
ClassName::create('App\\Migration3'),
|
||||
FilePath::create('/path/to/migration3.php')
|
||||
);
|
||||
|
||||
// Setup interface registry
|
||||
$interfaceRegistry = new InterfaceRegistry();
|
||||
$interfaceRegistry->add($mapping1);
|
||||
$interfaceRegistry->add($mapping2);
|
||||
$interfaceRegistry->add($mapping3);
|
||||
|
||||
$discoveryRegistry = new DiscoveryRegistry(
|
||||
new AttributeRegistry(),
|
||||
$interfaceRegistry,
|
||||
new RouteRegistry(),
|
||||
new TemplateRegistry()
|
||||
);
|
||||
|
||||
$loader = new MigrationLoader($discoveryRegistry, $this->container);
|
||||
|
||||
// Setup container to return migration instances
|
||||
$this->container->method('get')
|
||||
->willReturnMap([
|
||||
['App\\Migration1', $migration1],
|
||||
['App\\Migration2', $migration2],
|
||||
['App\\Migration3', $migration3],
|
||||
]);
|
||||
|
||||
$result = $loader->loadMigrations();
|
||||
|
||||
$this->assertInstanceOf(MigrationCollection::class, $result);
|
||||
$this->assertSame(3, $result->count());
|
||||
|
||||
// Verify migrations are sorted by version
|
||||
$migrations = $result->toArray();
|
||||
$this->assertSame($migration2, $migrations[0]); // 2024_01_01_120000
|
||||
$this->assertSame($migration3, $migrations[1]); // 2024_01_02_120000
|
||||
$this->assertSame($migration1, $migrations[2]); // 2024_01_03_120000
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function loadMigrations_uses_discovery_registry_to_find_migration_implementations(): void
|
||||
{
|
||||
$interfaceRegistry = new InterfaceRegistry();
|
||||
|
||||
$discoveryRegistry = new DiscoveryRegistry(
|
||||
new AttributeRegistry(),
|
||||
$interfaceRegistry,
|
||||
new RouteRegistry(),
|
||||
new TemplateRegistry()
|
||||
);
|
||||
|
||||
$loader = new MigrationLoader($discoveryRegistry, $this->container);
|
||||
|
||||
$result = $loader->loadMigrations();
|
||||
|
||||
// Verify that the discovery registry was used by checking we get an empty collection
|
||||
$this->assertInstanceOf(MigrationCollection::class, $result);
|
||||
$this->assertTrue($result->isEmpty());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function loadMigrations_uses_container_to_instantiate_migrations(): void
|
||||
{
|
||||
$migration = $this->createMockMigration('2024_01_01_120000', 'Test migration');
|
||||
|
||||
$mapping = new InterfaceMapping(
|
||||
ClassName::create(Migration::class),
|
||||
ClassName::create('App\\TestMigration'),
|
||||
FilePath::create('/path/to/test_migration.php')
|
||||
);
|
||||
|
||||
$interfaceRegistry = new InterfaceRegistry();
|
||||
$interfaceRegistry->add($mapping);
|
||||
|
||||
$discoveryRegistry = new DiscoveryRegistry(
|
||||
new AttributeRegistry(),
|
||||
$interfaceRegistry,
|
||||
new RouteRegistry(),
|
||||
new TemplateRegistry()
|
||||
);
|
||||
|
||||
$loader = new MigrationLoader($discoveryRegistry, $this->container);
|
||||
|
||||
$this->container->expects($this->once())
|
||||
->method('get')
|
||||
->with('App\\TestMigration')
|
||||
->willReturn($migration);
|
||||
|
||||
$result = $loader->loadMigrations();
|
||||
|
||||
$this->assertSame(1, $result->count());
|
||||
$this->assertSame($migration, $result->toArray()[0]);
|
||||
}
|
||||
|
||||
private function createMockMigration(string $version, string $description): Migration
|
||||
{
|
||||
$migration = $this->createMock(Migration::class);
|
||||
$migration->method('getVersion')
|
||||
->willReturn(MigrationVersion::fromTimestamp($version));
|
||||
$migration->method('getDescription')
|
||||
->willReturn($description);
|
||||
|
||||
return $migration;
|
||||
}
|
||||
}
|
||||
359
tests/Framework/Database/Migration/MigrationRunnerTest.php
Normal file
359
tests/Framework/Database/Migration/MigrationRunnerTest.php
Normal file
@@ -0,0 +1,359 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Framework\Database\ConnectionInterface;
|
||||
use App\Framework\Database\Exception\DatabaseException;
|
||||
use App\Framework\Database\Migration\Migration;
|
||||
use App\Framework\Database\Migration\MigrationRunner;
|
||||
use App\Framework\Database\ResultInterface;
|
||||
|
||||
beforeEach(function () {
|
||||
$this->connection = new TestConnection();
|
||||
$this->migrationRunner = new MigrationRunner($this->connection, 'test_migrations');
|
||||
});
|
||||
|
||||
test('constructor creates migrations table', function () {
|
||||
// Constructor should have already been called in beforeEach
|
||||
$queries = $this->connection->getQueries();
|
||||
|
||||
expect($queries)->toHaveCount(1)
|
||||
->and($queries[0]['type'])->toBe('execute')
|
||||
->and($queries[0]['sql'])->toContain('CREATE TABLE IF NOT EXISTS test_migrations');
|
||||
});
|
||||
|
||||
test('migrate runs pending migrations', function () {
|
||||
// Mock migration
|
||||
$migration = new TestMigration();
|
||||
$migrationData = (object) [
|
||||
'version' => '2024_01_01_000000',
|
||||
'description' => 'Test Migration',
|
||||
'instance' => $migration,
|
||||
];
|
||||
|
||||
// Set no applied migrations initially
|
||||
$this->connection->setAppliedMigrations([]);
|
||||
|
||||
$result = $this->migrationRunner->migrate([$migrationData]);
|
||||
|
||||
expect($result)->toContain('2024_01_01_000000');
|
||||
expect($migration->wasExecuted())->toBeTrue();
|
||||
|
||||
// Verify the migration was recorded
|
||||
$queries = $this->connection->getQueries();
|
||||
$insertQueries = array_filter(
|
||||
$queries,
|
||||
fn ($q) =>
|
||||
$q['type'] === 'execute' && str_contains($q['sql'], 'INSERT INTO test_migrations')
|
||||
);
|
||||
expect($insertQueries)->toHaveCount(1);
|
||||
});
|
||||
|
||||
test('migrate skips already applied migrations', function () {
|
||||
$migration = new TestMigration();
|
||||
$migrationData = (object) [
|
||||
'version' => '2024_01_01_000000',
|
||||
'description' => 'Test Migration',
|
||||
'instance' => $migration,
|
||||
];
|
||||
|
||||
// Set migration as already applied
|
||||
$this->connection->setAppliedMigrations([['version' => '2024_01_01_000000']]);
|
||||
|
||||
$result = $this->migrationRunner->migrate([$migrationData]);
|
||||
|
||||
expect($result)->toBeEmpty();
|
||||
expect($migration->wasExecuted())->toBeFalse();
|
||||
});
|
||||
|
||||
test('migrate rolls back on failure', function () {
|
||||
// Mock failing migration
|
||||
$failingMigration = new FailingTestMigration();
|
||||
$migrationData = (object) [
|
||||
'version' => '2024_01_01_000000',
|
||||
'description' => 'Failing Migration',
|
||||
'instance' => $failingMigration,
|
||||
];
|
||||
|
||||
// Set no applied migrations initially
|
||||
$this->connection->setAppliedMigrations([]);
|
||||
$this->connection->setShouldFail(true);
|
||||
|
||||
expect(fn () => $this->migrationRunner->migrate([$migrationData]))
|
||||
->toThrow(DatabaseException::class);
|
||||
|
||||
// Verify transaction was used (inTransaction was called)
|
||||
expect($this->connection->inTransaction())->toBeFalse(); // Should be rolled back
|
||||
});
|
||||
|
||||
test('rollback reverts applied migration', function () {
|
||||
$migration = new TestMigration();
|
||||
$migrationData = (object) [
|
||||
'version' => '2024_01_01_000000',
|
||||
'description' => 'Test Migration',
|
||||
'instance' => $migration,
|
||||
];
|
||||
|
||||
// Set migration as already applied
|
||||
$this->connection->setAppliedMigrations([['version' => '2024_01_01_000000']]);
|
||||
|
||||
$result = $this->migrationRunner->rollback([$migrationData], 1);
|
||||
|
||||
expect($result)->toContain('2024_01_01_000000');
|
||||
expect($migration->wasRolledBack())->toBeTrue();
|
||||
|
||||
// Verify the migration record was deleted
|
||||
$queries = $this->connection->getQueries();
|
||||
$deleteQueries = array_filter(
|
||||
$queries,
|
||||
fn ($q) =>
|
||||
$q['type'] === 'execute' && str_contains($q['sql'], 'DELETE FROM test_migrations')
|
||||
);
|
||||
expect($deleteQueries)->toHaveCount(1);
|
||||
});
|
||||
|
||||
test('get status returns migration status', function () {
|
||||
$migration1Data = (object) [
|
||||
'version' => '2024_01_01_000000',
|
||||
'description' => 'Applied Migration',
|
||||
'instance' => new TestMigration(),
|
||||
];
|
||||
$migration2Data = (object) [
|
||||
'version' => '2024_01_02_000000',
|
||||
'description' => 'Pending Migration',
|
||||
'instance' => new TestMigration(),
|
||||
];
|
||||
|
||||
// Set only first migration as applied
|
||||
$this->connection->setAppliedMigrations([['version' => '2024_01_01_000000']]);
|
||||
|
||||
$status = $this->migrationRunner->getStatus([$migration1Data, $migration2Data]);
|
||||
|
||||
expect($status)->toHaveCount(2)
|
||||
->and($status[0]['applied'])->toBeTrue()
|
||||
->and($status[1]['applied'])->toBeFalse();
|
||||
});
|
||||
|
||||
// Test fixtures
|
||||
class TestMigration implements Migration
|
||||
{
|
||||
private bool $executed = false;
|
||||
|
||||
private bool $rolledBack = false;
|
||||
|
||||
public function up(ConnectionInterface $connection): void
|
||||
{
|
||||
$this->executed = true;
|
||||
// Simulate migration execution
|
||||
$connection->execute('CREATE TABLE test_table (id INT)');
|
||||
}
|
||||
|
||||
public function down(ConnectionInterface $connection): void
|
||||
{
|
||||
$this->rolledBack = true;
|
||||
// Simulate migration rollback
|
||||
$connection->execute('DROP TABLE test_table');
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Test Migration';
|
||||
}
|
||||
|
||||
public function getVersion(): \App\Framework\Database\Migration\MigrationVersion
|
||||
{
|
||||
return \App\Framework\Database\Migration\MigrationVersion::fromTimestamp('2024_01_01_000000');
|
||||
}
|
||||
|
||||
public function wasExecuted(): bool
|
||||
{
|
||||
return $this->executed;
|
||||
}
|
||||
|
||||
public function wasRolledBack(): bool
|
||||
{
|
||||
return $this->rolledBack;
|
||||
}
|
||||
}
|
||||
|
||||
class FailingTestMigration implements Migration
|
||||
{
|
||||
public function up(ConnectionInterface $connection): void
|
||||
{
|
||||
throw new \Exception('Migration failed');
|
||||
}
|
||||
|
||||
public function down(ConnectionInterface $connection): void
|
||||
{
|
||||
// No-op
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Failing Migration';
|
||||
}
|
||||
|
||||
public function getVersion(): \App\Framework\Database\Migration\MigrationVersion
|
||||
{
|
||||
return \App\Framework\Database\Migration\MigrationVersion::fromTimestamp('2024_01_01_000000');
|
||||
}
|
||||
}
|
||||
|
||||
class TestConnection implements ConnectionInterface
|
||||
{
|
||||
private array $queries = [];
|
||||
|
||||
private array $appliedMigrations = [];
|
||||
|
||||
private bool $inTransaction = false;
|
||||
|
||||
private bool $shouldFail = false;
|
||||
|
||||
public function setAppliedMigrations(array $migrations): void
|
||||
{
|
||||
$this->appliedMigrations = $migrations;
|
||||
}
|
||||
|
||||
public function setShouldFail(bool $fail): void
|
||||
{
|
||||
$this->shouldFail = $fail;
|
||||
}
|
||||
|
||||
public function execute(string $sql, array $parameters = []): int
|
||||
{
|
||||
$this->queries[] = ['type' => 'execute', 'sql' => $sql, 'params' => $parameters];
|
||||
|
||||
if ($this->shouldFail && strpos($sql, 'INSERT INTO') !== false) {
|
||||
throw new DatabaseException('Simulated database failure');
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
public function query(string $sql, array $parameters = []): ResultInterface
|
||||
{
|
||||
$this->queries[] = ['type' => 'query', 'sql' => $sql, 'params' => $parameters];
|
||||
|
||||
return new TestResult($this->appliedMigrations);
|
||||
}
|
||||
|
||||
public function queryOne(string $sql, array $parameters = []): ?array
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function queryColumn(string $sql, array $parameters = []): array
|
||||
{
|
||||
$this->queries[] = ['type' => 'queryColumn', 'sql' => $sql, 'params' => $parameters];
|
||||
|
||||
// Return the versions from applied migrations
|
||||
return array_column($this->appliedMigrations, 'version');
|
||||
}
|
||||
|
||||
public function queryScalar(string $sql, array $parameters = []): mixed
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function beginTransaction(): void
|
||||
{
|
||||
$this->inTransaction = true;
|
||||
}
|
||||
|
||||
public function commit(): void
|
||||
{
|
||||
$this->inTransaction = false;
|
||||
}
|
||||
|
||||
public function rollback(): void
|
||||
{
|
||||
$this->inTransaction = false;
|
||||
}
|
||||
|
||||
public function inTransaction(): bool
|
||||
{
|
||||
return $this->inTransaction;
|
||||
}
|
||||
|
||||
public function lastInsertId(): string
|
||||
{
|
||||
return '1';
|
||||
}
|
||||
|
||||
public function getPdo(): \PDO
|
||||
{
|
||||
return new class () extends \PDO {
|
||||
public function __construct()
|
||||
{
|
||||
// Skip parent constructor to avoid actual DB connection
|
||||
}
|
||||
|
||||
public function getAttribute(int $attribute): mixed
|
||||
{
|
||||
if ($attribute === \PDO::ATTR_DRIVER_NAME) {
|
||||
return 'sqlite';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public function getQueries(): array
|
||||
{
|
||||
return $this->queries;
|
||||
}
|
||||
}
|
||||
|
||||
class TestResult implements ResultInterface
|
||||
{
|
||||
private array $data;
|
||||
|
||||
private int $position = 0;
|
||||
|
||||
public function __construct(array $data = [])
|
||||
{
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
public function fetch(): ?array
|
||||
{
|
||||
if ($this->position >= count($this->data)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->data[$this->position++];
|
||||
}
|
||||
|
||||
public function fetchAll(): array
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
public function fetchColumn(int $column = 0): array
|
||||
{
|
||||
return array_column($this->data, $column);
|
||||
}
|
||||
|
||||
public function fetchScalar(): mixed
|
||||
{
|
||||
$row = $this->fetch();
|
||||
|
||||
return $row ? reset($row) : null;
|
||||
}
|
||||
|
||||
public function rowCount(): int
|
||||
{
|
||||
return count($this->data);
|
||||
}
|
||||
|
||||
public function getIterator(): \Iterator
|
||||
{
|
||||
return new \ArrayIterator($this->data);
|
||||
}
|
||||
|
||||
public function count(): int
|
||||
{
|
||||
return count($this->data);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user