update-entry.test.mjs 6.8 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
  1. import { test } from 'node:test'
  2. import assert from 'node:assert/strict'
  3. import { mkdtemp, readFile, writeFile, realpath, 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 { _electron as electron, expect } from '@playwright/test'
  8. import { dataKey } from './service-manager.mjs'
  9. test('the main local app can open the native updater and repeated requests reuse its window', { timeout: 60000 }, async () => {
  10. const root = resolve(import.meta.dirname, '..')
  11. const home = await realpath(await mkdtemp(join(tmpdir(), 'yaoyao-update-entry-')))
  12. const version = JSON.parse(await readFile(join(root, '.desktop-build/release.json'), 'utf8')).webVersion
  13. const identity = { protocol: 1, instanceId: 'update-entry', pid: process.pid, dataKey: dataKey(home), version }
  14. // Let Playwright attach to the boot renderer before the fixture changes its URL.
  15. let releaseIdentity
  16. const identityReady = new Promise(resolve => { releaseIdentity = resolve })
  17. const server = createServer(async (req, res) => {
  18. if (req.url === '/desktop/service/session') { res.setHeader('Content-Type','application/json'); res.end(JSON.stringify({user:{role:'admin',localDesktop:true},setCookies:['hermes_yaoyao_session=fixture; Path=/; HttpOnly; Max-Age=31536000']})); return }
  19. if (req.url === '/api/app/desktop-session') { res.setHeader('Set-Cookie','hermes_yaoyao_session=fixture; Path=/; HttpOnly; Max-Age=31536000'); res.end('{}'); return }
  20. if (req.url === '/desktop/service') { await identityReady; res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify(identity)) }
  21. else if(req.url.split('?')[0]==='/api/app/bootstrap'){res.setHeader('Content-Type','application/json');res.end(JSON.stringify({authenticated:true,csrfToken:'fixture'}))}
  22. else { res.setHeader('Content-Type', 'text/html; charset=utf-8'); res.end('<title>Update entry fixture</title><button onclick="window.yaoyaoDesktop.openUpdates()">检测更新</button>') }
  23. })
  24. await new Promise(done => server.listen(0, '127.0.0.1', done))
  25. const origin = `http://127.0.0.1:${server.address().port}`
  26. await writeFile(join(home, 'service-instance.json'), JSON.stringify({ ...identity, url: origin, token: 'fixture' }))
  27. let app
  28. try {
  29. app = await electron.launch({ args: [root], cwd: root, env: { ...process.env, HERMES_YAOYAO_DESKTOP_TEST_HOME: home, HERMES_YAOYAO_DESKTOP_PORT: String(server.address().port), HERMES_YAOYAO_DESKTOP_TEST_SYNC: '0' } })
  30. const page = await app.firstWindow()
  31. releaseIdentity()
  32. await page.waitForURL(origin + '/**')
  33. await app.evaluate(({ net }) => { net.fetch = async () => new Response('', { status: 403 }) })
  34. const opened = app.waitForEvent('window')
  35. await page.getByRole('button', { name: '检测更新', exact: true }).click()
  36. const updates = await opened
  37. await expect(updates.getByRole('alert')).toContainText('GitHub 请求受限')
  38. await page.evaluate(() => window.yaoyaoDesktop.openUpdates())
  39. assert.equal(app.windows().length, 2)
  40. const denied = await app.evaluate(async ({ BrowserWindow }, { origin, preload }) => {
  41. const outsider = new BrowserWindow({ show: false, webPreferences: { sandbox: true, contextIsolation: true, preload } })
  42. try {
  43. await outsider.loadURL(origin)
  44. return await outsider.webContents.executeJavaScript('window.yaoyaoDesktop.openUpdates().then(() => false, () => true)')
  45. } finally { outsider.destroy() }
  46. }, { origin, preload: join(root, 'desktop/preload.cjs') })
  47. assert.equal(denied, true)
  48. assert.equal(server.listening, true)
  49. } finally {
  50. releaseIdentity()
  51. await app?.close().catch(() => {})
  52. await new Promise(done => server.close(done))
  53. await rm(home, { recursive: true, force: true })
  54. }
  55. })
  56. test('client mode opens local App updates without a local service or admin role', { timeout: 60000 }, async () => {
  57. const root = resolve(import.meta.dirname, '..')
  58. const home = await realpath(await mkdtemp(join(tmpdir(), 'yaoyao-client-update-')))
  59. const remote = createServer((req, res) => {
  60. if (req.url === '/desktop/service/session') { res.setHeader('Content-Type','application/json'); res.end(JSON.stringify({user:{role:'admin',localDesktop:true},setCookies:['hermes_yaoyao_session=fixture; Path=/; HttpOnly; Max-Age=31536000']})); return }
  61. if (req.url === '/api/app/desktop-session') { res.setHeader('Set-Cookie','hermes_yaoyao_session=fixture; Path=/; HttpOnly; Max-Age=31536000'); res.end('{}'); return }
  62. if (req.url.split('?')[0] === '/api/app/bootstrap') { res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify({ authenticated: true, csrfToken: 'fixture', user: { role: 'member' } })) }
  63. else { res.setHeader('Content-Type', 'text/html'); res.end('<title>Remote member</title><button onclick="window.yaoyaoDesktop.openUpdates()">App update</button>') }
  64. })
  65. await new Promise(done => remote.listen(0, '127.0.0.1', done))
  66. const origin = `http://127.0.0.1:${remote.address().port}`
  67. await writeFile(join(home, 'desktop-preferences.json'), JSON.stringify({ startupChoice: 'remote', remoteServer: origin }))
  68. let app
  69. try {
  70. app = await electron.launch({ args: [root], cwd: root, env: { ...process.env, HERMES_YAOYAO_DESKTOP_TEST_HOME: home, HERMES_YAOYAO_DESKTOP_PORT: '1', HERMES_YAOYAO_DESKTOP_TEST_SYNC: '0' } })
  71. const page = await app.firstWindow(); await page.waitForURL(origin + '/**')
  72. assert.equal((await page.evaluate(() => window.yaoyaoDesktop.modeState())).mode, 'client')
  73. assert.equal(await page.evaluate(() => typeof window.yaoyaoUpdate), 'undefined')
  74. await app.evaluate(({ net }) => { net.fetch = async () => new Response('', { status: 403 }) })
  75. const opened = app.waitForEvent('window'); await page.getByRole('button', { name: 'App update' }).click()
  76. const updates = await opened
  77. await expect(updates.getByRole('heading', { name: 'App 更新' })).toBeVisible()
  78. await expect(updates.getByRole('alert')).toContainText('GitHub 请求受限')
  79. const denied = await app.evaluate(async ({ BrowserWindow }, { origin, preload }) => {
  80. const outsider = new BrowserWindow({ show: false, webPreferences: { sandbox: true, contextIsolation: true, preload } })
  81. try {
  82. await outsider.loadURL(origin)
  83. return await outsider.webContents.executeJavaScript('Promise.all([window.yaoyaoUpdate.check(),window.yaoyaoUpdate.download(),window.yaoyaoUpdate.install()].map(p=>p.then(()=>false,()=>true)))')
  84. } finally { outsider.destroy() }
  85. }, { origin, preload: join(root, 'desktop/update-preload.cjs') })
  86. assert.deepEqual(denied, [true, true, true])
  87. assert.equal(remote.listening, true)
  88. } finally {
  89. await app?.close().catch(() => {})
  90. await new Promise(done => { remote.close(done); remote.closeAllConnections() })
  91. await rm(home, { recursive: true, force: true })
  92. }
  93. })