修复远程前端测试隔离依赖及跨平台断言
Some checks are pending
Security Checks / security (push) Waiting to run

This commit is contained in:
smail 2026-09-18 01:17:22 +08:00
parent d193324e6d
commit 78c5936388
4 changed files with 78 additions and 1 deletions

View File

@ -0,0 +1,59 @@
"""验证前端审查依赖的只读挂载范围,不启动本机应用。"""
import importlib.util
from pathlib import Path
import unittest
from unittest.mock import patch
source = Path(__file__).resolve().parents[1] / 'tools' / 'run_frontend_review.py'
spec = importlib.util.spec_from_file_location('frontend_review', source)
review = importlib.util.module_from_spec(spec)
spec.loader.exec_module(review)
class DependencyBindingsTest(unittest.TestCase):
root = Path('/var/tmp/smail-review-current')
def bindings(self, targets):
with patch.object(Path, 'resolve', side_effect=map(Path, targets)):
with patch.object(Path, 'is_dir', return_value=True):
return review.dependency_bindings(self.root)
def test_local_dependencies_need_no_extra_mount(self):
self.assertEqual(self.bindings([
str(self.root / 'node-v22.23.2-linux-x64'),
str(self.root / 'frontend/node_modules'),
]), [])
def test_shared_dependencies_are_explicit(self):
targets = ['/var/tmp/smail-review-previous/node',
'/var/tmp/smail-review-previous/frontend/node_modules']
self.assertEqual(self.bindings(targets), sorted(targets))
def test_duplicate_mounts_are_combined(self):
target = '/var/tmp/smail-review-previous/dependencies'
self.assertEqual(self.bindings([target, target]), [target])
def test_unsafe_targets_are_rejected(self):
for target in ('/opt/smail', '/run/smail', '/var/tmp/unrelated/node',
'/var/tmp/smail-review-previous',
'/var/tmp/smail-review-previous/node:other',
'/var/tmp/smail-review-previous/node other'):
with self.subTest(target=target), self.assertRaises(ValueError):
self.bindings([target])
def test_missing_dependency_fails_before_start(self):
with patch.object(Path, 'resolve', side_effect=FileNotFoundError):
with self.assertRaises(FileNotFoundError):
review.dependency_bindings(self.root)
def test_file_dependency_is_rejected(self):
with patch.object(Path, 'resolve', return_value=self.root / 'node'):
with patch.object(Path, 'is_dir', return_value=False):
with self.assertRaises(ValueError):
review.dependency_bindings(self.root)
if __name__ == '__main__':
unittest.main()

View File

@ -15,6 +15,7 @@ const { readFormConfig, getFieldGroups, updateFormConfig, validateFormConfig } =
test('公网 HTTPS 端口独立编辑且保留内部监听与模式切换', () => { test('公网 HTTPS 端口独立编辑且保留内部监听与模式切换', () => {
const config = { const config = {
database: { type: 'sqlite' },
domains: { primary: 'example.com', web: 'mail.example.com', accepted: ['example.com'] }, domains: { primary: 'example.com', web: 'mail.example.com', accepted: ['example.com'] },
http: { port: 8001, httpsPort: 8002, httpsEnabled: true, tlsMode: 'direct' } http: { port: 8001, httpsPort: 8002, httpsEnabled: true, tlsMode: 'direct' }
} }

View File

@ -96,7 +96,7 @@ test('六个平台默认包含三个插件,构建和发布名称与后端目
assert.match(await read('Makefile'), /package: build_server plugin/) assert.match(await read('Makefile'), /package: build_server plugin/)
assert.match(await read('.github/workflows/release.yml'), /node deploy\/build-plugins\.mjs --target=/) assert.match(await read('.github/workflows/release.yml'), /node deploy\/build-plugins\.mjs --target=/)
assert.match(await read('deploy/package.mjs'), /sha256: createHash\('sha256'\)/) assert.match(await read('deploy/package.mjs'), /sha256: createHash\('sha256'\)/)
assert.match(await read('deploy/package.mjs'), /files,\n createdAt/) assert.match(await read('deploy/package.mjs'), /files,\r?\n createdAt/)
}) })
test('安全工作流必须先构建嵌入资源再执行 Go 测试', async () => { test('安全工作流必须先构建嵌入资源再执行 Go 测试', async () => {

View File

@ -10,6 +10,20 @@ import subprocess
import time import time
def dependency_bindings(root):
bindings = set()
for relative in ('node-v22.23.2-linux-x64', 'frontend/node_modules'):
target = (root / relative).resolve(strict=True)
if (len(target.parts) < 5 or target.parts[1:3] != ('var', 'tmp')
or not target.parts[3].startswith('smail-review-')
or any(not re.fullmatch(r'[A-Za-z0-9_.-]+', part) for part in target.parts[3:])
or not target.is_dir()):
raise ValueError('测试依赖必须位于受控审查目录')
if not target.is_relative_to(root):
bindings.add(str(target))
return sorted(bindings)
def main(): def main():
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument('directory') parser.add_argument('directory')
@ -21,6 +35,8 @@ def main():
assert os.geteuid() == 0 assert os.geteuid() == 0
memory = dict(line.split(':', 1) for line in Path('/proc/meminfo').read_text().splitlines()) memory = dict(line.split(':', 1) for line in Path('/proc/meminfo').read_text().splitlines())
assert int(memory['MemAvailable'].split()[0]) >= 256 * 1024, '可用内存不足' assert int(memory['MemAvailable'].split()[0]) >= 256 * 1024, '可用内存不足'
# PrivateTmp 会隐藏其他审查目录,只读挂载已验证的依赖,不开放整个临时目录。
bindings = dependency_bindings(root)
command = [ command = [
'systemd-run', '--wait', '--pipe', '--unit=smail-frontend-tests-' + time.strftime('%Y%m%d-%H%M%S'), 'systemd-run', '--wait', '--pipe', '--unit=smail-frontend-tests-' + time.strftime('%Y%m%d-%H%M%S'),
'-p', 'User=smail', '-p', 'Group=smail', '-p', 'MemoryMax=128M', '-p', 'MemorySwapMax=0', '-p', 'User=smail', '-p', 'Group=smail', '-p', 'MemoryMax=128M', '-p', 'MemorySwapMax=0',
@ -29,6 +45,7 @@ def main():
'-p', 'PrivateTmp=yes', '-p', 'PrivateNetwork=yes', '-p', 'BindPaths=' + str(root), '-p', 'PrivateTmp=yes', '-p', 'PrivateNetwork=yes', '-p', 'BindPaths=' + str(root),
'-p', 'ProtectSystem=strict', '-p', 'ProtectHome=yes', '-p', 'InaccessiblePaths=/opt/smail /run/smail', '-p', 'ProtectSystem=strict', '-p', 'ProtectHome=yes', '-p', 'InaccessiblePaths=/opt/smail /run/smail',
'-p', 'NoNewPrivileges=yes', '-p', 'ReadWritePaths=' + str(root), '-p', 'WorkingDirectory=' + str(root), '-p', 'NoNewPrivileges=yes', '-p', 'ReadWritePaths=' + str(root), '-p', 'WorkingDirectory=' + str(root),
*[argument for target in bindings for argument in ('-p', 'BindReadOnlyPaths=' + target)],
'python3', str(root / 'run_frontend_review.py'), str(root), '--worker', 'python3', str(root / 'run_frontend_review.py'), str(root), '--worker',
] ]
return subprocess.run(command).returncode return subprocess.run(command).returncode