setup-test-db.mjs 4.06 KB
#!/usr/bin/env node
// scripts/setup-test-db.mjs — DROP + CREATE 测试库。
// 由 coding.mjs 的 test-gate 调用;schema 由 Flyway 在 Spring Boot 启动时重放。
// DB 凭据从仓库根 config-vars.yaml 的 database: 段读取:schema 经标识符校验后才拼进 SQL(防误删 / 注入,见下方守卫);
// host / user / password 信任该文件,port 仅校验范围。

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 CONFIG_FILE = join(SCRIPT_DIR, '..', 'config-vars.yaml')

// 极简 YAML 读取(2 层 map + 标量;与插件 lib/yaml-config.mjs 同规则,内联以免运行时依赖)。
function parseScalar(raw) {
  let s = String(raw).trim()
  if (s === '' || s[0] === '#') return ''
  const q = s[0]
  if (q === '"' || q === "'") {
    const end = s.indexOf(q, 1)
    if (end !== -1) return s.slice(1, end)
  }
  const hash = s.indexOf(' #')
  if (hash !== -1) s = s.slice(0, hash).trim()
  return s
}
function parseYamlConfig(text) {
  const root = {}
  let section = null
  for (const rawLine of text.split('\n')) {
    const line = rawLine.replace(/\r$/, '')
    const trimmed = line.trim()
    if (trimmed === '' || trimmed[0] === '#') continue
    const colon = line.indexOf(':')
    if (colon === -1) continue
    const key = line.slice(0, colon).trim()
    if (key === '') continue
    const indent = line.length - line.replace(/^\s+/, '').length
    const value = parseScalar(line.slice(colon + 1))
    if (indent === 0) {
      if (value === '') {
        section = {}
        root[key] = section
      } else {
        root[key] = value
        section = null
      }
    } else if (section) {
      section[key] = value
    } else {
      root[key] = value
    }
  }
  return root
}

if (!existsSync(CONFIG_FILE)) {
  console.error(`[setup-test-db] config-vars.yaml 不存在(${CONFIG_FILE})`)
  process.exit(1)
}

const db = parseYamlConfig(readFileSync(CONFIG_FILE, 'utf8')).database || {}

const DB_HOST = db.host ?? ''
const DB_PORT = db.port ?? '3306'
const DB_USER = db.user ?? ''
const DB_PASSWORD = db.password ?? ''
const DB_SCHEMA = db.schema ?? ''

if (!/^\d+$/.test(DB_PORT) || Number(DB_PORT) <= 0 || Number(DB_PORT) > 65535) {
  console.error(`[setup-test-db] database.port 非法: ${DB_PORT}(必须是 1..65535 的整数)`)
  process.exit(1)
}

// schema 是被无条件 DROP + CREATE 的标识符——必须严格校验后才拼进 SQL:
//   · 空值 → 避免 DROP DATABASE `` 这类无意义/误删语句
//   · 「【人工填写】」占位 → 配置尚未填好,不应连库
//   · 含反引号 → 防止 `erp`; DROP DATABASE `prod` 形态的标识符注入(值来自 config-vars.yaml,按 fail-closed 处理)
//   注:仅接受 ASCII 标识符;非 ASCII schema 名一律拒绝(即便 MySQL / apply-ddl 允许),与推荐的 test/_dev 命名一致
if (!/^[A-Za-z0-9_$]+$/.test(DB_SCHEMA)) {
  console.error(`[setup-test-db] database.schema 非法或未填: ${JSON.stringify(DB_SCHEMA)}(需为 [A-Za-z0-9_$] 标识符;空值 / 「【人工填写】」占位 / 含反引号均拒绝)`)
  process.exit(1)
}

console.log(`[setup-test-db] 即将 DROP + CREATE \`${DB_SCHEMA}\` on ${DB_HOST}:${DB_PORT}`)

const sql =
  `DROP DATABASE IF EXISTS \`${DB_SCHEMA}\`; ` +
  `CREATE DATABASE \`${DB_SCHEMA}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;`

const mysqlArgs = [
  `--host=${DB_HOST}`,
  `--port=${DB_PORT}`,
  `--user=${DB_USER}`,
  `--password=${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')