115 lines
3.9 KiB
JavaScript
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/);
|
|
});
|