scripts-setup-test-db-template.mjs
5.37 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
#!/usr/bin/env node
// scripts/setup-test-db.mjs — 数据库重置脚本:drop + create 空库。
// schema apply 由 Flyway 在 Spring Boot 启动时自动处理(见 docs/04 技术栈 + sql/migrations/V*.sql)。
// seed 数据由测试框架负责(Spring @Sql / Flyway R__seed.sql / data.sql)。
//
// 使用场景:
// - scripts/test.mjs 开头:清空库,让 Spring 启动时 Flyway 从 V1 开始重放所有 migration
// - scripts/test.mjs 结尾:清空库,避免测试遗留污染下次运行
// - 手动调试时:reset 到零状态
//
// 跨平台:用纯 JS 解析 .env.local(dotenv 风格,逐行 KEY=VALUE),**绝不** shell-source,
// 因此 mac / Windows 原生 node 均可运行,且消除 shell 注入 / 变量展开隐患。
// DROP/CREATE 通过 `mysql` 客户端以 argv 数组方式执行(不经 shell),密码不进命令行解析层。
//
// 防护:本脚本只允许在本地 host + 测试库名上执行;非预期目标会被拒绝,
// 避免 .env.local 误指向 staging/prod 时触发不可逆 DROP。
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')
// dotenv 风格解析:逐行 KEY=VALUE,跳过空行与 # 注释,去除两侧空白,
// 可选地剥离一层成对单/双引号。**不做**变量展开,特殊字符按字面保留。
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 ?? ''
const TEST_DB_ALLOW_REMOTE = env.TEST_DB_ALLOW_REMOTE ?? process.env.TEST_DB_ALLOW_REMOTE ?? '0'
const TEST_DB_ALLOW_PROD_NAME =
env.TEST_DB_ALLOW_PROD_NAME ?? process.env.TEST_DB_ALLOW_PROD_NAME ?? '0'
// 防护 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>"')
console.error(' 一次性绕过:在 .env.local 设 TEST_DB_ALLOW_REMOTE=1')
if (TEST_DB_ALLOW_REMOTE !== '1') 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)`
)
console.error(' 如确为期望行为,请显式声明:在 .env.local 设 TEST_DB_ALLOW_PROD_NAME=1')
if (TEST_DB_ALLOW_PROD_NAME !== '1') process.exit(1)
}
// 防护 3:显式 banner,让人看见自己在 drop 什么;远程 host 额外提示白名单内容。
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;`
// 以 argv 数组调用 mysql(不经 shell):密码不进 shell 解析,跨平台一致。
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')