update-window.test.mjs 6.6 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091
  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. assert.equal(await boot.evaluate(() => window.yaoyaoDesktop.openUpdates().then(() => false, () => true)), true)
  32. // Even another renderer carrying the real bridge cannot acquire update privileges.
  33. const denied = await app.evaluate(async ({ BrowserWindow }, preload) => {
  34. const outsider = new BrowserWindow({ show: false, webPreferences: { sandbox: true, contextIsolation: true, preload } })
  35. try {
  36. await outsider.loadURL('data:text/html,<html><body>Untrusted renderer</body></html>')
  37. return await outsider.webContents.executeJavaScript("window.yaoyaoUpdate.check().then(()=>false,()=>true)")
  38. } finally { outsider.destroy() }
  39. }, join(root, 'desktop/update-preload.cjs'))
  40. assert.equal(denied, true)
  41. const opened = app.waitForEvent('window')
  42. await app.evaluate(({ net, shell, Menu }, fixture) => {
  43. const tag = `v${fixture.version}`, api = 'https://api.github.com/repos/Lsamien/yaoyao'
  44. const prefix = `https://github.com/Lsamien/yaoyao/releases/download/${tag}/`
  45. const commit = 'a'.repeat(40)
  46. net.fetch = async url => {
  47. const path = String(url)
  48. if (path === `${api}/releases/latest`) return Response.json({ draft: false, prerelease: false, tag_name: tag, body: '下载、校验、手动安装。', assets: [
  49. { name: fixture.name, size: fixture.size, state: 'uploaded', digest: `sha256:${fixture.digest}`, browser_download_url: prefix + fixture.name },
  50. { name: 'SHA256SUMS.txt', size: fixture.sums.length, state: 'uploaded', browser_download_url: prefix + 'SHA256SUMS.txt' },
  51. ] })
  52. if (path === `${api}/git/ref/tags/${tag}`) return Response.json({ object: { type: 'commit', sha: commit } })
  53. if (path === `https://raw.githubusercontent.com/Lsamien/yaoyao/${commit}/release.json`) return Response.json({ schemaVersion: 1, releaseVersion: fixture.version, webVersion: fixture.version, gitTag: tag })
  54. if (path === prefix + 'SHA256SUMS.txt') return new Response(fixture.sums)
  55. if (path === prefix + fixture.name) return new Response(Buffer.from(fixture.data, 'base64'))
  56. throw new Error('Unexpected update request')
  57. }
  58. shell.openPath = async path => { process.env.YAOYAO_TEST_OPENED_UPDATE = path; return '' }
  59. Menu.getApplicationMenu().getMenuItemById('desktop-update-check').click()
  60. }, { version, name, sums, data: bytes.toString('base64'), size: bytes.length, digest })
  61. const page = await opened
  62. await page.getByRole('button', { name: '下载 DMG', exact: true }).waitFor()
  63. await page.getByRole('button', { name: '下载 DMG', exact: true }).click()
  64. await page.getByRole('button', { name: '打开安装包', exact: true }).waitFor({ timeout: 30000 })
  65. await page.getByRole('button', { name: '检查更新', exact: true }).focus()
  66. await page.keyboard.press('Tab')
  67. await expect(page.getByRole('button', { name: '打开安装包', exact: true })).toBeFocused()
  68. assert.equal(await app.evaluate(() => process.env.YAOYAO_TEST_OPENED_UPDATE), undefined)
  69. await mkdir(join(root, 'test-results/desktop'), { recursive: true })
  70. await page.emulateMedia({ colorScheme: 'light' })
  71. await page.screenshot({ path: join(root, 'test-results/desktop/github-update-light.png') })
  72. await page.emulateMedia({ colorScheme: 'dark', reducedMotion: 'reduce' })
  73. await page.setViewportSize({ width: 380, height: 620 })
  74. assert.equal(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), true)
  75. await page.screenshot({ path: join(root, 'test-results/desktop/github-update-dark.png') })
  76. await page.getByRole('button', { name: '打开安装包', exact: true }).click()
  77. await expect.poll(() => app.evaluate(() => process.env.YAOYAO_TEST_OPENED_UPDATE)).toBe(join(home, 'updates', 'desktop-downloads', version, name))
  78. await app.evaluate(({ net }) => { net.fetch = async () => new Response('', { status: 403 }) })
  79. await page.getByRole('button', { name: '检查更新', exact: true }).click()
  80. await expect(page.getByRole('alert')).toContainText('GitHub 请求受限')
  81. await expect(page.getByRole('button', { name: '打开安装包', exact: true })).toBeHidden()
  82. assert.equal(blocked.listening, true)
  83. await page.keyboard.press('Escape').catch(error => { if (!page.isClosed()) throw error })
  84. await expect.poll(() => page.isClosed()).toBe(true)
  85. } finally {
  86. await app?.close().catch(() => {})
  87. await new Promise(done => blocked.close(done))
  88. await rm(home, { recursive: true, force: true })
  89. }
  90. })