Files
a-share-analysis/tools/deployment/verify-source-deployment.test.mjs
T

115 lines
3.9 KiB
JavaScript

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', '<!doctype html><title>A Share Analysis</title>\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/);
});