scripts-setup-test-db-template.mjs 3.76 KB
#!/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="<host1> <host2>"')
  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')