@@ -31,7 +31,12 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GIT_SSL_NO_VERIFY: 'true'
|
GIT_SSL_NO_VERIFY: 'true'
|
||||||
CI_ENVIRONMENT: testing
|
CI_ENVIRONMENT: testing
|
||||||
database.tests.hostname: 127.0.0.1
|
TEST_DB_HOST: mysql
|
||||||
|
TEST_DB_PORT: 3306
|
||||||
|
TEST_DB_NAME: alrahma_test
|
||||||
|
TEST_DB_USER: alrahma
|
||||||
|
TEST_DB_PASSWORD: alrahma
|
||||||
|
database.tests.hostname: mysql
|
||||||
database.tests.database: alrahma_test
|
database.tests.database: alrahma_test
|
||||||
database.tests.username: alrahma
|
database.tests.username: alrahma
|
||||||
database.tests.password: alrahma
|
database.tests.password: alrahma
|
||||||
@@ -75,8 +80,92 @@ jobs:
|
|||||||
touch writable/cache/testing/.ci-write-test
|
touch writable/cache/testing/.ci-write-test
|
||||||
rm writable/cache/testing/.ci-write-test
|
rm writable/cache/testing/.ci-write-test
|
||||||
|
|
||||||
|
- name: Verify database configuration
|
||||||
|
run: |
|
||||||
|
php -d variables_order=EGPCS -r '
|
||||||
|
define("FCPATH", __DIR__ . "/public/");
|
||||||
|
define("ENVIRONMENT", getenv("CI_ENVIRONMENT") ?: "production");
|
||||||
|
require "app/Config/Paths.php";
|
||||||
|
$paths = new Config\Paths();
|
||||||
|
require $paths->systemDirectory . "/Boot.php";
|
||||||
|
CodeIgniter\Boot::bootConsole($paths);
|
||||||
|
|
||||||
|
$database = config("Database");
|
||||||
|
$dbConfig = $database->{$database->defaultGroup};
|
||||||
|
|
||||||
|
echo "Environment: " . ENVIRONMENT . PHP_EOL;
|
||||||
|
echo "DB group: " . $database->defaultGroup . PHP_EOL;
|
||||||
|
echo "DB host: " . $dbConfig["hostname"] . PHP_EOL;
|
||||||
|
echo "DB port: " . $dbConfig["port"] . PHP_EOL;
|
||||||
|
echo "DB name: " . $dbConfig["database"] . PHP_EOL;
|
||||||
|
'
|
||||||
|
|
||||||
|
- name: Wait for MariaDB
|
||||||
|
run: |
|
||||||
|
for attempt in $(seq 1 30); do
|
||||||
|
php -r '
|
||||||
|
mysqli_report(MYSQLI_REPORT_OFF);
|
||||||
|
$db = @new mysqli(
|
||||||
|
getenv("TEST_DB_HOST"),
|
||||||
|
getenv("TEST_DB_USER"),
|
||||||
|
getenv("TEST_DB_PASSWORD"),
|
||||||
|
getenv("TEST_DB_NAME"),
|
||||||
|
(int) getenv("TEST_DB_PORT")
|
||||||
|
);
|
||||||
|
|
||||||
|
exit($db->connect_errno === 0 ? 0 : 1);
|
||||||
|
' && break
|
||||||
|
|
||||||
|
echo "Waiting for MariaDB, attempt ${attempt}/30"
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
|
||||||
|
php -r '
|
||||||
|
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
|
||||||
|
|
||||||
|
new mysqli(
|
||||||
|
getenv("TEST_DB_HOST"),
|
||||||
|
getenv("TEST_DB_USER"),
|
||||||
|
getenv("TEST_DB_PASSWORD"),
|
||||||
|
getenv("TEST_DB_NAME"),
|
||||||
|
(int) getenv("TEST_DB_PORT")
|
||||||
|
);
|
||||||
|
|
||||||
|
echo "MariaDB connection successful\n";
|
||||||
|
'
|
||||||
|
|
||||||
|
- name: Import baseline schema
|
||||||
|
run: php tests/_support/import_baseline.php Database_Dump/u280815660_school_12072025
|
||||||
|
|
||||||
- name: Run database migrations
|
- name: Run database migrations
|
||||||
run: php spark migrate --all --no-interaction
|
run: |
|
||||||
|
php -d variables_order=EGPCS spark migrate --all --no-interaction
|
||||||
|
php -r '
|
||||||
|
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
|
||||||
|
|
||||||
|
$db = new mysqli(
|
||||||
|
getenv("TEST_DB_HOST"),
|
||||||
|
getenv("TEST_DB_USER"),
|
||||||
|
getenv("TEST_DB_PASSWORD"),
|
||||||
|
getenv("TEST_DB_NAME"),
|
||||||
|
(int) getenv("TEST_DB_PORT")
|
||||||
|
);
|
||||||
|
|
||||||
|
$tables = [];
|
||||||
|
$result = $db->query("SHOW TABLES");
|
||||||
|
while ($row = $result->fetch_array(MYSQLI_NUM)) {
|
||||||
|
$tables[$row[0]] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (["users", "students", "attendance_data"] as $table) {
|
||||||
|
if (!isset($tables[$table])) {
|
||||||
|
fwrite(STDERR, "Migration schema missing required table: {$table}\n");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Migration schema verified\n";
|
||||||
|
'
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: composer test
|
run: composer test
|
||||||
|
|||||||
+12
-8
@@ -16,6 +16,10 @@ class Database extends Config
|
|||||||
{
|
{
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
|
|
||||||
|
if (ENVIRONMENT === 'testing') {
|
||||||
|
$this->defaultGroup = 'tests';
|
||||||
|
}
|
||||||
|
|
||||||
$this->default = [
|
$this->default = [
|
||||||
'DSN' => '',
|
'DSN' => '',
|
||||||
'hostname' => env('database.default.hostname'),
|
'hostname' => env('database.default.hostname'),
|
||||||
@@ -38,22 +42,22 @@ class Database extends Config
|
|||||||
|
|
||||||
$this->tests = [
|
$this->tests = [
|
||||||
'DSN' => '',
|
'DSN' => '',
|
||||||
'hostname' => env('database.tests.hostname', env('database.default.hostname')),
|
'hostname' => env('TEST_DB_HOST', env('database.tests.hostname', env('database.default.hostname'))),
|
||||||
'username' => env('database.tests.username', env('database.default.username')),
|
'username' => env('TEST_DB_USER', env('database.tests.username', env('database.default.username'))),
|
||||||
'password' => env('database.tests.password', env('database.default.password')),
|
'password' => env('TEST_DB_PASSWORD', env('database.tests.password', env('database.default.password'))),
|
||||||
'database' => env('database.tests.database', env('database.default.database')),
|
'database' => env('TEST_DB_NAME', env('database.tests.database', env('database.default.database'))),
|
||||||
'DBDriver' => env('database.tests.DBDriver', env('database.default.DBDriver', 'MySQLi')),
|
'DBDriver' => env('database.tests.DBDriver', env('database.default.DBDriver', 'MySQLi')),
|
||||||
'DBPrefix' => env('database.tests.DBPrefix', 'db_'),
|
'DBPrefix' => env('database.tests.DBPrefix', ''),
|
||||||
'pConnect' => false,
|
'pConnect' => false,
|
||||||
'DBDebug' => true,
|
'DBDebug' => true,
|
||||||
'charset' => 'utf8',
|
'charset' => 'utf8mb4',
|
||||||
'DBCollat' => 'utf8_general_ci',
|
'DBCollat' => 'utf8mb4_general_ci',
|
||||||
'swapPre' => '',
|
'swapPre' => '',
|
||||||
'encrypt' => false,
|
'encrypt' => false,
|
||||||
'compress' => false,
|
'compress' => false,
|
||||||
'strictOn' => false,
|
'strictOn' => false,
|
||||||
'failover' => [],
|
'failover' => [],
|
||||||
'port' => (int) env('database.tests.port', env('database.default.port', 3306)),
|
'port' => (int) env('TEST_DB_PORT', env('database.tests.port', env('database.default.port', 3306))),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ class FixPrintRequestsForeignKey extends Migration
|
|||||||
// Drop the old foreign key if it exists
|
// Drop the old foreign key if it exists
|
||||||
$this->forge->dropForeignKey('print_requests', 'print_requests_class_id_foreign');
|
$this->forge->dropForeignKey('print_requests', 'print_requests_class_id_foreign');
|
||||||
|
|
||||||
|
if (! $this->db->tableExists('class_sections')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Add the new foreign key
|
// Add the new foreign key
|
||||||
$this->forge->addForeignKey('class_id', 'class_sections', 'id', 'CASCADE', 'CASCADE');
|
$this->forge->addForeignKey('class_id', 'class_sections', 'id', 'CASCADE', 'CASCADE');
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,14 @@ class RevertPrintRequestsForeignKey extends Migration
|
|||||||
public function up()
|
public function up()
|
||||||
{
|
{
|
||||||
// Drop the old foreign key if it exists
|
// Drop the old foreign key if it exists
|
||||||
$this->forge->dropForeignKey('print_requests', 'print_requests_class_id_foreign');
|
$db = \Config\Database::connect();
|
||||||
|
$keys = $db->getForeignKeyData('print_requests');
|
||||||
|
foreach ($keys as $key) {
|
||||||
|
if ($key->constraint_name === 'print_requests_class_id_foreign') {
|
||||||
|
$this->forge->dropForeignKey('print_requests', 'print_requests_class_id_foreign');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Add the new foreign key
|
// Add the new foreign key
|
||||||
$this->forge->addForeignKey('class_id', 'classes', 'id', 'CASCADE', 'CASCADE');
|
$this->forge->addForeignKey('class_id', 'classes', 'id', 'CASCADE', 'CASCADE');
|
||||||
|
|||||||
@@ -12,8 +12,7 @@ class FixPrintRequestsForeignKeyAgain extends Migration
|
|||||||
$keys = $db->getForeignKeyData('print_requests');
|
$keys = $db->getForeignKeyData('print_requests');
|
||||||
foreach ($keys as $key) {
|
foreach ($keys as $key) {
|
||||||
if ($key->constraint_name === 'print_requests_class_id_foreign') {
|
if ($key->constraint_name === 'print_requests_class_id_foreign') {
|
||||||
$this->forge->dropForeignKey('print_requests', 'print_requests_class_id_foreign');
|
return;
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,10 +8,18 @@ class RemoveClassAssignmentSemester extends Migration
|
|||||||
{
|
{
|
||||||
public function up()
|
public function up()
|
||||||
{
|
{
|
||||||
if ($this->db->tableExists('teacher_class')) {
|
if ($this->db->tableExists('teacher_class') && $this->db->fieldExists('semester', 'teacher_class')) {
|
||||||
|
if ($this->indexExists('teacher_class', 'unique_teacher_assignment')) {
|
||||||
|
$this->db->query('ALTER TABLE `teacher_class` DROP INDEX `unique_teacher_assignment`');
|
||||||
|
}
|
||||||
|
|
||||||
$this->forge->dropColumn('teacher_class', 'semester');
|
$this->forge->dropColumn('teacher_class', 'semester');
|
||||||
|
|
||||||
|
$this->db->query(
|
||||||
|
'ALTER TABLE `teacher_class` ADD UNIQUE KEY `unique_teacher_assignment` (`teacher_id`, `class_section_id`, `school_year`)'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if ($this->db->tableExists('student_class')) {
|
if ($this->db->tableExists('student_class') && $this->db->fieldExists('semester', 'student_class')) {
|
||||||
$this->forge->dropColumn('student_class', 'semester');
|
$this->forge->dropColumn('student_class', 'semester');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -39,4 +47,13 @@ class RemoveClassAssignmentSemester extends Migration
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function indexExists(string $table, string $index): bool
|
||||||
|
{
|
||||||
|
$result = $this->db->query(
|
||||||
|
'SHOW INDEX FROM `' . str_replace('`', '``', $table) . '` WHERE Key_name = ' . $this->db->escape($index)
|
||||||
|
);
|
||||||
|
|
||||||
|
return $result->getNumRows() > 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -3,6 +3,7 @@
|
|||||||
namespace App\Database\Migrations;
|
namespace App\Database\Migrations;
|
||||||
|
|
||||||
use CodeIgniter\Database\Migration;
|
use CodeIgniter\Database\Migration;
|
||||||
|
use CodeIgniter\Database\RawSql;
|
||||||
|
|
||||||
class CreateTeacherSubmissionNotificationHistory extends Migration
|
class CreateTeacherSubmissionNotificationHistory extends Migration
|
||||||
{
|
{
|
||||||
@@ -56,7 +57,7 @@ class CreateTeacherSubmissionNotificationHistory extends Migration
|
|||||||
'sent_at' => [
|
'sent_at' => [
|
||||||
'type' => 'DATETIME',
|
'type' => 'DATETIME',
|
||||||
'null' => false,
|
'null' => false,
|
||||||
'default' => 'CURRENT_TIMESTAMP',
|
'default' => new RawSql('CURRENT_TIMESTAMP'),
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
namespace App\Database\Migrations;
|
namespace App\Database\Migrations;
|
||||||
|
|
||||||
use CodeIgniter\Database\Migration;
|
use CodeIgniter\Database\Migration;
|
||||||
|
use CodeIgniter\Database\RawSql;
|
||||||
|
|
||||||
class CreateExamDraftSubmissions extends Migration
|
class CreateExamDraftSubmissions extends Migration
|
||||||
{
|
{
|
||||||
@@ -89,7 +90,7 @@ class CreateExamDraftSubmissions extends Migration
|
|||||||
'created_at' => [
|
'created_at' => [
|
||||||
'type' => 'DATETIME',
|
'type' => 'DATETIME',
|
||||||
'null' => false,
|
'null' => false,
|
||||||
'default' => 'CURRENT_TIMESTAMP',
|
'default' => new RawSql('CURRENT_TIMESTAMP'),
|
||||||
],
|
],
|
||||||
'updated_at' => [
|
'updated_at' => [
|
||||||
'type' => 'DATETIME',
|
'type' => 'DATETIME',
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class CreatePlacementLevels extends Migration
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
$this->forge->addKey('id', true);
|
$this->forge->addKey('id', true);
|
||||||
$this->forge->addKey(['student_id', 'school_year'], true);
|
$this->forge->addUniqueKey(['student_id', 'school_year'], 'unique_student_school_year');
|
||||||
$this->forge->addKey('school_year');
|
$this->forge->addKey('school_year');
|
||||||
$this->forge->createTable('placement_levels');
|
$this->forge->createTable('placement_levels');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class CreatePlacementScores extends Migration
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
$this->forge->addKey('id', true);
|
$this->forge->addKey('id', true);
|
||||||
$this->forge->addKey(['batch_id', 'student_id'], true);
|
$this->forge->addUniqueKey(['batch_id', 'student_id'], 'unique_batch_student');
|
||||||
$this->forge->addKey('batch_id');
|
$this->forge->addKey('batch_id');
|
||||||
$this->forge->createTable('placement_scores');
|
$this->forge->createTable('placement_scores');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,7 +65,10 @@ class CreateMissingScoreOverrides extends Migration
|
|||||||
|
|
||||||
$this->forge->addKey('id', true);
|
$this->forge->addKey('id', true);
|
||||||
$this->forge->addKey(['class_section_id', 'semester', 'school_year', 'item_type'], false, 'missing_score_overrides_scope');
|
$this->forge->addKey(['class_section_id', 'semester', 'school_year', 'item_type'], false, 'missing_score_overrides_scope');
|
||||||
$this->forge->addKey(['student_id', 'class_section_id', 'semester', 'school_year', 'item_type', 'item_index'], true, 'missing_score_overrides_unique');
|
$this->forge->addUniqueKey(
|
||||||
|
['student_id', 'class_section_id', 'semester', 'school_year', 'item_type', 'item_index'],
|
||||||
|
'missing_score_overrides_unique'
|
||||||
|
);
|
||||||
$this->forge->createTable('missing_score_overrides', true);
|
$this->forge->createTable('missing_score_overrides', true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,13 +10,18 @@ class AddStyleToPreferences extends Migration
|
|||||||
{
|
{
|
||||||
if ($this->db->tableExists('user_preferences')) {
|
if ($this->db->tableExists('user_preferences')) {
|
||||||
if (! $this->db->fieldExists('style_color', 'user_preferences')) {
|
if (! $this->db->fieldExists('style_color', 'user_preferences')) {
|
||||||
|
$styleColor = [
|
||||||
|
'type' => 'VARCHAR',
|
||||||
|
'constraint' => 32,
|
||||||
|
'null' => true,
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($this->db->fieldExists('timezone', 'user_preferences')) {
|
||||||
|
$styleColor['after'] = 'timezone';
|
||||||
|
}
|
||||||
|
|
||||||
$this->forge->addColumn('user_preferences', [
|
$this->forge->addColumn('user_preferences', [
|
||||||
'style_color' => [
|
'style_color' => $styleColor,
|
||||||
'type' => 'VARCHAR',
|
|
||||||
'constraint' => 32,
|
|
||||||
'null' => true,
|
|
||||||
'after' => 'timezone',
|
|
||||||
],
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
if (! $this->db->fieldExists('menu_color', 'user_preferences')) {
|
if (! $this->db->fieldExists('menu_color', 'user_preferences')) {
|
||||||
|
|||||||
@@ -122,6 +122,8 @@ class FixSchoolYearColumns extends Migration
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->db->resetDataCache();
|
||||||
|
|
||||||
// These annual entities lacked a school_year column in the audited schema.
|
// These annual entities lacked a school_year column in the audited schema.
|
||||||
foreach (['class_progress_reports', 'exams', 'print_requests'] as $table) {
|
foreach (['class_progress_reports', 'exams', 'print_requests'] as $table) {
|
||||||
if ($this->db->tableExists($table) && ! $this->db->fieldExists('school_year', $table)) {
|
if ($this->db->tableExists($table) && ! $this->db->fieldExists('school_year', $table)) {
|
||||||
@@ -137,6 +139,8 @@ class FixSchoolYearColumns extends Migration
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->db->resetDataCache();
|
||||||
|
|
||||||
// Validate and normalize every direct owner to VARCHAR(9).
|
// Validate and normalize every direct owner to VARCHAR(9).
|
||||||
foreach ($this->yearOwnerTables as $table) {
|
foreach ($this->yearOwnerTables as $table) {
|
||||||
if (! $this->db->tableExists($table) || ! $this->db->fieldExists('school_year', $table)) {
|
if (! $this->db->tableExists($table) || ! $this->db->fieldExists('school_year', $table)) {
|
||||||
@@ -163,6 +167,8 @@ class FixSchoolYearColumns extends Migration
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->db->resetDataCache();
|
||||||
|
|
||||||
// Remove redundant columns. Dependent non-primary indexes are removed first.
|
// Remove redundant columns. Dependent non-primary indexes are removed first.
|
||||||
foreach ($this->tablesWithoutDirectYear as $table) {
|
foreach ($this->tablesWithoutDirectYear as $table) {
|
||||||
if (! $this->db->tableExists($table) || ! $this->db->fieldExists('school_year', $table)) {
|
if (! $this->db->tableExists($table) || ! $this->db->fieldExists('school_year', $table)) {
|
||||||
@@ -172,6 +178,7 @@ class FixSchoolYearColumns extends Migration
|
|||||||
$this->dropIndexesContainingColumn($table, 'school_year');
|
$this->dropIndexesContainingColumn($table, 'school_year');
|
||||||
$this->dropChecksContainingColumn($table, 'school_year');
|
$this->dropChecksContainingColumn($table, 'school_year');
|
||||||
$this->forge->dropColumn($table, 'school_year');
|
$this->forge->dropColumn($table, 'school_year');
|
||||||
|
$this->db->resetDataCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($this->tablesWithoutDirectSemester as $table) {
|
foreach ($this->tablesWithoutDirectSemester as $table) {
|
||||||
@@ -182,8 +189,11 @@ class FixSchoolYearColumns extends Migration
|
|||||||
$this->dropIndexesContainingColumn($table, 'semester');
|
$this->dropIndexesContainingColumn($table, 'semester');
|
||||||
$this->dropChecksContainingColumn($table, 'semester');
|
$this->dropChecksContainingColumn($table, 'semester');
|
||||||
$this->forge->dropColumn($table, 'semester');
|
$this->forge->dropColumn($table, 'semester');
|
||||||
|
$this->db->resetDataCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->db->resetDataCache();
|
||||||
|
|
||||||
foreach ($this->yearOwnerTables as $table) {
|
foreach ($this->yearOwnerTables as $table) {
|
||||||
if (! $this->db->tableExists($table) || ! $this->db->fieldExists('school_year', $table)) {
|
if (! $this->db->tableExists($table) || ! $this->db->fieldExists('school_year', $table)) {
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
+1
-1
@@ -49,6 +49,6 @@
|
|||||||
"sort-packages": true
|
"sort-packages": true
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "phpunit"
|
"test": "php -d variables_order=EGPCS vendor/bin/phpunit"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,19 +2,39 @@
|
|||||||
|
|
||||||
namespace Tests\Support;
|
namespace Tests\Support;
|
||||||
|
|
||||||
use CodeIgniter\Test\CIUnitTestCase;
|
|
||||||
use Config\Database;
|
use Config\Database;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
class DBReset
|
class DBReset
|
||||||
{
|
{
|
||||||
|
private const DB_GROUP = 'tests';
|
||||||
|
|
||||||
protected static array $excludeTables = ['migrations'];
|
protected static array $excludeTables = ['migrations'];
|
||||||
|
|
||||||
public static function resetDatabase(): void
|
public static function resetDatabase(): void
|
||||||
{
|
{
|
||||||
$db = Database::connect();
|
$config = config('Database');
|
||||||
|
$dbConfig = $config->{self::DB_GROUP} ?? [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = Database::connect(self::DB_GROUP);
|
||||||
|
$db->initialize();
|
||||||
|
} catch (Throwable $exception) {
|
||||||
|
throw new \RuntimeException(
|
||||||
|
sprintf(
|
||||||
|
'Test database connection failed: group=%s host=%s port=%s database=%s',
|
||||||
|
self::DB_GROUP,
|
||||||
|
$dbConfig['hostname'] ?? '',
|
||||||
|
$dbConfig['port'] ?? '',
|
||||||
|
$dbConfig['database'] ?? ''
|
||||||
|
),
|
||||||
|
0,
|
||||||
|
$exception
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
$db->disableForeignKeyChecks();
|
$db->disableForeignKeyChecks();
|
||||||
|
|
||||||
// Get all table names
|
|
||||||
$tables = $db->listTables();
|
$tables = $db->listTables();
|
||||||
|
|
||||||
foreach ($tables as $table) {
|
foreach ($tables as $table) {
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
if (PHP_SAPI !== 'cli') {
|
||||||
|
fwrite(STDERR, "This script must be run from the command line.\n");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$dumpPath = $argv[1] ?? null;
|
||||||
|
|
||||||
|
if ($dumpPath === null || ! is_file($dumpPath) || ! is_readable($dumpPath)) {
|
||||||
|
fwrite(STDERR, "Usage: php tests/_support/import_baseline.php path/to/dump.sql\n");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$requiredEnv = [
|
||||||
|
'TEST_DB_HOST',
|
||||||
|
'TEST_DB_USER',
|
||||||
|
'TEST_DB_PASSWORD',
|
||||||
|
'TEST_DB_NAME',
|
||||||
|
'TEST_DB_PORT',
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($requiredEnv as $name) {
|
||||||
|
if (getenv($name) === false || getenv($name) === '') {
|
||||||
|
fwrite(STDERR, "Missing required environment variable: {$name}\n");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
|
||||||
|
|
||||||
|
$db = new mysqli(
|
||||||
|
getenv('TEST_DB_HOST'),
|
||||||
|
getenv('TEST_DB_USER'),
|
||||||
|
getenv('TEST_DB_PASSWORD'),
|
||||||
|
getenv('TEST_DB_NAME'),
|
||||||
|
(int) getenv('TEST_DB_PORT')
|
||||||
|
);
|
||||||
|
|
||||||
|
$db->set_charset('utf8mb4');
|
||||||
|
$db->query('SET FOREIGN_KEY_CHECKS=0');
|
||||||
|
|
||||||
|
$tables = $db->query('SHOW FULL TABLES WHERE Table_type = "BASE TABLE"')->fetch_all(MYSQLI_NUM);
|
||||||
|
|
||||||
|
foreach ($tables as [$table]) {
|
||||||
|
$db->query('DROP TABLE IF EXISTS `' . str_replace('`', '``', $table) . '`');
|
||||||
|
}
|
||||||
|
|
||||||
|
$db->query('SET FOREIGN_KEY_CHECKS=1');
|
||||||
|
|
||||||
|
$sql = file_get_contents($dumpPath);
|
||||||
|
|
||||||
|
if ($sql === false) {
|
||||||
|
fwrite(STDERR, "Unable to read baseline dump: {$dumpPath}\n");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$db->multi_query($sql);
|
||||||
|
|
||||||
|
do {
|
||||||
|
if ($result = $db->store_result()) {
|
||||||
|
$result->free();
|
||||||
|
}
|
||||||
|
} while ($db->more_results() && $db->next_result());
|
||||||
|
|
||||||
|
$baselineVersion = '2025-12-07-235959';
|
||||||
|
$migrationFiles = glob(dirname(__DIR__, 2) . '/app/Database/Migrations/*.php') ?: [];
|
||||||
|
$maxId = (int) ($db->query('SELECT COALESCE(MAX(id), 0) AS id FROM migrations')->fetch_assoc()['id'] ?? 0);
|
||||||
|
$maxBatch = (int) ($db->query('SELECT COALESCE(MAX(batch), 0) AS batch FROM migrations')->fetch_assoc()['batch'] ?? 0);
|
||||||
|
|
||||||
|
foreach ($migrationFiles as $file) {
|
||||||
|
$filename = basename($file, '.php');
|
||||||
|
|
||||||
|
if (! preg_match('/^(\d{4}-\d{2}-\d{2}-\d{6})_(.+)$/', $filename, $matches)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
[$all, $version] = $matches;
|
||||||
|
|
||||||
|
if ($version > $baselineVersion) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$contents = file_get_contents($file);
|
||||||
|
|
||||||
|
if ($contents === false || ! preg_match('/class\s+([A-Za-z_][A-Za-z0-9_]*)\s+extends\s+Migration/', $contents, $classMatch)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$class = 'App\\Database\\Migrations\\' . $classMatch[1];
|
||||||
|
$exists = $db
|
||||||
|
->query(
|
||||||
|
'SELECT 1 FROM migrations WHERE version = "' . $db->real_escape_string($version) . '"'
|
||||||
|
. ' AND class = "' . $db->real_escape_string($class) . '" LIMIT 1'
|
||||||
|
)
|
||||||
|
->num_rows > 0;
|
||||||
|
|
||||||
|
if ($exists) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$date = DateTimeImmutable::createFromFormat('Y-m-d-His', $version);
|
||||||
|
$time = $date instanceof DateTimeImmutable ? $date->getTimestamp() : time();
|
||||||
|
|
||||||
|
$statement = $db->prepare(
|
||||||
|
'INSERT INTO migrations (id, version, class, `group`, namespace, time, batch) VALUES (?, ?, ?, ?, ?, ?, ?)'
|
||||||
|
);
|
||||||
|
|
||||||
|
$id = ++$maxId;
|
||||||
|
$group = 'default';
|
||||||
|
$namespace = 'App';
|
||||||
|
$batch = $maxBatch;
|
||||||
|
$statement->bind_param('issssii', $id, $version, $class, $group, $namespace, $time, $batch);
|
||||||
|
$statement->execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
$count = $db->query('SHOW TABLES')->num_rows;
|
||||||
|
|
||||||
|
if ($count === 0) {
|
||||||
|
fwrite(STDERR, "Baseline import produced no tables\n");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Baseline schema imported: {$count} table(s)\n";
|
||||||
@@ -16,25 +16,48 @@ class AttendanceDataModelTest extends CIUnitTestCase
|
|||||||
|
|
||||||
public function testCanInsertAndRetrieve()
|
public function testCanInsertAndRetrieve()
|
||||||
{
|
{
|
||||||
$record = fake(AttendanceDataModel::class);
|
$model = new AttendanceDataModel();
|
||||||
$this->assertNotNull($record->id);
|
$id = $model->insert($this->validAttendanceRow());
|
||||||
$fetched = (new AttendanceDataModel())->find($record->id);
|
|
||||||
$this->assertEquals($record->id, $fetched->id);
|
$this->assertNotFalse($id);
|
||||||
|
|
||||||
|
$fetched = $model->find($id);
|
||||||
|
$this->assertSame((int) $id, (int) $fetched['id']);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testCanUpdate()
|
public function testCanUpdate()
|
||||||
{
|
{
|
||||||
$record = fake(AttendanceDataModel::class);
|
|
||||||
$model = new AttendanceDataModel();
|
$model = new AttendanceDataModel();
|
||||||
$model->update($record->id, ['updated_at' => date('Y-m-d H:i:s')]);
|
$id = $model->insert($this->validAttendanceRow());
|
||||||
$this->assertTrue(true); // simple test to verify no exception thrown
|
|
||||||
|
$this->assertTrue($model->update($id, ['updated_at' => date('Y-m-d H:i:s')]));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testCanDelete()
|
public function testCanDelete()
|
||||||
{
|
{
|
||||||
$record = fake(AttendanceDataModel::class);
|
|
||||||
$model = new AttendanceDataModel();
|
$model = new AttendanceDataModel();
|
||||||
$model->delete($record->id);
|
$id = $model->insert($this->validAttendanceRow());
|
||||||
$this->assertNull($model->find($record->id));
|
|
||||||
|
$model->delete($id);
|
||||||
|
|
||||||
|
$this->assertNull($model->find($id));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function validAttendanceRow(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'class_id' => 1,
|
||||||
|
'class_section_id' => 1,
|
||||||
|
'school_id' => 1,
|
||||||
|
'student_id' => 1,
|
||||||
|
'date' => '2026-01-01',
|
||||||
|
'status' => 'present',
|
||||||
|
'reason' => '',
|
||||||
|
'is_reported' => 'no',
|
||||||
|
'is_notified' => 'no',
|
||||||
|
'semester' => 'Fall',
|
||||||
|
'school_year' => '2025-2026',
|
||||||
|
'modified_by' => 1,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user