137 lines
4.2 KiB
PHP
137 lines
4.2 KiB
PHP
<?php
|
|
|
|
namespace Tests;
|
|
|
|
use Libs\Database;
|
|
use Libs\Model;
|
|
use PDO;
|
|
use PHPUnit\Framework\TestCase as FrameworkTestCase;
|
|
|
|
/**
|
|
* TestCase - DuckBrain test harness
|
|
*
|
|
* Base class for the framework's own tests. Deliberately lightweight: this
|
|
* repo has no migrations/ nor duckbrain-commands, so each test defines its
|
|
* schema with raw DDL through createTable(), and tables registered there are
|
|
* dropped automatically after the class finishes.
|
|
*/
|
|
abstract class TestCase extends FrameworkTestCase
|
|
{
|
|
/**
|
|
* @var list<string> Tables created through createTable() for this class.
|
|
*/
|
|
private static array $tables = [];
|
|
|
|
/**
|
|
* Clears Model's static query-builder state before every test.
|
|
*
|
|
* A query that dies mid-flight (unsupported function, constraint
|
|
* violation, ...) never reaches Model's internal resetQuery(), so
|
|
* leftovers would leak into the next test otherwise. This keeps the
|
|
* suite order-independent regardless of PHPUnit's executionOrder.
|
|
*/
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
|
|
(new \ReflectionMethod(Model::class, 'resetQuery'))->invoke(null);
|
|
}
|
|
|
|
/**
|
|
* Returns the PDO connection to the in-memory test database.
|
|
*
|
|
* @return PDO
|
|
*/
|
|
protected static function db(): PDO
|
|
{
|
|
return Database::getInstance(DB_TYPE, DB_HOST, DB_NAME, DB_USER, DB_PASS);
|
|
}
|
|
|
|
/**
|
|
* DDL fragments per engine for each logical column type.
|
|
*
|
|
* @var array<string,array<string,string>>
|
|
*/
|
|
private const TYPE_DDL = [
|
|
'sqlite' => [
|
|
'pk' => 'INTEGER PRIMARY KEY AUTOINCREMENT',
|
|
'string' => 'VARCHAR(255)',
|
|
'text' => 'TEXT',
|
|
'number' => 'REAL',
|
|
'bool' => 'INTEGER',
|
|
'timestamp' => 'TEXT',
|
|
],
|
|
'mysql' => [
|
|
'pk' => 'INT AUTO_INCREMENT PRIMARY KEY',
|
|
'string' => 'VARCHAR(255)',
|
|
'text' => 'TEXT',
|
|
'number' => 'DOUBLE',
|
|
'bool' => 'TINYINT(1)',
|
|
'timestamp' => 'DATETIME',
|
|
],
|
|
'pgsql' => [
|
|
'pk' => 'SERIAL PRIMARY KEY',
|
|
'string' => 'VARCHAR(255)',
|
|
'text' => 'TEXT',
|
|
'number' => 'DOUBLE PRECISION',
|
|
'bool' => 'BOOLEAN',
|
|
'timestamp' => 'TIMESTAMP',
|
|
],
|
|
];
|
|
|
|
/**
|
|
* Creates a table in the test database from logical column types and
|
|
* registers it for cleanup. The DDL is translated per engine, so the
|
|
* same declaration works on sqlite, MySQL/MariaDB and PostgreSQL.
|
|
*
|
|
* @param string $name
|
|
* Table name (avoid SQL reserved words).
|
|
*
|
|
* @param array<string,string> $columns
|
|
* Map of column name to logical type:
|
|
* pk|string|text|number|bool|timestamp.
|
|
*/
|
|
protected static function createTable(string $name, array $columns): void
|
|
{
|
|
$types = self::TYPE_DDL[DB_TYPE] ?? throw new \InvalidArgumentException(
|
|
'No type translation for DB_TYPE ' . DB_TYPE
|
|
);
|
|
|
|
$ddl = [];
|
|
foreach ($columns as $column => $type) {
|
|
$fragment = $types[$type] ?? throw new \InvalidArgumentException(
|
|
"Unknown logical type '{$type}' for column '{$column}'; "
|
|
. 'expected: ' . implode(', ', array_keys($types))
|
|
);
|
|
$ddl[] = $column . ' ' . $fragment;
|
|
}
|
|
|
|
static::db()->exec("CREATE TABLE IF NOT EXISTS {$name} (" . implode(', ', $ddl) . ')');
|
|
static::$tables[] = $name;
|
|
}
|
|
|
|
/**
|
|
* Never let an aborted test leak an open transaction into the next one.
|
|
*/
|
|
protected function tearDown(): void
|
|
{
|
|
if (static::db()->inTransaction()) {
|
|
static::db()->rollBack();
|
|
}
|
|
|
|
parent::tearDown();
|
|
}
|
|
|
|
/**
|
|
* Drops every table registered through createTable() during this class.
|
|
*/
|
|
public static function tearDownAfterClass(): void
|
|
{
|
|
foreach (array_unique(static::$tables) as $table) {
|
|
static::db()->exec("DROP TABLE IF EXISTS {$table}");
|
|
}
|
|
|
|
static::$tables = [];
|
|
}
|
|
}
|