#!/usr/bin/env node // scripts/setup-test-db.mjs — DROP + CREATE 空测试库。 // 由 coding.mjs 的 test-gate 调用;schema 由 Flyway 在 Spring Boot 启动时重放。 // 只允许本地 host(或 TEST_DB_ALLOWED_HOSTS 白名单内的 host)+ 测试库名(含 test/_dev/_local/_ci)。 import { spawnSync } from 'node:child_process' import { existsSync, readFileSync } from 'node:fs' import { dirname, join } from 'node:path' import { fileURLToPath } from 'node:url' const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url)) const ENV_FILE = join(SCRIPT_DIR, '..', '.env.local') function parseEnv(text) { const env = {} for (const rawLine of text.split(/\r?\n/)) { const line = rawLine.trim() if (line === '' || line.startsWith('#')) continue const eq = line.indexOf('=') if (eq === -1) continue const key = line.slice(0, eq).trim() if (!key) continue let value = line.slice(eq + 1).trim() if ( value.length >= 2 && ((value.startsWith("'") && value.endsWith("'")) || (value.startsWith('"') && value.endsWith('"'))) ) { value = value.slice(1, -1) } env[key] = value } return env } if (!existsSync(ENV_FILE)) { console.error(`[setup-test-db] .env.local 不存在(${ENV_FILE})`) process.exit(1) } const env = parseEnv(readFileSync(ENV_FILE, 'utf8')) const DB_HOST = env.DB_HOST ?? '' const DB_PORT = env.DB_PORT ?? '3306' const DB_USER = env.DB_USER ?? '' const DB_PASSWORD = env.DB_PASSWORD ?? '' const DB_SCHEMA = env.DB_SCHEMA ?? '' // 防护 1:默认只允许本地 host(localhost / 127.0.0.1 / ::1)。 // 额外允许的远程 host 在 .env.local 的 TEST_DB_ALLOWED_HOSTS 中(空格或逗号分隔)。 const extraHosts = (env.TEST_DB_ALLOWED_HOSTS ?? '') .split(/[\s,]+/) .filter(Boolean) const allowedHosts = ['localhost', '127.0.0.1', '::1', ...extraHosts] if (!allowedHosts.includes(DB_HOST)) { console.error(`[setup-test-db] 拒绝在非白名单 host (${DB_HOST}) 上执行 DROP DATABASE`) console.error(` 当前白名单:${allowedHosts.join(' ')}`) console.error(' 加入 host:在 .env.local 追加 TEST_DB_ALLOWED_HOSTS=" "') process.exit(1) } // 防护 2:schema 名需像测试/开发库(含 test / _dev / _local / _ci),否则拒绝。 const schemaLooksLikeTest = /test/.test(DB_SCHEMA) || /_dev$/.test(DB_SCHEMA) || /_local$/.test(DB_SCHEMA) || /_ci$/.test(DB_SCHEMA) if (!schemaLooksLikeTest) { console.error( `[setup-test-db] schema '${DB_SCHEMA}' 不像测试库(期望命名含 test / _dev / _local / _ci)` ) process.exit(1) } console.log(`[setup-test-db] 即将 DROP + CREATE \`${DB_SCHEMA}\` on ${DB_HOST}:${DB_PORT}`) if (!['localhost', '127.0.0.1', '::1'].includes(DB_HOST)) { console.log( '[setup-test-db] 目标是 **远程** host(已在 TEST_DB_ALLOWED_HOSTS 白名单中,每次 test.mjs 都会 DROP)' ) console.log(`[setup-test-db] 当前白名单: ${allowedHosts.join(' ')}`) console.log( '[setup-test-db] 若不希望每次自动 DROP,从 .env.local 的 TEST_DB_ALLOWED_HOSTS 删掉此 host' ) } const sql = `DROP DATABASE IF EXISTS \`${DB_SCHEMA}\`; ` + `CREATE DATABASE \`${DB_SCHEMA}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;` const mysqlArgs = [ `-h${DB_HOST}`, `-P${DB_PORT}`, `-u${DB_USER}`, `-p${DB_PASSWORD}`, '-e', sql, ] const res = spawnSync('mysql', mysqlArgs, { stdio: 'inherit' }) if (res.error) { console.error(`[setup-test-db] FATAL: 无法执行 mysql(请确认其在 PATH 中): ${res.error.message}`) process.exit(1) } if (res.status !== 0) { console.error(`[setup-test-db] FAIL: mysql exit=${res.status}`) process.exit(res.status === null ? 1 : res.status) } console.log('[setup-test-db] done — schema will be applied by Flyway when Spring Boot starts')