From c9c08dea56ab2afe1a9314655a4ad123b7a1c664 Mon Sep 17 00:00:00 2001 From: bugfix-subagent Date: Sun, 16 Aug 2026 19:01:46 +0000 Subject: [PATCH] test(api): wire up real mysqli in SchemaHealthCheckTest (TRU-77 unit CI) The Unit suite starts with a freshly-dropped test database and does not create a $db global, so: - customer_invoice_email_schema_bootstrap::ensureSchema() short- circuited and never created the users table. - runSchemaCheck had nothing to inspect and reported ok=false. Wire up a real mysqli connection at file load time using the same CONFIG_DB_* env vars the rest of the CI suite exports, then have beforeEach run the bootstrap and create the bare-minimum invoices / bookings tables that runSchemaCheck verifies. This keeps the test self-contained inside the Unit suite without changing the production code or the production schema. Refs: api#383, TRU-77 --- .../app/tests/Unit/SchemaHealthCheckTest.php | 119 ++++++++++++++++++ 1 file changed, 119 insertions(+) diff --git a/services/nginx/app/tests/Unit/SchemaHealthCheckTest.php b/services/nginx/app/tests/Unit/SchemaHealthCheckTest.php index 5be1fb62..8bd3a6ef 100644 --- a/services/nginx/app/tests/Unit/SchemaHealthCheckTest.php +++ b/services/nginx/app/tests/Unit/SchemaHealthCheckTest.php @@ -12,6 +12,8 @@ * endpoint in `adminRoute.php` which the deploy pipeline hits. */ +app_require('classes/customer_invoice_email_schema_bootstrap.php'); + use classes\customer_invoice_email_schema_bootstrap; const REQUIRED_USERS_COLUMNS = [ @@ -28,13 +30,130 @@ const REQUIRED_USERS_COLUMNS = [ 'created_at', ]; +/** + * The unit test bootstrap does not create a $db global. This contract + * test is unique in that it needs a real database to verify schema + * state, so wire one up here using the same CONFIG_DB_* env vars the + * rest of the CI suite exports. If the database is unavailable, the + * tests below will fail with a clear "no_db_connection" error. + */ +schema_health_check_test_wire_db(); + +function schema_health_check_test_wire_db(): void +{ + if (isset($GLOBALS['db']) && is_object($GLOBALS['db'])) { + return; + } + if (!class_exists('mysqli')) { + return; + } + + $host = (string)(getenv('CONFIG_DB_HOST') ?: 'mysql-debug'); + $user = (string)(getenv('CONFIG_DB_USER') ?: 'root'); + $password = (string)(getenv('CONFIG_DB_PASSWORD') ?: 'debug_root_password'); + $database = (string)(getenv('CONFIG_DB_DATABASE') ?: 'nnks_db_debug'); + $port = (int)(getenv('CONFIG_DB_PORT') ?: 3306); + + try { + mysqli_report(MYSQLI_REPORT_OFF); + $conn = new mysqli($host, $user, $password, $database, $port); + if ($conn->connect_errno) { + return; + } + $conn->set_charset('utf8mb4'); + } catch (\Throwable $e) { + return; + } + + $GLOBALS['db'] = new class($conn) { + private mysqli $conn; + + public function __construct(mysqli $conn) + { + $this->conn = $conn; + } + + public function query(string $sql) + { + return $this->conn->query($sql); + } + + public function fetch_assoc($result) + { + return $result ? $result->fetch_assoc() : null; + } + + public function close(): void + { + try { + $this->conn->close(); + } catch (\Throwable) { + } + } + }; +} + +/** + * The unit suite starts with a freshly-dropped `nnks_db_debug` database + * (see run_ci_suite::reset_ci_state). The schema bootstrap only owns + * the `users` table; `invoices` and `bookings` are managed by other + * migrations that don't run in the unit suite. Create the bare-minimum + * schema that adminRoute::runSchemaCheck needs so the third test can + * verify the "all columns exist" happy path. + */ +function schema_health_check_test_ensure_aux_tables(): void +{ + global $db; + if (!isset($db) || !is_object($db) || !method_exists($db, 'query')) { + return; + } + + $create = function (string $table, string $createSql, array $requiredColumns) use ($db): void { + $r = $db->query("SHOW TABLES LIKE '{$table}'"); + if (!$r || (int)$r->num_rows === 0) { + $db->query($createSql); + return; + } + foreach ($requiredColumns as $column => $definition) { + $r = $db->query("SHOW COLUMNS FROM `{$table}` LIKE '{$column}'"); + if (!$r || (int)$r->num_rows === 0) { + $db->query("ALTER TABLE `{$table}` ADD COLUMN `{$column}` {$definition}"); + } + } + }; + + $create('invoices', "CREATE TABLE `invoices` ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + customer_number INT NOT NULL DEFAULT 0, + po_number VARCHAR(64) NULL, + closed_at DATETIME NULL + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci", [ + 'po_number' => 'VARCHAR(64) NULL', + 'closed_at' => 'DATETIME NULL', + 'customer_number' => 'INT NOT NULL DEFAULT 0', + ]); + + $create('bookings', "CREATE TABLE `bookings` ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + customer_number INT NOT NULL DEFAULT 0, + department INT NULL + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci", [ + 'customer_number' => 'INT NOT NULL DEFAULT 0', + 'department' => 'INT NULL', + ]); +} + beforeEach(function () { // Self-heal: run the schema bootstrap so the test DB has all // the columns the contract requires. The bootstrap is additive // and idempotent — safe to run on every test. + if (!isset($GLOBALS['db']) || !is_object($GLOBALS['db'])) { + schema_health_check_test_wire_db(); + } if (class_exists(customer_invoice_email_schema_bootstrap::class)) { customer_invoice_email_schema_bootstrap::ensureSchema(); } + schema_health_check_test_ensure_aux_tables(); }); it('users table has every required column the code references', function () {