import assert from 'node:assert/strict'; import { mkdtemp, mkdir, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import path from 'node:path'; import test from 'node:test'; import { verifySourceDeployment } from './verify-source-deployment.mjs'; const processNames = ['api', 'scheduler', 'worker']; function systemdUnit(processName, { user = 'a-share' } = {}) { const displayName = processName === 'api' ? 'API' : `${processName[0].toUpperCase()}${processName.slice(1)}`; return `[Unit] Description=A Share Analysis ${displayName} [Service] Type=simple User=${user} Group=a-share WorkingDirectory=/opt/a-share-analysis/current EnvironmentFile=/etc/a-share-analysis/${processName}.env ExecStartPre=/usr/bin/test -r /etc/a-share-analysis/${processName}.env ExecStart=/usr/bin/node /opt/a-share-analysis/current/server/dist/apps/${processName}/main.js [Install] WantedBy=multi-user.target `; } async function createSourceReleaseFixture({ omit, apiUser = 'a-share', invalidNginx = false, } = {}) { const root = await mkdtemp(path.join(tmpdir(), 'a-share-source-release-')); const files = new Map([ ['web/dist/index.html', 'A Share Analysis\n'], ['server/dist/apps/api/main.js', 'export {};\n'], ['server/dist/apps/scheduler/main.js', 'export {};\n'], ['server/dist/apps/worker/main.js', 'export {};\n'], ...processNames.map((processName) => [ `deploy/systemd/a-share-${processName}.service.example`, systemdUnit(processName, { user: processName === 'api' ? apiUser : 'a-share' }), ]), [ 'deploy/nginx/a-share-analysis.conf.example', invalidNginx ? 'server { root /srv/invalid; }\n' : `server { root /opt/a-share-analysis/current/web/dist; location /api/ { proxy_pass http://127.0.0.1:3000; } } `, ], ]); for (const [relativePath, content] of files) { if (relativePath === omit) continue; const absolutePath = path.join(root, relativePath); await mkdir(path.dirname(absolutePath), { recursive: true }); await writeFile(absolutePath, content); } return root; } test('完整源码发布夹具通过', async () => { const root = await createSourceReleaseFixture(); await assert.doesNotReject(() => verifySourceDeployment(root)); }); test('任一 Docker 活动文件会阻断源码发布', async () => { const root = await createSourceReleaseFixture(); await writeFile(path.join(root, 'Dockerfile'), 'FROM node:22\n'); await assert.rejects(() => verifySourceDeployment(root), /禁止存在 Docker 活动文件.*Dockerfile/); }); test('未预先列举的 Dockerfile 和 Compose 文件仍会阻断源码发布', async () => { for (const relativePath of ['ops/Dockerfile', 'deploy/compose/extra.yml']) { const root = await createSourceReleaseFixture(); const absolutePath = path.join(root, relativePath); await mkdir(path.dirname(absolutePath), { recursive: true }); await writeFile(absolutePath, 'services: {}\n'); await assert.rejects( () => verifySourceDeployment(root), new RegExp(`禁止存在 Docker 活动文件.*${relativePath.replace('/', '\\/')}`), ); } }); test('Server 构建入口缺失会阻断源码发布', async () => { const root = await createSourceReleaseFixture({ omit: 'server/dist/apps/worker/main.js' }); await assert.rejects(() => verifySourceDeployment(root), /Worker 构建入口不存在/); }); test('systemd 配置不是最小权限时会阻断源码发布', async () => { const root = await createSourceReleaseFixture({ apiUser: 'root' }); await assert.rejects(() => verifySourceDeployment(root), /API systemd 必须使用 User=a-share/); }); test('Nginx 未代理 API 或未指向 Web dist 时会阻断源码发布', async () => { const root = await createSourceReleaseFixture({ invalidNginx: true }); await assert.rejects(() => verifySourceDeployment(root), /Nginx/); });