update-window.test.mjs 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990
  1. import { test } from 'node:test'
  2. import assert from 'node:assert/strict'
  3. import { mkdtemp, mkdir, writeFile, readFile, rm } from 'node:fs/promises'
  4. import { tmpdir } from 'node:os'
  5. import { join, resolve } from 'node:path'
  6. import { createServer } from 'node:http'
  7. import { execFile } from 'node:child_process'
  8. import { promisify } from 'node:util'
  9. import { createHash } from 'node:crypto'
  10. import { _electron as electron, expect } from '@playwright/test'
  11. const root = resolve(import.meta.dirname, '..')
  12. test('native update window works without a Web service, verifies a real DMG and denies the boot renderer', { timeout: 90000 }, async () => {
  13. const home = await mkdtemp(join(tmpdir(), 'yaoyao-update-window-'))
  14. const blocked = createServer((req, res) => { res.setHeader('Content-Type', 'application/json'); res.end('{"ok":true}') })
  15. await new Promise(done => blocked.listen(0, '127.0.0.1', done))
  16. let app
  17. try {
  18. const folder = join(home, 'image-source'); await mkdir(folder); await writeFile(join(folder, 'fixture.txt'), 'DMG integrity fixture')
  19. const image = join(home, 'fixture.dmg')
  20. await promisify(execFile)('/usr/bin/hdiutil', ['create', '-srcfolder', folder, '-volname', 'YaoYao Update Test', '-format', 'UDZO', image])
  21. const bytes = await readFile(image), digest = createHash('sha256').update(bytes).digest('hex')
  22. const current = JSON.parse(await readFile(join(root, 'release.json'), 'utf8')).webVersion
  23. const version = current.split('.').map((part, i) => i === 2 ? String(Number(part) + 1) : part).join('.')
  24. const name = `Yaoyao-${version}-arm64.dmg`, sums = `${digest} ${name}\n`
  25. app = await electron.launch({ ...(process.env.DESKTOP_TEST_EXECUTABLE ? { executablePath: process.env.DESKTOP_TEST_EXECUTABLE } : {}),
  26. args: process.env.DESKTOP_TEST_EXECUTABLE ? [] : [root], cwd: root,
  27. env: { ...process.env, HERMES_YAOYAO_DESKTOP_TEST_HOME: home, HERMES_YAOYAO_DESKTOP_TEST_SYNC: '1', HERMES_YAOYAO_DESKTOP_PORT: String(blocked.address().port), HERMES_YAOYAO_UPSTREAM: 'http://127.0.0.1:1' } })
  28. const boot = await app.firstWindow()
  29. await boot.locator('#status[role="alert"]').waitFor()
  30. assert.equal(await boot.evaluate(() => typeof window.yaoyaoUpdate), 'undefined')
  31. // Even another renderer carrying the real bridge cannot acquire update privileges.
  32. const denied = await app.evaluate(async ({ BrowserWindow }, preload) => {
  33. const outsider = new BrowserWindow({ show: false, webPreferences: { sandbox: true, contextIsolation: true, preload } })
  34. try {
  35. await outsider.loadURL('data:text/html,<html><body>Untrusted renderer</body></html>')
  36. return await outsider.webContents.executeJavaScript("window.yaoyaoUpdate.check().then(()=>false,()=>true)")
  37. } finally { outsider.destroy() }
  38. }, join(root, 'desktop/update-preload.cjs'))
  39. assert.equal(denied, true)
  40. const opened = app.waitForEvent('window')
  41. await app.evaluate(({ net, shell, Menu }, fixture) => {
  42. const tag = `v${fixture.version}`, api = 'https://api.github.com/repos/Lsamien/hermes-yaoyao'
  43. const prefix = `https://github.com/Lsamien/hermes-yaoyao/releases/download/${tag}/`
  44. const commit = 'a'.repeat(40)
  45. net.fetch = async url => {
  46. const path = String(url)
  47. if (path === `${api}/releases/latest`) return Response.json({ draft: false, prerelease: false, tag_name: tag, body: '下载、校验、手动安装。', assets: [
  48. { name: fixture.name, size: fixture.size, state: 'uploaded', digest: `sha256:${fixture.digest}`, browser_download_url: prefix + fixture.name },
  49. { name: 'SHA256SUMS.txt', size: fixture.sums.length, state: 'uploaded', browser_download_url: prefix + 'SHA256SUMS.txt' },
  50. ] })
  51. if (path === `${api}/git/ref/tags/${tag}`) return Response.json({ object: { type: 'commit', sha: commit } })
  52. if (path === `https://raw.githubusercontent.com/Lsamien/hermes-yaoyao/${commit}/release.json`) return Response.json({ schemaVersion: 1, releaseVersion: fixture.version, webVersion: fixture.version, gitTag: tag })
  53. if (path === prefix + 'SHA256SUMS.txt') return new Response(fixture.sums)
  54. if (path === prefix + fixture.name) return new Response(Buffer.from(fixture.data, 'base64'))
  55. throw new Error('Unexpected update request')
  56. }
  57. shell.openPath = async path => { process.env.YAOYAO_TEST_OPENED_UPDATE = path; return '' }
  58. Menu.getApplicationMenu().getMenuItemById('desktop-update-check').click()
  59. }, { version, name, sums, data: bytes.toString('base64'), size: bytes.length, digest })
  60. const page = await opened
  61. await page.getByRole('button', { name: '下载 DMG', exact: true }).waitFor()
  62. await page.getByRole('button', { name: '下载 DMG', exact: true }).click()
  63. await page.getByRole('button', { name: '打开安装包', exact: true }).waitFor({ timeout: 30000 })
  64. await page.getByRole('button', { name: '检查更新', exact: true }).focus()
  65. await page.keyboard.press('Tab')
  66. await expect(page.getByRole('button', { name: '打开安装包', exact: true })).toBeFocused()
  67. assert.equal(await app.evaluate(() => process.env.YAOYAO_TEST_OPENED_UPDATE), undefined)
  68. await mkdir(join(root, 'test-results/desktop'), { recursive: true })
  69. await page.emulateMedia({ colorScheme: 'light' })
  70. await page.screenshot({ path: join(root, 'test-results/desktop/github-update-light.png') })
  71. await page.emulateMedia({ colorScheme: 'dark', reducedMotion: 'reduce' })
  72. await page.setViewportSize({ width: 380, height: 620 })
  73. assert.equal(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), true)
  74. await page.screenshot({ path: join(root, 'test-results/desktop/github-update-dark.png') })
  75. await page.getByRole('button', { name: '打开安装包', exact: true }).click()
  76. await expect.poll(() => app.evaluate(() => process.env.YAOYAO_TEST_OPENED_UPDATE)).toBe(join(home, 'updates', 'desktop-downloads', version, name))
  77. await app.evaluate(({ net }) => { net.fetch = async () => new Response('', { status: 403 }) })
  78. await page.getByRole('button', { name: '检查更新', exact: true }).click()
  79. await expect(page.getByRole('alert')).toContainText('GitHub 请求受限')
  80. await expect(page.getByRole('button', { name: '打开安装包', exact: true })).toBeHidden()
  81. assert.equal(blocked.listening, true)
  82. await page.keyboard.press('Escape').catch(error => { if (!page.isClosed()) throw error })
  83. await expect.poll(() => page.isClosed()).toBe(true)
  84. } finally {
  85. await app?.close().catch(() => {})
  86. await new Promise(done => blocked.close(done))
  87. await rm(home, { recursive: true, force: true })
  88. }
  89. })