Prechádzať zdrojové kódy

校验正式签名配置并完善 Windows 集成测试诊断

Samien 1 mesiac pred
rodič
commit
fef09f3976

+ 2 - 2
desktop/windows-smoke.test.mjs

@@ -56,9 +56,9 @@ test('packaged Windows client logs in, encrypts pairing, restores its session an
     const menu = await app.evaluate(({ Menu }) => ['desktop-host-use-local', 'desktop-stop-and-quit', 'runner-import'].map(id => Boolean(Menu.getApplicationMenu().getMenuItemById(id))))
     assert.deepEqual(menu, [false, false, false])
     const native = await app.evaluate(async ({ app, BrowserWindow, screen }, { home, url }) => {
-      const { pathToFileURL } = process.getBuiltinModule('node:url')
+      const { createRequire } = process.getBuiltinModule('node:module')
       const { join } = process.getBuiltinModule('node:path')
-      const { DesktopHostCore } = await import(pathToFileURL(join(app.getAppPath(), 'environment-host.mjs')).href)
+      const { DesktopHostCore } = createRequire(join(app.getAppPath(), 'main.mjs'))('./environment-host.mjs')
       const core = new DesktopHostCore({ root: join(process.resourcesPath, 'runtime'), dataRoot: join(home, 'native-acceptance') })
       const owner = 'a'.repeat(64), resource = 'b'.repeat(64)
       const command = { owner, resource, mode: 'local', profile: 'temporary', deadline: Date.now() + 30000 }

+ 14 - 5
desktop/windows-upgrade.test.mjs

@@ -3,7 +3,7 @@ import assert from 'node:assert/strict'
 import { createServer } from 'node:http'
 import { execFile } from 'node:child_process'
 import { promisify } from 'node:util'
-import { mkdtemp, readFile, writeFile, mkdir, rm, readdir } from 'node:fs/promises'
+import { mkdtemp, readFile, writeFile, mkdir, rm, readdir, realpath, copyFile } from 'node:fs/promises'
 import { tmpdir } from 'node:os'
 import { join, resolve, basename } from 'node:path'
 import { randomUUID } from 'node:crypto'
@@ -15,13 +15,13 @@ test('installed NSIS client rejects a corrupt update, upgrades and preserves set
   skip: process.platform !== 'win32' || process.env.DESKTOP_TEST_UPGRADE !== '1', timeout: 600000,
 }, async () => {
   const root = resolve(import.meta.dirname, '..'), output = join(root, 'desktop-release/windows-x64')
-  const evidence = join(root, 'test-results/windows-client'), sandbox = await mkdtemp(join(tmpdir(), 'yaoyao-upgrade-'))
+  const evidence = join(root, 'test-results/windows-client'), sandbox = await realpath(await mkdtemp(join(tmpdir(), 'yaoyao-upgrade-')))
   const home = join(sandbox, '中文配置'), installed = join(sandbox, 'app'), feed = join(sandbox, 'feed')
   const executablePath = join(installed, 'Yaoyao.exe'), cacheName = 'yaoyao-upgrade-' + randomUUID()
   const originalManifest = await readFile(join(root, '.desktop-build/release.json'), 'utf8')
   const originalPackage = await readFile(join(root, '.desktop-build/shell/package.json'), 'utf8')
   const version = JSON.parse(originalPackage).version, nextVersion = version.replace(/\d+$/, value => String(Number(value) + 1))
-  let application, corrupt = true, requireCookie = false, restored = 0
+  let application, logs, corrupt = true, requireCookie = false, restored = 0
   const server = createServer(async (req, res) => {
     try {
       const path = new URL(req.url, 'http://localhost').pathname
@@ -56,12 +56,13 @@ test('installed NSIS client rejects a corrupt update, upgrades and preserves set
       await writeFile(join(root, '.desktop-build/shell/package.json'), JSON.stringify({ ...JSON.parse(originalPackage), version: nextVersion }))
       process.env.CSC_IDENTITY_AUTO_DISCOVERY = 'false'
       await build({ projectDir: root, targets: Platform.WINDOWS.createTarget(['nsis'], Arch.x64), publish: 'never',
-        config: { directories: { output: feed }, forceCodeSigning: false } })
+        config: { directories: { output: feed }, forceCodeSigning: false, win: { signExecutable: false } } })
     } finally {
       await writeFile(join(root, '.desktop-build/release.json'), originalManifest)
       await writeFile(join(root, '.desktop-build/shell/package.json'), originalPackage)
     }
     await execute(join(output, `Yaoyao-${version}-win-x64-setup.exe`), ['/S', `/D=${installed}`], { timeout: 120000 })
+    console.log('NSIS 安装完成,开始测试隔离更新源')
     const preferences = { startupChoice: 'remote', remoteServer: origin, backgroundAtLogin: true }
     await writeFile(join(home, 'desktop-preferences.json'), JSON.stringify(preferences))
     // This installed test copy alone uses the isolated HTTP update source.
@@ -69,6 +70,7 @@ test('installed NSIS client rejects a corrupt update, upgrades and preserves set
     const launch = () => electron.launch({ executablePath, env: { ...process.env,
       HERMES_YAOYAO_DESKTOP_TEST_HOME: home, HERMES_YAOYAO_DESKTOP_TEST_AUTO_UPDATE: '1' } })
     application = await launch(); let page = await application.firstWindow(); await page.waitForURL(origin + '/**')
+    logs = await application.evaluate(({ app }) => app.getPath('logs'))
     assert.equal(await application.evaluate(({ app }) => app.getVersion()), version)
     await application.evaluate(async ({ session, safeStorage }, { origin, home }) => {
       await session.defaultSession.cookies.set({ url: origin, name: 'session', value: 'upgrade-fixture', expirationDate: Date.now() / 1000 + 3600 })
@@ -83,15 +85,18 @@ test('installed NSIS client rejects a corrupt update, upgrades and preserves set
     await updates.locator('#download').click()
     await expect(updates.locator('#error')).toBeVisible({ timeout: 120000 })
     assert.equal(await updates.evaluate(async () => (await window.yaoyaoUpdate.state()).phase), 'failed')
+    console.log('已拒绝损坏安装包,重试完整下载')
     corrupt = false
     await updates.locator('#check').click(); await expect(updates.locator('#download')).toBeEnabled()
     await updates.locator('#download').click(); await expect(updates.locator('#install')).toBeEnabled({ timeout: 120000 })
     await updates.screenshot({ path: join(evidence, 'upgrade-ready.png') })
+    console.log('完整更新包已就绪,开始重启替换')
     const closed = application.waitForEvent('close'); await updates.locator('#install').click().catch(error => { if (!updates.isClosed()) throw error })
     await closed; application = undefined
-    await expect.poll(async () => JSON.parse(await readFile(join(installed, 'resources/runtime/release.json'), 'utf8')).webVersion,
+    await expect.poll(async () => readFile(join(installed, 'resources/runtime/release.json'), 'utf8').then(text => JSON.parse(text).webVersion).catch(() => ''),
       { timeout: 120000 }).toBe(nextVersion)
     await expect.poll(() => restored, { timeout: 60000 }).toBeGreaterThan(0)
+    console.log('新版本已自动启动,登录 Cookie 保留')
     await stopInstalled()
     application = await launch(); page = await application.firstWindow(); await page.waitForURL(origin + '/**')
     assert.equal(await application.evaluate(({ app }) => app.getVersion()), nextVersion)
@@ -109,8 +114,12 @@ test('installed NSIS client rejects a corrupt update, upgrades and preserves set
     await writeFile(join(evidence, 'upgrade.json'), JSON.stringify({ platform: 'win32', from: version, to: nextVersion,
       installed: true, corruptDownloadRejected: true, nsisReplacement: true, automaticRestart: true,
       cookieRestored: true, settingsPreserved: true, dpapiPreserved: true, uninstalled: true, dataRetained: true }, null, 2))
+  } catch (error) {
+    for (const [index, page] of (application?.windows() || []).entries()) await page.screenshot({ path: join(evidence, `upgrade-failed-${index}.png`) }).catch(() => {})
+    throw error
   } finally {
     await application?.close().catch(() => {}); await stopInstalled()
+    if (logs) await copyFile(join(logs, 'verification/server.log'), join(evidence, 'upgrade-main.log')).catch(() => {})
     server.closeAllConnections(); await new Promise(done => server.close(done))
     await rm(sandbox, { recursive: true, force: true, maxRetries: 10, retryDelay: 500 })
     await rm(join(process.env.LOCALAPPDATA, cacheName), { recursive: true, force: true }).catch(() => {})

+ 7 - 2
desktop/windows.test.mjs

@@ -64,11 +64,16 @@ test('NSIS errors before install can retry, while dispatched installs stay fence
   manager.nativeStarted = true; manager.installAttempted = true; manager.failure(new Error('installer failed'))
   assert.equal(manager.snapshot().retryable, false)
 })
-test('signed Windows builds fail closed without a certificate and publisher', () => {
-  assert.equal(windowsSigningOptions({}).forceCodeSigning, false)
+test('signed Windows builds fail closed without a certificate and publisher', async () => {
+  const { validateConfiguration } = await import('app-builder-lib/out/util/config/config.js')
+  const unsigned = windowsSigningOptions({})
+  assert.equal(unsigned.forceCodeSigning, false); assert.equal(unsigned.win.signExecutable, false)
+  await validateConfiguration(unsigned)
   assert.throws(() => windowsSigningOptions({ YAOYAO_WINDOWS_SIGNED: '1' }), /CSC_LINK/)
   const result = windowsSigningOptions({ YAOYAO_WINDOWS_SIGNED: '1', CSC_LINK: 'fixture.pfx', YAOYAO_WINDOWS_PUBLISHER: 'Fixture publisher' })
   assert.equal(result.forceCodeSigning, true); assert.equal(result.win.verifyUpdateCodeSignature, true)
+  assert.equal(result.win.signExecutable, true); assert.equal(result.win.signtoolOptions.publisherName, 'Fixture publisher')
+  await validateConfiguration(result)
 })
 test('Windows artifact verification detects corruption before delivery', async t => {
   const dir = await mkdtemp(join(tmpdir(), 'yaoyao-win-release-')); t.after(() => rm(dir, { recursive: true, force: true }))

+ 4 - 2
scripts/windows-desktop-release.mjs

@@ -11,9 +11,10 @@ async function hash(path, algorithm, encoding) {
   return result.digest(encoding)
 }
 export function windowsSigningOptions(env = process.env) {
-  if (env.YAOYAO_WINDOWS_SIGNED !== '1') return { forceCodeSigning: false, win: { signAndEditExecutable: true } }
+  if (env.YAOYAO_WINDOWS_SIGNED !== '1') return { forceCodeSigning: false, win: { signAndEditExecutable: true, signExecutable: false } }
   if (!env.CSC_LINK || !env.YAOYAO_WINDOWS_PUBLISHER?.trim()) throw new Error('正式 Windows 包需要 CSC_LINK、CSC_KEY_PASSWORD(如适用)和 YAOYAO_WINDOWS_PUBLISHER')
-  return { forceCodeSigning: true, win: { publisherName: env.YAOYAO_WINDOWS_PUBLISHER.trim(), verifyUpdateCodeSignature: true } }
+  return { forceCodeSigning: true, win: { signExecutable: true,
+    signtoolOptions: { publisherName: env.YAOYAO_WINDOWS_PUBLISHER.trim() }, verifyUpdateCodeSignature: true } }
 }
 export async function verifyWindowsArtifacts(directory, version) {
   const name = `Yaoyao-${version}-win-x64-setup.exe`
@@ -43,6 +44,7 @@ export async function packageWindowsDesktop() {
   await verifyWindowsArtifacts(directory, version)
   const feed = load(await readFile(join(directory, 'win-unpacked/resources/app-update.yml'), 'utf8'))
   if (feed.provider !== 'github' || feed.owner !== 'Lsamien' || feed.repo !== 'yaoyao') throw new Error('Windows 更新源配置无效')
+  if (process.env.YAOYAO_WINDOWS_SIGNED === '1' && ![feed.publisherName].flat().includes(process.env.YAOYAO_WINDOWS_PUBLISHER.trim())) throw new Error('正式 Windows 更新配置缺少发布者签名校验')
   const info = JSON.parse(await readFile(join(root, 'build-info.json'), 'utf8'))
   await writeFile(join(directory, 'BUILD-INFO.json'), JSON.stringify({ version, platform: 'win32', arch: 'x64',
     signing: process.env.YAOYAO_WINDOWS_SIGNED === '1' ? 'signed' : 'unsigned-test', ...info }, null, 2) + '\n')