Files
vue-driven-cloud-storage/backend/tests/architecture-tests.js
237899745 a736d70d57
Some checks failed
Build and tests / test (push) Has been cancelled
build: publish desktop installer via Gitea releases
2026-07-27 15:20:09 +08:00

150 lines
6.6 KiB
JavaScript

const assert = require('assert');
const fs = require('fs');
const path = require('path');
const projectRoot = path.resolve(__dirname, '..', '..');
const serverPath = path.join(projectRoot, 'backend', 'server.js');
const source = fs.readFileSync(serverPath, 'utf8');
const results = { passed: 0, failed: 0 };
function test(name, fn) {
try {
fn();
results.passed += 1;
console.log(` [PASS] ${name}`);
} catch (error) {
results.failed += 1;
console.error(` [FAIL] ${name}: ${error.message}`);
}
}
console.log('\n========== 架构约束测试 ==========\n');
test('Express async 错误补丁在所有路由之前加载', () => {
const patchIndex = source.indexOf("require('express-async-errors')");
const routeIndex = source.search(/\bapp\.(get|post|put|delete|patch)\s*\(/);
assert.ok(patchIndex >= 0);
assert.ok(routeIndex > patchIndex);
});
test('全局错误中间件保持为最后一个 app.use', () => {
const errorHandlerIndex = source.indexOf('app.use(expressErrorHandler);');
assert.ok(errorHandlerIndex >= 0);
assert.strictEqual(source.lastIndexOf('app.use('), errorHandlerIndex);
});
test('抽取的纯函数不在 server.js 保留第二份定义', () => {
const extractedFunctions = [
'normalizeVersion',
'normalizeSha256',
'normalizeVirtualPath',
'buildHttpDownloadUrl',
'parseDownloadTrafficLine',
'isShareIpAllowed',
'resolveClientType',
'parseBooleanLike'
];
for (const functionName of extractedFunctions) {
assert.strictEqual(new RegExp(`function\\s+${functionName}\\s*\\(`).test(source), false, functionName);
}
});
test('旧占位路由已清除且真实系统路由已挂载', () => {
assert.strictEqual(fs.existsSync(path.join(projectRoot, 'backend', 'routes', 'health.js')), false);
assert.strictEqual(fs.existsSync(path.join(projectRoot, 'backend', 'routes', 'index.js')), false);
assert.ok(source.includes("require('./routes/system.routes')"));
assert.ok(source.includes("app.use('/api', createSystemRouter"));
});
test('package.json 与 package-lock.json 顶层依赖同步', () => {
const packageJson = require('../package.json');
const packageLock = require('../package-lock.json');
assert.deepStrictEqual(packageLock.packages[''].dependencies, packageJson.dependencies);
});
test('前端 package.json 与 lock 文件依赖同步且没有手工缓存版本号', () => {
const frontendPackage = require('../../frontend/package.json');
const frontendLock = require('../../frontend/package-lock.json');
assert.deepStrictEqual(frontendLock.packages[''].dependencies, frontendPackage.dependencies);
assert.deepStrictEqual(frontendLock.packages[''].devDependencies, frontendPackage.devDependencies);
const frontendDirectory = path.join(projectRoot, 'frontend');
for (const fileName of fs.readdirSync(frontendDirectory).filter((name) => name.endsWith('.html'))) {
const html = fs.readFileSync(path.join(frontendDirectory, fileName), 'utf8');
assert.strictEqual(/\?v=\d+/.test(html), false, fileName);
}
});
test('生产部署统一使用 Vite dist 且不会删除 lock 文件', () => {
const installer = fs.readFileSync(path.join(projectRoot, 'install.sh'), 'utf8');
const nginxConfig = fs.readFileSync(path.join(projectRoot, 'nginx', 'nginx.conf'), 'utf8');
const nginxExample = fs.readFileSync(path.join(projectRoot, 'nginx', 'nginx.conf.example'), 'utf8');
assert.ok(installer.includes('npm ci --include=dev'));
assert.ok(installer.includes('npm run build'));
assert.ok(installer.includes('frontend/dist'));
assert.strictEqual(/rm\s+-rf\s+node_modules\s+package-lock\.json/.test(installer), false);
assert.ok(nginxConfig.includes('root /usr/share/nginx/html;'));
assert.ok(nginxConfig.includes('alias /runtime/downloads/;'));
for (const config of [installer, nginxConfig, nginxExample]) {
assert.ok(config.includes('proxy_hide_header X-Request-ID;'));
assert.ok(config.includes('proxy_hide_header Content-Security-Policy;'));
}
});
test('桌面安装包由 Gitea Release 发布且不会再次提交到 Git', () => {
const gitignore = fs.readFileSync(path.join(projectRoot, '.gitignore'), 'utf8');
const release = JSON.parse(fs.readFileSync(
path.join(projectRoot, 'frontend', 'desktop-release.json'),
'utf8'
));
const installer = fs.readFileSync(path.join(projectRoot, 'install.sh'), 'utf8');
const compose = fs.readFileSync(path.join(projectRoot, 'docker-compose.yml'), 'utf8');
assert.ok(gitignore.split(/\r?\n/).includes('/frontend/downloads/*.exe'));
assert.strictEqual(gitignore.includes('!/frontend/downloads/'), false);
assert.match(release.sha256, /^[a-f0-9]{64}$/);
assert.ok(Number.isSafeInteger(release.size) && release.size > 0);
assert.strictEqual(release.tag, `v${release.version}`);
assert.strictEqual(release.publicPath, `/downloads/${release.asset}`);
assert.ok(installer.includes('sync_desktop_release'));
assert.ok(compose.includes('desktop-release:'));
});
test('Docker 构建上下文排除依赖、环境配置和运行时数据', () => {
const dockerIgnore = fs.readFileSync(path.join(projectRoot, 'backend', '.dockerignore'), 'utf8');
for (const requiredEntry of ['node_modules', '.env', 'data/*', 'storage/*']) {
assert.ok(dockerIgnore.split(/\r?\n/).includes(requiredEntry), requiredEntry);
}
const dockerfile = fs.readFileSync(path.join(projectRoot, 'backend', 'Dockerfile'), 'utf8');
assert.ok(dockerfile.includes('AS dependencies'));
assert.ok(dockerfile.includes('AS runtime'));
assert.ok(dockerfile.includes('npm_config_nodedir=/usr/local'));
});
test('install.sh 生成的 Nginx server 块花括号平衡', () => {
const installer = fs.readFileSync(path.join(projectRoot, 'install.sh'), 'utf8');
const templates = [...installer.matchAll(/cat > .*?<< EOF\r?\n([\s\S]*?)\r?\nEOF/g)]
.map((match) => match[1])
.filter((template) => template.includes('server {'));
assert.strictEqual(templates.length, 3);
for (const template of templates) {
const openingBraces = (template.match(/\{/g) || []).length;
const closingBraces = (template.match(/\}/g) || []).length;
assert.strictEqual(openingBraces, closingBraces);
}
});
test('PM2 部署没有启用 cluster 或多实例参数', () => {
const installer = fs.readFileSync(path.join(projectRoot, 'install.sh'), 'utf8');
assert.strictEqual(/pm2\s+start[^\n]*(?:\s-i\s|--instances)/.test(installer), false);
});
console.log('\n========================================');
console.log('测试总结');
console.log('========================================');
console.log(`通过: ${results.passed}`);
console.log(`失败: ${results.failed}`);
process.exit(results.failed > 0 ? 1 : 0);