api-autoupdater-darwin-spec.ts 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  1. import { expect } from 'chai'
  2. import * as cp from 'child_process'
  3. import * as http from 'http'
  4. import * as express from 'express'
  5. import * as fs from 'fs-extra'
  6. import * as os from 'os'
  7. import * as path from 'path'
  8. import { AddressInfo } from 'net';
  9. const features = process.electronBinding('features')
  10. const fixturesPath = path.resolve(__dirname, '../spec/fixtures')
  11. // We can only test the auto updater on darwin non-component builds
  12. const describeFn = (process.platform === 'darwin' && !process.mas && !features.isComponentBuild() ? describe : describe.skip)
  13. describeFn('autoUpdater behavior', function () {
  14. this.timeout(120000)
  15. let identity = ''
  16. beforeEach(function () {
  17. const result = cp.spawnSync(path.resolve(__dirname, '../script/codesign/get-trusted-identity.sh'))
  18. if (result.status !== 0 || result.stdout.toString().trim().length === 0) {
  19. // Per https://circleci.com/docs/2.0/env-vars:
  20. // CIRCLE_PR_NUMBER is only present on forked PRs
  21. if (isCI && !process.env.CIRCLE_PR_NUMBER) {
  22. throw new Error('No valid signing identity available to run autoUpdater specs')
  23. }
  24. this.skip()
  25. } else {
  26. identity = result.stdout.toString().trim()
  27. }
  28. })
  29. it('should have a valid code signing identity', () => {
  30. expect(identity).to.be.a('string').with.lengthOf.at.least(1)
  31. })
  32. const copyApp = async (newDir: string, fixture = 'initial') => {
  33. const appBundlePath = path.resolve(process.execPath, '../../..')
  34. const newPath = path.resolve(newDir, 'Electron.app')
  35. cp.spawnSync('cp', ['-R', appBundlePath, path.dirname(newPath)])
  36. const appDir = path.resolve(newPath, 'Contents/Resources/app')
  37. await fs.mkdirp(appDir)
  38. await fs.copy(path.resolve(fixturesPath, 'auto-update', fixture), appDir)
  39. const plistPath = path.resolve(newPath, 'Contents', 'Info.plist')
  40. await fs.writeFile(
  41. plistPath,
  42. (await fs.readFile(plistPath, 'utf8')).replace('<key>BuildMachineOSBuild</key>', `<key>NSAppTransportSecurity</key>
  43. <dict>
  44. <key>NSAllowsArbitraryLoads</key>
  45. <true/>
  46. <key>NSExceptionDomains</key>
  47. <dict>
  48. <key>localhost</key>
  49. <dict>
  50. <key>NSExceptionAllowsInsecureHTTPLoads</key>
  51. <true/>
  52. <key>NSIncludesSubdomains</key>
  53. <true/>
  54. </dict>
  55. </dict>
  56. </dict><key>BuildMachineOSBuild</key>`)
  57. )
  58. return newPath
  59. }
  60. const spawn = (cmd: string, args: string[], opts: any = {}) => {
  61. let out = ''
  62. const child = cp.spawn(cmd, args, opts)
  63. child.stdout.on('data', (chunk: Buffer) => {
  64. out += chunk.toString()
  65. })
  66. child.stderr.on('data', (chunk: Buffer) => {
  67. out += chunk.toString()
  68. })
  69. return new Promise<{ code: number, out: string }>((resolve) => {
  70. child.on('exit', (code, signal) => {
  71. expect(signal).to.equal(null)
  72. resolve({
  73. code: code!,
  74. out
  75. })
  76. })
  77. })
  78. }
  79. const signApp = (appPath: string) => {
  80. return spawn('codesign', ['-s', identity, '--deep', '--force', appPath])
  81. }
  82. const launchApp = (appPath: string, args: string[] = []) => {
  83. return spawn(path.resolve(appPath, 'Contents/MacOS/Electron'), args)
  84. }
  85. const withTempDirectory = async (fn: (dir: string) => Promise<void>) => {
  86. const dir = await fs.mkdtemp(path.resolve(os.tmpdir(), 'electron-update-spec-'))
  87. try {
  88. await fn(dir)
  89. } finally {
  90. cp.spawnSync('rm', ['-r' , dir])
  91. }
  92. }
  93. const logOnError = (what: any, fn: () => void) => {
  94. try {
  95. fn()
  96. } catch (err) {
  97. console.error(what)
  98. throw err
  99. }
  100. }
  101. it('should fail to set the feed URL when the app is not signed', async () => {
  102. await withTempDirectory(async (dir) => {
  103. const appPath = await copyApp(dir)
  104. const launchResult = await launchApp(appPath, ['http://myupdate'])
  105. expect(launchResult.code).to.equal(1)
  106. expect(launchResult.out).to.include('Could not get code signature for running application')
  107. })
  108. })
  109. it('should cleanly set the feed URL when the app is signed', async () => {
  110. await withTempDirectory(async (dir) => {
  111. const appPath = await copyApp(dir)
  112. await signApp(appPath)
  113. const launchResult = await launchApp(appPath, ['http://myupdate'])
  114. expect(launchResult.code).to.equal(0)
  115. expect(launchResult.out).to.include('Feed URL Set: http://myupdate')
  116. })
  117. })
  118. describe('with update server', () => {
  119. let port = 0;
  120. let server: express.Application = null as any;
  121. let httpServer: http.Server = null as any
  122. let requests: express.Request[] = [];
  123. beforeEach((done) => {
  124. requests = []
  125. server = express()
  126. server.use((req, res, next) => {
  127. requests.push(req)
  128. next()
  129. })
  130. httpServer = server.listen(0, '127.0.0.1', () => {
  131. port = (httpServer.address() as AddressInfo).port
  132. done()
  133. })
  134. })
  135. afterEach((done) => {
  136. if (httpServer) {
  137. httpServer.close(() => {
  138. httpServer = null as any
  139. server = null as any
  140. done()
  141. })
  142. }
  143. })
  144. it('should hit the update endpoint when checkForUpdates is called', async () => {
  145. await withTempDirectory(async (dir) => {
  146. const appPath = await copyApp(dir, 'check')
  147. await signApp(appPath)
  148. server.get('/update-check', (req, res) => {
  149. res.status(204).send()
  150. })
  151. const launchResult = await launchApp(appPath, [`http://localhost:${port}/update-check`])
  152. logOnError(launchResult, () => {
  153. expect(launchResult.code).to.equal(0)
  154. expect(requests).to.have.lengthOf(1)
  155. expect(requests[0]).to.have.property('url', '/update-check')
  156. expect(requests[0].header('user-agent')).to.include('Electron/')
  157. })
  158. })
  159. })
  160. it('should hit the download endpoint when an update is available and error if the file is bad', async () => {
  161. await withTempDirectory(async (dir) => {
  162. const appPath = await copyApp(dir, 'update')
  163. await signApp(appPath)
  164. server.get('/update-file', (req, res) => {
  165. res.status(500).send('This is not a file')
  166. })
  167. server.get('/update-check', (req, res) => {
  168. res.json({
  169. url: `http://localhost:${port}/update-file`,
  170. name: 'My Release Name',
  171. notes: 'Theses are some release notes innit',
  172. pub_date: (new Date()).toString()
  173. })
  174. })
  175. const launchResult = await launchApp(appPath, [`http://localhost:${port}/update-check`])
  176. logOnError(launchResult, () => {
  177. expect(launchResult).to.have.property('code', 1)
  178. expect(launchResult.out).to.include('Update download failed. The server sent an invalid response.')
  179. expect(requests).to.have.lengthOf(2)
  180. expect(requests[0]).to.have.property('url', '/update-check')
  181. expect(requests[1]).to.have.property('url', '/update-file')
  182. expect(requests[0].header('user-agent')).to.include('Electron/')
  183. expect(requests[1].header('user-agent')).to.include('Electron/')
  184. })
  185. })
  186. })
  187. it('should hit the download endpoint when an update is available and update successfully when the zip is provided', async () => {
  188. await withTempDirectory(async (dir) => {
  189. const appPath = await copyApp(dir, 'update')
  190. await signApp(appPath)
  191. // Prepare update
  192. await withTempDirectory(async (dir2) => {
  193. const secondAppPath = await copyApp(dir2, 'update')
  194. const appPJPath = path.resolve(secondAppPath, 'Contents', 'Resources', 'app', 'package.json')
  195. await fs.writeFile(
  196. appPJPath,
  197. (await fs.readFile(appPJPath, 'utf8')).replace('1.0.0', '2.0.0')
  198. )
  199. await signApp(secondAppPath)
  200. const updateZipPath = path.resolve(dir2, 'update.zip');
  201. await spawn('zip', ['-r', '--symlinks', updateZipPath, './'], {
  202. cwd: dir2
  203. })
  204. server.get('/update-file', (req, res) => {
  205. res.download(updateZipPath)
  206. })
  207. server.get('/update-check', (req, res) => {
  208. res.json({
  209. url: `http://localhost:${port}/update-file`,
  210. name: 'My Release Name',
  211. notes: 'Theses are some release notes innit',
  212. pub_date: (new Date()).toString()
  213. })
  214. })
  215. const relaunchPromise = new Promise((resolve, reject) => {
  216. server.get('/update-check/updated/:version', (req, res) => {
  217. res.status(204).send()
  218. resolve()
  219. })
  220. })
  221. const launchResult = await launchApp(appPath, [`http://localhost:${port}/update-check`])
  222. logOnError(launchResult, () => {
  223. expect(launchResult).to.have.property('code', 0)
  224. expect(launchResult.out).to.include('Update Downloaded')
  225. expect(requests).to.have.lengthOf(2)
  226. expect(requests[0]).to.have.property('url', '/update-check')
  227. expect(requests[1]).to.have.property('url', '/update-file')
  228. expect(requests[0].header('user-agent')).to.include('Electron/')
  229. expect(requests[1].header('user-agent')).to.include('Electron/')
  230. })
  231. await relaunchPromise
  232. expect(requests).to.have.lengthOf(3)
  233. expect(requests[2]).to.have.property('url', '/update-check/updated/2.0.0')
  234. expect(requests[2].header('user-agent')).to.include('Electron/')
  235. })
  236. })
  237. })
  238. })
  239. })