修复固件哈希兼容性并完善部署配置

This commit is contained in:
mid
2026-08-08 16:18:41 +08:00
parent 290da46567
commit 34503f2638
13 changed files with 271 additions and 25 deletions
+66
View File
@@ -0,0 +1,66 @@
import { readFile } from 'node:fs/promises'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { describe, expect, it } from 'vitest'
import { DEFAULT_PORT } from '../server/index.mjs'
const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
async function readProjectFile(relativePath) {
return readFile(path.join(projectRoot, relativePath), 'utf8')
}
describe('deployment configuration', () => {
it('keeps the default production port synchronized', async () => {
const [dockerfile, compose, environment, script, readme] = await Promise.all([
readProjectFile('Dockerfile'),
readProjectFile('docker-compose.yml'),
readProjectFile('.env.example'),
readProjectFile('web-tools.ps1'),
readProjectFile('README.zh-CN.md'),
])
expect(DEFAULT_PORT).toBe(9000)
expect(dockerfile).toContain('PORT=9000')
expect(dockerfile).toContain('EXPOSE 9000')
expect(dockerfile).toContain('http://127.0.0.1:9000/healthz')
expect(compose).toContain('${WEB_TOOLS_PORT:-9000}:9000')
expect(environment.trim()).toBe('WEB_TOOLS_PORT=9000')
expect(script).toMatch(/\[int\]\$Port = 9000/)
expect(readme).toContain('默认访问地址:`http://服务器地址:9000`')
expect(readme).toContain('生产服务默认监听 `0.0.0.0:9000`')
})
it('preserves the Windows and container production safeguards', async () => {
const [dockerfile, compose, dockerignore, script] = await Promise.all([
readProjectFile('Dockerfile'),
readProjectFile('docker-compose.yml'),
readProjectFile('.dockerignore'),
readProjectFile('web-tools.ps1'),
])
expect(dockerfile).toContain('USER node')
expect(compose).toContain('read_only: true')
expect(compose).toContain('no-new-privileges:true')
expect(compose).toContain('cap_drop:')
expect(dockerignore).toContain('node_modules')
expect(dockerignore).toContain('data')
expect(script).toContain('#Requires -Version 5.1')
expect(script).toContain("@('pull', '--ff-only', $Remote, $Branch)")
expect(script).toContain("@('ci', '--include=dev')")
})
it('provides standard entry points for a new AI session', async () => {
const [entryReadme, agentGuide, completeReadme] = await Promise.all([
readProjectFile('README.md'),
readProjectFile('AGENTS.md'),
readProjectFile('README.zh-CN.md'),
])
expect(entryReadme).toContain('[README.zh-CN.md](README.zh-CN.md)')
expect(entryReadme).toContain('[AGENTS.md](AGENTS.md)')
expect(agentGuide).toContain('## First actions in a new session')
expect(agentGuide).toContain('git status --short --branch')
expect(completeReadme).toContain('## AI 接手清单')
})
})
+9
View File
@@ -3,6 +3,7 @@ import { describe, expect, it } from 'vitest'
import {
buildFirmwareHashInput,
calculateFirmwareHashReport,
digestFirmwareBytes,
encodeFirmwareBlockHeader,
type FirmwareHashByteOrder,
type FirmwareHashMode,
@@ -131,3 +132,11 @@ describe('firmware SHA report', () => {
expect(relocatedWithHeaders.combined).not.toEqual(withHeaders.combined)
})
})
describe('firmware digest provider', () => {
it('falls back to the local implementation when SubtleCrypto is unavailable', async () => {
const data = textToBytes('plain HTTP LAN fallback')
expect(await digestFirmwareBytes('SHA-256', data, undefined)).toBe(hash('sha256', data))
expect(await digestFirmwareBytes('SHA-512', data, undefined)).toBe(hash('sha512', data))
})
})
+48 -1
View File
@@ -2,7 +2,7 @@ import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import path from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import { createWebToolsServer, MAX_JSON_BODY_SIZE } from '../server/index.mjs'
import { assertProductionBuild, createWebToolsServer, MAX_JSON_BODY_SIZE } from '../server/index.mjs'
const temporaryDirectories = []
const servers = []
@@ -47,6 +47,12 @@ async function createFixtureServer() {
}
describe('HTTP server', () => {
it('fails before listening when the production build is missing', async () => {
const directory = await mkdtemp(path.join(tmpdir(), 'web-tools-missing-build-'))
temporaryDirectories.push(directory)
await expect(assertProductionBuild(path.join(directory, 'dist'))).rejects.toThrow('npm.cmd run build')
})
it('serves direct SPA navigation but returns 404 for missing assets', async () => {
const { baseUrl } = await createFixtureServer()
const page = await fetch(`${baseUrl}/tools/base64`, { headers: { Accept: 'text/html' } })
@@ -88,6 +94,47 @@ describe('HTTP server', () => {
expect(JSON.parse(await readFile(path.join(dataDir, 'navigation.json'), 'utf8'))).toEqual(updated)
})
it('normalizes an existing navigation file when a later release adds a tool', async () => {
const { baseUrl, dataDir } = await createFixtureServer()
const legacy = { version: 1, categories: [{ id: 'encoding', tools: ['hex'] }, { id: 'crypto', tools: ['cmac'] }] }
await mkdir(dataDir, { recursive: true })
await writeFile(path.join(dataDir, 'navigation.json'), JSON.stringify(legacy))
const response = await fetch(`${baseUrl}/api/navigation`)
expect(await response.json()).toEqual(legacy)
const expandedCatalog = { ...catalog, tools: [...catalog.tools, { id: 'base64', category: 'encoding' }] }
const expandedDefaults = { ...defaults, categories: [{ id: 'encoding', tools: ['hex', 'base64'] }, ...defaults.categories.slice(1)] }
const secondServer = await createWebToolsServer({
staticRoot: path.join(dataDir, 'missing-dist'),
dataDir,
catalog: expandedCatalog,
defaults: expandedDefaults,
logger: { error: () => undefined },
})
servers.push(secondServer)
await new Promise((resolve, reject) => {
secondServer.once('error', reject)
secondServer.listen(0, '127.0.0.1', resolve)
})
const address = secondServer.address()
const upgraded = await fetch(`http://127.0.0.1:${address.port}/api/navigation`)
expect(await upgraded.json()).toEqual({
version: 1,
categories: [
{ id: 'encoding', tools: ['hex', 'base64'] },
{ id: 'crypto', tools: ['cmac'] },
],
})
expect(JSON.parse(await readFile(path.join(dataDir, 'navigation.json'), 'utf8'))).toEqual({
version: 1,
categories: [
{ id: 'encoding', tools: ['hex', 'base64'] },
{ id: 'crypto', tools: ['cmac'] },
],
})
})
it('rejects invalid, oversized and cross-site navigation writes', async () => {
const { baseUrl } = await createFixtureServer()
const invalid = await fetch(`${baseUrl}/api/navigation`, {