96 lines
2.9 KiB
PHP
96 lines
2.9 KiB
PHP
<?php
|
|
|
|
namespace Tests\Integration;
|
|
|
|
use Exception;
|
|
use PHPUnit\Framework\Attributes\Group;
|
|
use PHPUnit\Framework\Attributes\Test;
|
|
use ReflectionMethod;
|
|
use Tests\Models\User;
|
|
use Tests\TestCase;
|
|
|
|
/**
|
|
* ModelTransactionTest - DuckBrain integration matrix
|
|
*
|
|
* Family: beginTransaction, commit, rollBack, plus the implicit rollBack
|
|
* that Model.php performs when a query fails mid-transaction (and the
|
|
* Exception it rethrows wrapping the PDO error).
|
|
*/
|
|
#[Group('integration')]
|
|
final class ModelTransactionTest extends TestCase
|
|
{
|
|
public static function setUpBeforeClass(): void
|
|
{
|
|
self::createTable('users', [
|
|
'id' => 'pk',
|
|
'username' => 'string',
|
|
'email' => 'string',
|
|
'bio' => 'text',
|
|
'age' => 'number',
|
|
'is_active' => 'bool',
|
|
'last_login' => 'timestamp',
|
|
]);
|
|
}
|
|
|
|
private static function seed(string $username): User
|
|
{
|
|
$user = new User();
|
|
$user->username = $username;
|
|
$user->email = $username . '@duckbrain.dev';
|
|
$user->age = 25;
|
|
$user->save();
|
|
|
|
return $user;
|
|
}
|
|
|
|
#[Test]
|
|
public function commitMakesPendingWritesVisible(): void
|
|
{
|
|
User::beginTransaction();
|
|
$user = self::seed('committed_alice');
|
|
$this->assertTrue(User::commit());
|
|
|
|
$this->assertNotNull(User::getById($user->id));
|
|
}
|
|
|
|
#[Test]
|
|
public function rollBackDiscardsPendingWrites(): void
|
|
{
|
|
User::beginTransaction();
|
|
$user = self::seed('rolled_bob');
|
|
$this->assertTrue(User::rollBack());
|
|
|
|
$this->assertNull(User::getById($user->id));
|
|
}
|
|
|
|
#[Test]
|
|
public function aFailingQueryRollsBackTheOpenTransaction(): void
|
|
{
|
|
User::beginTransaction();
|
|
$user = self::seed('wrecked_carol');
|
|
|
|
// Dirty the builder BEFORE failing: only a leftover-free result after
|
|
// the exception proves query() itself cleaned up (finding F2).
|
|
User::where('age', '>', '30');
|
|
|
|
try {
|
|
(new ReflectionMethod(User::class, 'query'))
|
|
->invokeArgs(null, ['SELECT * FROM table_that_does_not_exist']);
|
|
$this->fail('a failing query inside a transaction must throw');
|
|
} catch (Exception $e) {
|
|
$this->assertStringContainsString('Error at query to database', $e->getMessage());
|
|
}
|
|
|
|
$this->assertFalse(self::db()->inTransaction(), 'the exception path must close the transaction');
|
|
|
|
$sqlAfterFailure = (new ReflectionMethod(User::class, 'buildQuery'))->invoke(null);
|
|
$this->assertSame(
|
|
'SELECT * FROM users',
|
|
$sqlAfterFailure,
|
|
'a failed query must leave the builder at its default state'
|
|
);
|
|
|
|
$this->assertNull(User::where('username', 'wrecked_carol')->getFirst(), 'pending insert must be undone');
|
|
}
|
|
}
|