96 lines
5.2 KiB
JavaScript
96 lines
5.2 KiB
JavaScript
import assert from 'node:assert/strict'
|
|
import { mkdtemp, mkdir, readFile, symlink, writeFile } from 'node:fs/promises'
|
|
import os from 'node:os'
|
|
import path from 'node:path'
|
|
import test from 'node:test'
|
|
import { assertDeploymentLayout, readDevelopmentRuntime, resolveRuntimeFile, runtimeRoot, sourceRoot } from '../../deploy/runtime-config.mjs'
|
|
import { fileDigest, sourceFingerprint } from '../../deploy/build-state.mjs'
|
|
|
|
test('开发配置只从模拟部署目录读取且不返回凭据字段', async () => {
|
|
const root = await mkdtemp(path.join(os.tmpdir(), 'smail-runtime-config-'))
|
|
await mkdir(path.join(root, 'config'))
|
|
await mkdir(path.join(root, 'ssl'))
|
|
await writeFile(path.join(root, 'config/config.json'), JSON.stringify({
|
|
generation: 9, domains: { web: 'mail.example.test' }, database: { password: 'not-exported' }
|
|
}))
|
|
await writeFile(path.join(root, 'config/ssl.json'), JSON.stringify({
|
|
tls: { certificateRef: 'ssl/public.crt', privateKeyRef: 'ssl/private.key' }
|
|
}))
|
|
await writeFile(path.join(root, 'ssl/public.crt'), 'certificate')
|
|
await writeFile(path.join(root, 'ssl/private.key'), 'key')
|
|
const runtime = readDevelopmentRuntime(root)
|
|
assert.equal(runtime.origin, 'https://mail.example.test')
|
|
assert.equal(runtime.generation, 9)
|
|
assert.equal(runtime.privateKeyPath, path.join(root, 'ssl/private.key'))
|
|
assert.equal(JSON.stringify(runtime).includes('not-exported'), false)
|
|
assert.equal(runtimeRoot, path.join(sourceRoot, 'yunx-test'))
|
|
assert.throws(() => resolveRuntimeFile(root, '../outside.key'))
|
|
assert.throws(() => resolveRuntimeFile(root, path.join(root, 'ssl/private.key')))
|
|
})
|
|
|
|
test('源码指纹检测编辑且忽略编译嵌入目录', async () => {
|
|
const root = await mkdtemp(path.join(os.tmpdir(), 'smail-build-state-'))
|
|
for (const name of ['backend', 'frontend', 'plugins/pmail_telegram_push', 'deploy']) {
|
|
await mkdir(path.join(root, name), { recursive: true })
|
|
}
|
|
await writeFile(path.join(root, 'backend/main.go'), 'package main')
|
|
const before = await sourceFingerprint(root)
|
|
await mkdir(path.join(root, 'backend/dist'))
|
|
await writeFile(path.join(root, 'backend/dist/index.html'), 'generated')
|
|
assert.equal(await sourceFingerprint(root), before)
|
|
await writeFile(path.join(root, 'backend/main.go'), 'package main\n')
|
|
assert.notEqual(await sourceFingerprint(root), before)
|
|
assert.equal((await fileDigest(path.join(root, 'backend/main.go'))).length, 64)
|
|
await writeFile(path.join(root, 'empty.exe'), '')
|
|
await assert.rejects(fileDigest(path.join(root, 'empty.exe')))
|
|
})
|
|
|
|
test('运行文件拒绝通过父目录链接访问另一个业务目录', async () => {
|
|
const root = await mkdtemp(path.join(os.tmpdir(), 'smail-runtime-link-'))
|
|
await mkdir(path.join(root, 'ssl'))
|
|
await mkdir(path.join(root, 'data'))
|
|
await writeFile(path.join(root, 'data/private.key'), 'fixture')
|
|
await symlink(path.join(root, 'data'), path.join(root, 'ssl/linked'), process.platform === 'win32' ? 'junction' : 'dir')
|
|
assert.throws(() => resolveRuntimeFile(root, 'ssl/linked/private.key'), /链接/)
|
|
})
|
|
|
|
test('部署和启动必须完整构建、验证产物且不覆盖运行配置', async () => {
|
|
const read = name => readFile(path.join(sourceRoot, name), 'utf8')
|
|
const deploy = await read('deploy/deploy-runtime.ps1')
|
|
const start = await read('deploy/start-dev.ps1')
|
|
assert.ok(deploy.indexOf("'build.ps1'") < deploy.indexOf('Copy-Item'))
|
|
assert.match(deploy, /verify --deployed/)
|
|
assert.match(deploy, /deployment-history/)
|
|
assert.doesNotMatch(deploy, /Copy-Item[^\n]*(config|data|ssl)/)
|
|
assert.doesNotMatch(start, /SkipBuild|SkipFrontend/)
|
|
assert.match(start, /\$env:SMAIL_ROOT = \$runtime/)
|
|
assert.match(start, /\$workingDirectory = \$runtime/)
|
|
assert.match(start, /build-state\.mjs'\) verify --deployed/)
|
|
assert.match(await read('.gitignore'), /^\/yunx-test\/$/m)
|
|
})
|
|
|
|
test('部署启动检查兼容自动归档前后的密钥且拒绝双份冲突', async () => {
|
|
for (const grouped of [false, true]) {
|
|
const root = await mkdtemp(path.join(os.tmpdir(), 'smail-key-layout-'))
|
|
for (const name of ['config', 'ssl', 'data', 'log']) await mkdir(path.join(root, name))
|
|
await writeFile(path.join(root, 'config/config.json'), JSON.stringify({ setupState: 'ready', database: { type: 'mysql' } }))
|
|
for (const name of ['smtp', 'email', 'plugins']) await writeFile(path.join(root, `config/${name}.json`), '{}')
|
|
await writeFile(path.join(root, 'config/ssl.json'), JSON.stringify({ tls: { certificateRef: 'ssl/public.crt', privateKeyRef: 'ssl/private.key' } }))
|
|
await writeFile(path.join(root, 'ssl/public.crt'), 'certificate')
|
|
await writeFile(path.join(root, 'ssl/private.key'), 'key')
|
|
for (const [folder, name] of [
|
|
['credentials', 'credential.key'],
|
|
['external-email-encryption', 'external-email-encryption.key'],
|
|
['external-email-fingerprint', 'external-email-fingerprint.key']
|
|
]) {
|
|
if (grouped) await mkdir(path.join(root, 'ssl', folder))
|
|
await writeFile(path.join(root, 'ssl', ...(grouped ? [folder] : []), name), Buffer.alloc(32, 1))
|
|
}
|
|
assert.equal(assertDeploymentLayout(root).setupState, 'ready')
|
|
if (grouped) {
|
|
await writeFile(path.join(root, 'ssl/credential.key'), Buffer.alloc(32, 2))
|
|
assert.throws(() => assertDeploymentLayout(root), /冲突/)
|
|
}
|
|
}
|
|
})
|