setup-test-db-template.test.mjs 5.42 KB
// lib/setup-test-db-template.test.mjs — 校验生成模板 scripts/setup-test-db.mjs 的"复制源库→一次性副本"逻辑。
// 跑的是真实模板产物:复制到临时 scripts/ 下、写一个 ../config-vars.yaml、再 node 执行。
// 所有用例的 host/port 故意指向 127.0.0.1:1(必拒连),不会触碰真实库;本地校验先于任何 mysql 调用。
import { test } from 'node:test'
import assert from 'node:assert/strict'
import { spawnSync } from 'node:child_process'
import { mkdtempSync, mkdirSync, copyFileSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'

const TEMPLATE = fileURLToPath(new URL('../skills/plan/skeleton-gen/templates/scripts-setup-test-db-template.mjs', import.meta.url))

function runWithSchema(schemaLine) {
  return runWithDb({ schemaLine })
}

// env:传给子进程的增量环境变量;laneMarker:非 null 时写 .tmp/erp-lane-db 兜底标记。
// 基础环境强制剔除 ERP_TEST_DB_SCHEMA——外层 shell 若恰好设了该变量,"缺省行为"用例会被污染。
function runWithDb({ host = '127.0.0.1', port = '1', user = 'root', password = 'x', schemaLine = 'schema: erp_dev', env = {}, laneMarker = null }) {
  const dir = mkdtempSync(join(tmpdir(), 'erp-stdb-'))
  mkdirSync(join(dir, 'scripts'))
  copyFileSync(TEMPLATE, join(dir, 'scripts', 'setup-test-db.mjs'))
  writeFileSync(
    join(dir, 'config-vars.yaml'),
    ['database:', `  host: ${host}`, `  port: ${port}`, `  user: ${user}`, `  password: ${password}`, '  ' + schemaLine, ''].join('\n'),
  )
  if (laneMarker !== null) {
    mkdirSync(join(dir, '.tmp'))
    writeFileSync(join(dir, '.tmp', 'erp-lane-db'), laneMarker)
  }
  const childEnv = { ...process.env, ...env }
  if (!('ERP_TEST_DB_SCHEMA' in env)) delete childEnv.ERP_TEST_DB_SCHEMA
  return spawnSync('node', [join(dir, 'scripts', 'setup-test-db.mjs')], { encoding: 'utf8', env: childEnv })
}

test('setup-test-db: empty schema (source) fails before mysql is invoked', () => {
  const r = runWithSchema('schema:')
  assert.equal(r.status, 1)
  assert.match(r.stderr, /database\.schema/, '应是 schema 缺失报错而非连库失败 — stderr: ' + r.stderr)
})

test('setup-test-db: unfilled 【人工填写】 config placeholders fail before mysql is invoked', () => {
  for (const cfg of [
    { host: '【人工填写:MySQL host】' },
    { port: '【人工填写:MySQL port】' },
    { user: '【人工填写:账号】' },
    { password: '【人工填写:密码】' },
    { schemaLine: 'schema: 【人工填写:schema 名】' },
  ]) {
    const r = runWithDb(cfg)
    assert.equal(r.status, 1)
    assert.match(r.stderr, /仍是占位/, 'stderr: ' + r.stderr)
  }
})

// **安全红线**:副本名 = 源库名 → 拒绝(否则 DROP 副本 = DROP 源库)。守卫先于任何 mysql 调用。
test('setup-test-db: copy name equal to source is rejected before mysql (protects source)', () => {
  const r = runWithDb({ env: { ERP_TEST_DB_SCHEMA: 'erp_dev' } }) // 与 config 的源库同名
  assert.equal(r.status, 1)
  assert.match(r.stderr, /副本库名与源库名相同/, 'stderr: ' + r.stderr)
  assert.doesNotMatch(r.stdout, /复制源库/, '拒绝应发生在打印复制计划之前 — stdout: ' + r.stdout)
})

test('setup-test-db: an ordinary dev schema copies source → <source>__test copy', () => {
  const r = runWithSchema('schema: erp_dev')
  assert.match(r.stdout, /复制源库 erp_dev → 一次性副本 erp_dev__test/, 'stdout: ' + r.stdout)
  assert.notEqual(r.status, 0, '应在连库阶段失败(127.0.0.1:1 拒连)')
})

test('setup-test-db: a schema with a backtick still resolves and reaches the copy step', () => {
  const r = runWithSchema('schema: ev`il')
  assert.match(r.stdout, /复制源库 ev`il → 一次性副本 ev`il__test/, 'stdout: ' + r.stdout)
  assert.doesNotMatch(r.stderr, /database\.schema/, 'stderr: ' + r.stderr)
})

// ── 副本库名 lane 化:取值顺序 env > .tmp/erp-lane-db marker > `<source>__test` ──

test('setup-test-db: ERP_TEST_DB_SCHEMA env names the copy', () => {
  const r = runWithDb({ env: { ERP_TEST_DB_SCHEMA: 'erp_dev_lane2' } })
  assert.match(r.stdout, /复制源库 erp_dev → 一次性副本 erp_dev_lane2/, 'stdout: ' + r.stdout)
})

test('setup-test-db: empty ERP_TEST_DB_SCHEMA env falls through to <source>__test', () => {
  const r = runWithDb({ env: { ERP_TEST_DB_SCHEMA: '' } })
  assert.match(r.stdout, /一次性副本 erp_dev__test/, 'stdout: ' + r.stdout)
})

test('setup-test-db: .tmp/erp-lane-db marker names the copy when env is absent, with audit log', () => {
  const r = runWithDb({ laneMarker: 'erp_dev_lane3\n' })
  assert.match(r.stdout, /lane DB marker 生效: erp_dev_lane3/, 'stdout: ' + r.stdout)
  assert.match(r.stdout, /一次性副本 erp_dev_lane3/, 'stdout: ' + r.stdout)
})

test('setup-test-db: env wins over marker (marker log absent)', () => {
  const r = runWithDb({ env: { ERP_TEST_DB_SCHEMA: 'erp_dev_lane2' }, laneMarker: 'erp_dev_lane3\n' })
  assert.match(r.stdout, /一次性副本 erp_dev_lane2/, 'stdout: ' + r.stdout)
  assert.doesNotMatch(r.stdout, /lane DB marker 生效/, 'stdout: ' + r.stdout)
})

test('setup-test-db: blank marker falls through to <source>__test', () => {
  const r = runWithDb({ laneMarker: '\n' })
  assert.match(r.stdout, /一次性副本 erp_dev__test/, 'stdout: ' + r.stdout)
  assert.doesNotMatch(r.stdout, /lane DB marker 生效/, 'stdout: ' + r.stdout)
})