import test from 'node:test' import assert from 'node:assert/strict' import fs from 'node:fs' import path from 'node:path' import os from 'node:os' import { execFileSync } from 'node:child_process' import { frontendRoot, validTag, shellQuote, deploymentTarget, validateEntries, inspectPackage, bashExecutable } from './release.mjs' import { installScript } from './deploy-test.mjs' test('release and SSH inputs reject paths, options and shell injection', () => { for (const value of ['', '../release', '-latest', 'a;touch x', 'a\nother']) assert.throws(() => validTag(value)) assert.equal(validTag('a846bd90-rc.1'), 'a846bd90-rc.1') for (const args of [{ host: '-oProxyCommand=id' }, { user: 'root;id' }, { directory: '/' }, { directory: '/www/../etc' }, { port: '65536' }]) assert.throws(() => deploymentTarget(args)) assert.equal(shellQuote("a'b$(id)"), "'a'\\''b$(id)'") assert.equal(deploymentTarget().base, '/www/wwwroot/sw2.fuhua91.com') }) test('runtime archives exclude backend source and filesystem links', () => { const required = ['./Dockerfile', './nginx.conf', './deploy.sh', './release.json', './dist/index.html', './dist/version.txt'] validateEntries(required, required.map(() => '-')) for (const extra of ['../outside', '/etc/passwd', './dist/../../outside', './backend/app', './internal/data.go', './dist\\..\\outside']) assert.throws(() => validateEntries([...required, extra], required.map(() => '-'))) for (const type of ['l', 'h', 'p', 'b']) assert.throws(() => validateEntries(required, ['-', type])) }) test('package inspection verifies embedded version and reads the actual archive checksum', () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'sw-frontend-release-test-')) try { const stage = path.join(root, 'stage'); fs.mkdirSync(path.join(stage, 'dist'), { recursive: true }) for (const file of ['Dockerfile', 'nginx.conf', 'deploy.sh', 'dist/index.html']) fs.writeFileSync(path.join(stage, file), 'test\n') fs.writeFileSync(path.join(stage, 'release.json'), JSON.stringify({ formatVersion: 1, version: 'test-1', revision: 'a'.repeat(40), tree: 'b'.repeat(40) })) fs.writeFileSync(path.join(stage, 'dist/version.txt'), 'test-1\n') const archive = path.join(root, 'release.tar.gz') execFileSync('tar', ['-czf', archive, '-C', stage, '.']) const result = inspectPackage(archive) assert.equal(result.info.version, 'test-1') assert.match(result.checksum, /^[a-f0-9]{64}$/) fs.writeFileSync(path.join(stage, 'dist/version.txt'), 'wrong-version\n') execFileSync('tar', ['-czf', archive, '-C', stage, '.']) assert.throws(() => inspectPackage(archive), /版本标记不一致/) } finally { assert.equal(path.dirname(root), path.resolve(os.tmpdir())) fs.rmSync(root, { recursive: true }) } }) test('server shell syntax and deployment rollback preserve the backend', () => { const bash = bashExecutable() execFileSync(bash, ['-n', 'deploy/deploy.sh'], { cwd: frontendRoot }) execFileSync(bash, ['-n'], { input: installScript, cwd: frontendRoot }) const output = execFileSync(bash, ['deploy/deploy.test.sh'], { cwd: frontendRoot, encoding: 'utf8' }) assert.match(output, /all deployment scenarios passed/) })