98 lines
4.2 KiB
JavaScript
98 lines
4.2 KiB
JavaScript
const test = require('node:test')
|
||
const assert = require('node:assert/strict')
|
||
const fs = require('node:fs')
|
||
const path = require('node:path')
|
||
|
||
const deployPath = path.resolve(__dirname, '../../../deploy.sh')
|
||
const precheckPath = path.resolve(
|
||
__dirname,
|
||
'../../prisma/migrations/20260714_asset_permissions_disposal_dingtalk/precheck.sql'
|
||
)
|
||
const deployScript = fs.readFileSync(deployPath, 'utf8')
|
||
const precheckSql = fs.readFileSync(precheckPath, 'utf8')
|
||
|
||
test('标准部署在 build/up 前必须通过报废迁移预检', () => {
|
||
const gateCall = deployScript.lastIndexOf('\nrun_disposal_migration_precheck\n')
|
||
const buildCall = deployScript.indexOf('"${COMPOSE_CMD[@]}" build')
|
||
const upCall = deployScript.indexOf('"${COMPOSE_CMD[@]}" up -d --remove-orphans')
|
||
|
||
assert.ok(gateCall > 0, '部署脚本必须调用迁移前预检')
|
||
assert.ok(buildCall > gateCall, '预检必须早于镜像构建')
|
||
assert.ok(upCall > gateCall, '预检必须早于容器更新/迁移')
|
||
assert.match(
|
||
deployScript,
|
||
/backend\/prisma\/migrations\/20260714_asset_permissions_disposal_dingtalk\/precheck\.sql/
|
||
)
|
||
})
|
||
|
||
test('预检只连接固定资产 MySQL,不在命令行暴露密码', () => {
|
||
assert.match(deployScript, /MYSQL_CONTAINER="equipment-mysql"/)
|
||
assert.match(deployScript, /--defaults-extra-file="\$defaults_file"/)
|
||
assert.doesNotMatch(deployScript, /mysql\s+[^\n]*--password=/)
|
||
assert.doesNotMatch(deployScript, /shebeiguanli|\/opt\/shebeiguanli/)
|
||
assert.doesNotMatch(
|
||
deployScript,
|
||
/LEGACY_CONTAINERS|record_legacy_container_statuses|verify_legacy_container_statuses/
|
||
)
|
||
})
|
||
|
||
test('无法确认或存在未核对报废单时默认阻断,仅允许本次显式确认', () => {
|
||
assert.match(precheckSql, /SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;/)
|
||
assert.match(precheckSql, /START TRANSACTION WITH CONSISTENT SNAPSHOT, READ ONLY;/)
|
||
assert.match(precheckSql, /'ACTIVE_DISPOSAL_COUNT'/)
|
||
assert.match(precheckSql, /'REVIEW_ROW'/)
|
||
assert.match(precheckSql, /COMMIT;/)
|
||
assert.match(
|
||
deployScript,
|
||
/snapshot_output="\$\([\s\S]*run_equipment_mysql --batch --skip-column-names --raw < "\$DISPOSAL_PRECHECK_SQL"[\s\S]*\)"/
|
||
)
|
||
assert.match(deployScript, /printf '%s\\n' "\$review_rows" \| sha256sum/)
|
||
assert.match(
|
||
deployScript,
|
||
/CONFIRM_ACTIVE_DISPOSALS_REVIEWED" != "\$CURRENT_DISPOSAL_FINGERPRINT"/
|
||
)
|
||
assert.match(deployScript, /已默认阻止部署/)
|
||
})
|
||
|
||
test('未完成报废资产树存在环或深度达到 100 时禁止 override', () => {
|
||
const anomalyBlock = deployScript.indexOf('if [ "$tree_anomaly_count" -gt 0 ]')
|
||
const overrideCheck = deployScript.indexOf(
|
||
'if [ "$CONFIRM_ACTIVE_DISPOSALS_REVIEWED" != "$CURRENT_DISPOSAL_FINGERPRINT" ]'
|
||
)
|
||
|
||
assert.match(precheckSql, /WHERE `cycle_detected` = 1 OR `depth` >= 100/)
|
||
assert.ok(anomalyBlock > 0, '必须对资产树异常数量做硬阻断')
|
||
assert.ok(overrideCheck > anomalyBlock, '资产树异常阻断必须早于指纹 override')
|
||
assert.match(deployScript, /不允许通过确认指纹绕过/)
|
||
})
|
||
|
||
test('人工展示与指纹来自同一次数据库一致性快照', () => {
|
||
const functionBody = deployScript.slice(
|
||
deployScript.indexOf('calculate_active_disposal_fingerprint()'),
|
||
deployScript.indexOf('\nrun_disposal_migration_precheck()')
|
||
)
|
||
const mysqlCalls = functionBody.match(/run_equipment_mysql/g) || []
|
||
|
||
assert.equal(mysqlCalls.length, 1, '一次指纹计算只能打开一个数据库会话')
|
||
assert.match(functionBody, /review_rows=.*snapshot_output/)
|
||
assert.match(functionBody, /printf '%s\\n' "\$review_rows"/)
|
||
assert.doesNotMatch(
|
||
deployScript.slice(
|
||
deployScript.indexOf('run_disposal_migration_precheck()'),
|
||
deployScript.indexOf('\nrecheck_disposal_migration_precheck()')
|
||
),
|
||
/run_equipment_mysql\s+<\s+"\$DISPOSAL_PRECHECK_SQL"/
|
||
)
|
||
})
|
||
|
||
test('同名迁移同时存在完成和未完成记录时必须优先判为 incomplete', () => {
|
||
const stateQuery = deployScript.slice(
|
||
deployScript.indexOf('SELECT CASE'),
|
||
deployScript.indexOf('END\nFROM _prisma_migrations')
|
||
)
|
||
assert.ok(
|
||
stateQuery.indexOf("THEN 'incomplete'") < stateQuery.indexOf("THEN 'applied'"),
|
||
'未完成记录必须优先阻断,不能被旧的已完成记录掩盖'
|
||
)
|
||
})
|