| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116 |
- import { test } from 'node:test'
- import assert from 'node:assert/strict'
- import { mkdtemp, writeFile, readFile, rm, symlink } from 'node:fs/promises'
- import { join } from 'node:path'
- import { tmpdir } from 'node:os'
- import { randomUUID, createHash } from 'node:crypto'
- import { EventEmitter } from 'node:events'
- import { DesktopCredentials } from './credentials.mjs'
- import { DesktopAutoUpdateManager } from './auto-update-manager.mjs'
- import { powershellArguments } from './windows-shell.mjs'
- import { execHostShell } from './host-files.mjs'
- import { windowsFrame, sameWindowsDisplay } from './windows-input.mjs'
- import { FileTransferFiles } from './file-transfer.mjs'
- import { windowsSigningOptions, verifyWindowsArtifacts } from '../scripts/windows-desktop-release.mjs'
- test('Windows credentials use the asynchronous OS provider and reject foreign/corrupt ciphertext', async () => {
- const raw = 'fixture-token', cipher = Buffer.from('opaque-encrypted-value')
- const store = { isAsyncEncryptionAvailable: async () => true, encryptStringAsync: async value => { assert.equal(value, raw); return cipher },
- decryptStringAsync: async value => { assert.deepEqual(value, cipher); return { result: raw } } }
- const credentials = new DesktopCredentials({ home: 'C:\\fixture', platform: 'win32', safeStorage: store })
- const encrypted = await credentials.encrypt(raw)
- assert.equal(encrypted.includes(raw), false)
- assert.equal(await credentials.decrypt(encrypted), raw)
- await assert.rejects(credentials.decrypt(Buffer.from('YAOYAO-RUNNER-KEYCHAIN-1\ninvalid')), /不属于 Windows/)
- store.isAsyncEncryptionAvailable = async () => false
- await assert.rejects(credentials.encrypt(raw), /加密暂不可用/)
- store.decryptStringAsync = async () => { throw new Error('DPAPI failure') }
- await assert.rejects(credentials.decrypt(encrypted), /重新登录/)
- })
- test('PowerShell transports quoted Unicode as UTF-16 without shell interpolation', () => {
- const command = 'Write-Output "你好 $env:USERNAME"; exit 7', args = powershellArguments(command)
- assert.equal(args.at(-2), '-EncodedCommand')
- const script = Buffer.from(args.at(-1), 'base64').toString('utf16le')
- assert.ok(script.includes(command)); assert.match(script, /UTF8Encoding/); assert.match(script, /LASTEXITCODE/)
- })
- test('Windows input invalidates screenshots after DPI, geometry or primary display changes', () => {
- const d = { id: 1, scaleFactor: 1.5, bounds: { x: 0, y: 0, width: 1280, height: 720 } }
- const physical = { x: 0, y: 0, width: 1920, height: 1080 }
- const frame = windowsFrame(d, { width: 1600, height: 900 }, physical)
- assert.equal(sameWindowsDisplay(frame, d, physical), true)
- assert.equal(sameWindowsDisplay(frame, { ...d, scaleFactor: 2 }, physical), false)
- assert.equal(sameWindowsDisplay(frame, { ...d, id: 2 }, physical), false)
- assert.equal(sameWindowsDisplay(frame, d, { ...physical, width: 2560 }), false)
- })
- test('NSIS installation is explicit and runs exactly once after cleanup', async t => {
- const driver = new EventEmitter(), events = []
- driver.checkForUpdates = async () => ({ isUpdateAvailable: true, updateInfo: { version: '1.1.0' } })
- driver.downloadUpdate = async () => { driver.emit('update-downloaded'); return ['setup.exe'] }
- driver.quitAndInstall = () => events.push('install')
- const manager = new DesktopAutoUpdateManager({ driver, platform: 'win32', version: '1.0.0', prepareInstall: async () => events.push('cleanup') })
- t.after(() => { manager.stopChecking(); clearTimeout(manager.restartTimer) })
- assert.equal(driver.autoInstallOnAppQuit, false)
- await manager.check(); await manager.download()
- assert.equal(manager.snapshot().phase, 'ready'); assert.equal(manager.stagingTimer, undefined)
- assert.deepEqual(events, [])
- await manager.install(); await manager.install()
- assert.deepEqual(events, ['cleanup', 'install'])
- })
- test('NSIS errors before install can retry, while dispatched installs stay fenced', async t => {
- const driver = new EventEmitter(), manager = new DesktopAutoUpdateManager({ driver, platform: 'win32', version: '1.0.0' })
- t.after(() => manager.stopChecking())
- manager.nativeStarted = true; manager.failure(new Error('checksum'))
- assert.equal(manager.snapshot().retryable, true); assert.equal(manager.nativeStarted, false)
- 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', 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 }))
- const name = 'Yaoyao-1.0.0-win-x64-setup.exe', bytes = Buffer.from('MZ-test-installer')
- await writeFile(join(dir, name), bytes); await writeFile(join(dir, name + '.blockmap'), 'map')
- await writeFile(join(dir, 'latest.yml'), JSON.stringify({ version: '1.0.0', files: [{ url: name, size: bytes.length, sha512: createHash('sha512').update(bytes).digest('base64') }] }))
- await verifyWindowsArtifacts(dir, '1.0.0')
- assert.match(await readFile(join(dir, 'SHA256SUMS-win-x64.txt'), 'utf8'), /setup\.exe/)
- await writeFile(join(dir, name), 'corrupt')
- await assert.rejects(verifyWindowsArtifacts(dir, '1.0.0'), /不匹配/)
- })
- test('native Windows PowerShell preserves Chinese, exit status and cancellation', { skip: process.platform !== 'win32' }, async t => {
- const root = await mkdtemp(join(tmpdir(), '夭夭-shell-')); t.after(() => rm(root, { recursive: true, force: true }))
- const result = await execHostShell(root, { command: "[Console]::WriteLine('你好,Windows'); exit 7" })
- assert.equal(result.stdout.trim(), '你好,Windows'); assert.equal(result.exitCode, 7)
- const controller = new AbortController()
- const pending = execHostShell(root, { command: 'Start-Sleep -Seconds 60' }, { signal: controller.signal })
- setTimeout(() => controller.abort(), 500)
- assert.equal((await pending).cancelled, true)
- const childCommand = "$p = Start-Process -FilePath $env:ComSpec -ArgumentList '/c ping -n 60 127.0.0.1 > nul' -WindowStyle Hidden -PassThru; [Console]::WriteLine($p.Id)"
- for (const wait of [false, true]) {
- const result = await execHostShell(root, { command: childCommand + (wait ? '; Start-Sleep -Seconds 60' : ''), timeoutMs: 3000 })
- assert.equal(result.timedOut, wait)
- const pid = Number(result.stdout.trim()); assert.ok(pid > 0, result.stderr)
- const alive = await execHostShell(root, { command: `if (Get-Process -Id ${pid} -ErrorAction SilentlyContinue) { exit 1 } else { exit 0 }` })
- assert.equal(alive.exitCode, 0, 'PowerShell 子进程在结束或超时后必须退出')
- }
- })
- test('chunked file writes preserve contents and reject directory links outside the root', async t => {
- const root = await mkdtemp(join(tmpdir(), 'yaoyao-win-files-')), outside = await mkdtemp(join(tmpdir(), 'yaoyao-win-outside-'))
- const files = new FileTransferFiles(root)
- t.after(async () => { await files.close(); await rm(root, { recursive: true, force: true }); await rm(outside, { recursive: true, force: true }) })
- const bytes = Buffer.from('中文附件'), transferId = randomUUID(), path = '中文 文件.txt'
- await files.call({ op: 'transfer-write-open', transferId, path, maxBytes: 1048576, size: bytes.length, sha256: createHash('sha256').update(bytes).digest('hex'), overwrite: false })
- await files.call({ op: 'transfer-append', transferId, offset: 0, data: bytes.toString('base64') })
- await files.call({ op: 'transfer-finish', transferId })
- assert.equal(await readFile(join(root, path), 'utf8'), '中文附件')
- await symlink(outside, join(root, 'escape'), process.platform === 'win32' ? 'junction' : 'dir')
- await assert.rejects(files.path('escape/secret', true), /超出允许范围/)
- })
|