api-autoupdater-darwin-spec.ts 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900
  1. import { expect } from 'chai';
  2. import * as cp from 'node:child_process';
  3. import * as http from 'node:http';
  4. import * as express from 'express';
  5. import * as fs from 'fs-extra';
  6. import * as os from 'node:os';
  7. import * as path from 'node:path';
  8. import * as psList from 'ps-list';
  9. import { AddressInfo } from 'node:net';
  10. import { ifdescribe, ifit } from './lib/spec-helpers';
  11. import * as uuid from 'uuid';
  12. import { autoUpdater, systemPreferences } from 'electron';
  13. const features = process._linkedBinding('electron_common_features');
  14. const fixturesPath = path.resolve(__dirname, 'fixtures');
  15. // We can only test the auto updater on darwin non-component builds
  16. ifdescribe(process.platform === 'darwin' && !(process.env.CI && process.arch === 'arm64') && !process.mas && !features.isComponentBuild())('autoUpdater behavior', function () {
  17. this.timeout(120000);
  18. let identity = '';
  19. beforeEach(function () {
  20. const result = cp.spawnSync(path.resolve(__dirname, '../script/codesign/get-trusted-identity.sh'));
  21. if (result.status !== 0 || result.stdout.toString().trim().length === 0) {
  22. // Per https://circleci.com/docs/2.0/env-vars:
  23. // CIRCLE_PR_NUMBER is only present on forked PRs
  24. if (process.env.CI && !process.env.CIRCLE_PR_NUMBER) {
  25. throw new Error('No valid signing identity available to run autoUpdater specs');
  26. }
  27. this.skip();
  28. } else {
  29. identity = result.stdout.toString().trim();
  30. }
  31. });
  32. it('should have a valid code signing identity', () => {
  33. expect(identity).to.be.a('string').with.lengthOf.at.least(1);
  34. });
  35. const copyApp = async (newDir: string, fixture = 'initial') => {
  36. const appBundlePath = path.resolve(process.execPath, '../../..');
  37. const newPath = path.resolve(newDir, 'Electron.app');
  38. cp.spawnSync('cp', ['-R', appBundlePath, path.dirname(newPath)]);
  39. const appDir = path.resolve(newPath, 'Contents/Resources/app');
  40. await fs.mkdirp(appDir);
  41. await fs.copy(path.resolve(fixturesPath, 'auto-update', fixture), appDir);
  42. const plistPath = path.resolve(newPath, 'Contents', 'Info.plist');
  43. await fs.writeFile(
  44. plistPath,
  45. (await fs.readFile(plistPath, 'utf8')).replace('<key>BuildMachineOSBuild</key>', `<key>NSAppTransportSecurity</key>
  46. <dict>
  47. <key>NSAllowsArbitraryLoads</key>
  48. <true/>
  49. <key>NSExceptionDomains</key>
  50. <dict>
  51. <key>localhost</key>
  52. <dict>
  53. <key>NSExceptionAllowsInsecureHTTPLoads</key>
  54. <true/>
  55. <key>NSIncludesSubdomains</key>
  56. <true/>
  57. </dict>
  58. </dict>
  59. </dict><key>BuildMachineOSBuild</key>`)
  60. );
  61. return newPath;
  62. };
  63. const spawn = (cmd: string, args: string[], opts: any = {}) => {
  64. let out = '';
  65. const child = cp.spawn(cmd, args, opts);
  66. child.stdout.on('data', (chunk: Buffer) => {
  67. out += chunk.toString();
  68. });
  69. child.stderr.on('data', (chunk: Buffer) => {
  70. out += chunk.toString();
  71. });
  72. return new Promise<{ code: number, out: string }>((resolve) => {
  73. child.on('exit', (code, signal) => {
  74. expect(signal).to.equal(null);
  75. resolve({
  76. code: code!,
  77. out
  78. });
  79. });
  80. });
  81. };
  82. const signApp = (appPath: string) => {
  83. return spawn('codesign', ['-s', identity, '--deep', '--force', appPath]);
  84. };
  85. const launchApp = (appPath: string, args: string[] = []) => {
  86. return spawn(path.resolve(appPath, 'Contents/MacOS/Electron'), args);
  87. };
  88. const spawnAppWithHandle = (appPath: string, args: string[] = []) => {
  89. return cp.spawn(path.resolve(appPath, 'Contents/MacOS/Electron'), args);
  90. };
  91. const getRunningShipIts = async (appPath: string) => {
  92. const processes = await psList();
  93. const activeShipIts = processes.filter(p => p.cmd?.includes('Squirrel.framework/Resources/ShipIt com.github.Electron.ShipIt') && p.cmd!.startsWith(appPath));
  94. return activeShipIts;
  95. };
  96. const withTempDirectory = async (fn: (dir: string) => Promise<void>, autoCleanUp = true) => {
  97. const dir = await fs.mkdtemp(path.resolve(os.tmpdir(), 'electron-update-spec-'));
  98. try {
  99. await fn(dir);
  100. } finally {
  101. if (autoCleanUp) {
  102. cp.spawnSync('rm', ['-r', dir]);
  103. }
  104. }
  105. };
  106. const logOnError = (what: any, fn: () => void) => {
  107. try {
  108. fn();
  109. } catch (err) {
  110. console.error(what);
  111. throw err;
  112. }
  113. };
  114. const cachedZips: Record<string, string> = {};
  115. type Mutation = {
  116. mutate: (appPath: string) => Promise<void>,
  117. mutationKey: string,
  118. };
  119. const getOrCreateUpdateZipPath = async (version: string, fixture: string, mutateAppPreSign?: Mutation, mutateAppPostSign?: Mutation) => {
  120. const key = `${version}-${fixture}-${mutateAppPreSign?.mutationKey || 'no-pre-mutation'}-${mutateAppPostSign?.mutationKey || 'no-post-mutation'}`;
  121. if (!cachedZips[key]) {
  122. let updateZipPath: string;
  123. await withTempDirectory(async (dir) => {
  124. const secondAppPath = await copyApp(dir, fixture);
  125. const appPJPath = path.resolve(secondAppPath, 'Contents', 'Resources', 'app', 'package.json');
  126. await fs.writeFile(
  127. appPJPath,
  128. (await fs.readFile(appPJPath, 'utf8')).replace('1.0.0', version)
  129. );
  130. const infoPath = path.resolve(secondAppPath, 'Contents', 'Info.plist');
  131. await fs.writeFile(
  132. infoPath,
  133. (await fs.readFile(infoPath, 'utf8')).replace(/(<key>CFBundleShortVersionString<\/key>\s+<string>)[^<]+/g, `$1${version}`)
  134. );
  135. await mutateAppPreSign?.mutate(secondAppPath);
  136. await signApp(secondAppPath);
  137. await mutateAppPostSign?.mutate(secondAppPath);
  138. updateZipPath = path.resolve(dir, 'update.zip');
  139. await spawn('zip', ['-0', '-r', '--symlinks', updateZipPath, './'], {
  140. cwd: dir
  141. });
  142. }, false);
  143. cachedZips[key] = updateZipPath!;
  144. }
  145. return cachedZips[key];
  146. };
  147. after(() => {
  148. for (const version of Object.keys(cachedZips)) {
  149. cp.spawnSync('rm', ['-r', path.dirname(cachedZips[version])]);
  150. }
  151. });
  152. // On arm64 builds the built app is self-signed by default so the setFeedURL call always works
  153. ifit(process.arch !== 'arm64')('should fail to set the feed URL when the app is not signed', async () => {
  154. await withTempDirectory(async (dir) => {
  155. const appPath = await copyApp(dir);
  156. const launchResult = await launchApp(appPath, ['http://myupdate']);
  157. console.log(launchResult);
  158. expect(launchResult.code).to.equal(1);
  159. expect(launchResult.out).to.include('Could not get code signature for running application');
  160. });
  161. });
  162. it('should cleanly set the feed URL when the app is signed', async () => {
  163. await withTempDirectory(async (dir) => {
  164. const appPath = await copyApp(dir);
  165. await signApp(appPath);
  166. const launchResult = await launchApp(appPath, ['http://myupdate']);
  167. expect(launchResult.code).to.equal(0);
  168. expect(launchResult.out).to.include('Feed URL Set: http://myupdate');
  169. });
  170. });
  171. describe('with update server', () => {
  172. let port = 0;
  173. let server: express.Application = null as any;
  174. let httpServer: http.Server = null as any;
  175. let requests: express.Request[] = [];
  176. beforeEach((done) => {
  177. requests = [];
  178. server = express();
  179. server.use((req, res, next) => {
  180. requests.push(req);
  181. next();
  182. });
  183. httpServer = server.listen(0, '127.0.0.1', () => {
  184. port = (httpServer.address() as AddressInfo).port;
  185. done();
  186. });
  187. });
  188. afterEach(async () => {
  189. if (httpServer) {
  190. await new Promise<void>(resolve => {
  191. httpServer.close(() => {
  192. httpServer = null as any;
  193. server = null as any;
  194. resolve();
  195. });
  196. });
  197. }
  198. });
  199. it('should hit the update endpoint when checkForUpdates is called', async () => {
  200. await withTempDirectory(async (dir) => {
  201. const appPath = await copyApp(dir, 'check');
  202. await signApp(appPath);
  203. server.get('/update-check', (req, res) => {
  204. res.status(204).send();
  205. });
  206. const launchResult = await launchApp(appPath, [`http://localhost:${port}/update-check`]);
  207. logOnError(launchResult, () => {
  208. expect(launchResult.code).to.equal(0);
  209. expect(requests).to.have.lengthOf(1);
  210. expect(requests[0]).to.have.property('url', '/update-check');
  211. expect(requests[0].header('user-agent')).to.include('Electron/');
  212. });
  213. });
  214. });
  215. it('should hit the update endpoint with customer headers when checkForUpdates is called', async () => {
  216. await withTempDirectory(async (dir) => {
  217. const appPath = await copyApp(dir, 'check-with-headers');
  218. await signApp(appPath);
  219. server.get('/update-check', (req, res) => {
  220. res.status(204).send();
  221. });
  222. const launchResult = await launchApp(appPath, [`http://localhost:${port}/update-check`]);
  223. logOnError(launchResult, () => {
  224. expect(launchResult.code).to.equal(0);
  225. expect(requests).to.have.lengthOf(1);
  226. expect(requests[0]).to.have.property('url', '/update-check');
  227. expect(requests[0].header('x-test')).to.equal('this-is-a-test');
  228. });
  229. });
  230. });
  231. it('should hit the download endpoint when an update is available and error if the file is bad', async () => {
  232. await withTempDirectory(async (dir) => {
  233. const appPath = await copyApp(dir, 'update');
  234. await signApp(appPath);
  235. server.get('/update-file', (req, res) => {
  236. res.status(500).send('This is not a file');
  237. });
  238. server.get('/update-check', (req, res) => {
  239. res.json({
  240. url: `http://localhost:${port}/update-file`,
  241. name: 'My Release Name',
  242. notes: 'Theses are some release notes innit',
  243. pub_date: (new Date()).toString()
  244. });
  245. });
  246. const launchResult = await launchApp(appPath, [`http://localhost:${port}/update-check`]);
  247. logOnError(launchResult, () => {
  248. expect(launchResult).to.have.property('code', 1);
  249. expect(launchResult.out).to.include('Update download failed. The server sent an invalid response.');
  250. expect(requests).to.have.lengthOf(2);
  251. expect(requests[0]).to.have.property('url', '/update-check');
  252. expect(requests[1]).to.have.property('url', '/update-file');
  253. expect(requests[0].header('user-agent')).to.include('Electron/');
  254. expect(requests[1].header('user-agent')).to.include('Electron/');
  255. });
  256. });
  257. });
  258. const withUpdatableApp = async (opts: {
  259. nextVersion: string;
  260. startFixture: string;
  261. endFixture: string;
  262. mutateAppPreSign?: Mutation;
  263. mutateAppPostSign?: Mutation;
  264. }, fn: (appPath: string, zipPath: string) => Promise<void>) => {
  265. await withTempDirectory(async (dir) => {
  266. const appPath = await copyApp(dir, opts.startFixture);
  267. await opts.mutateAppPreSign?.mutate(appPath);
  268. const infoPath = path.resolve(appPath, 'Contents', 'Info.plist');
  269. await fs.writeFile(
  270. infoPath,
  271. (await fs.readFile(infoPath, 'utf8')).replace(/(<key>CFBundleShortVersionString<\/key>\s+<string>)[^<]+/g, '$11.0.0')
  272. );
  273. await signApp(appPath);
  274. const updateZipPath = await getOrCreateUpdateZipPath(opts.nextVersion, opts.endFixture, opts.mutateAppPreSign, opts.mutateAppPostSign);
  275. await fn(appPath, updateZipPath);
  276. });
  277. };
  278. it('should hit the download endpoint when an update is available and update successfully when the zip is provided', async () => {
  279. await withUpdatableApp({
  280. nextVersion: '2.0.0',
  281. startFixture: 'update',
  282. endFixture: 'update'
  283. }, async (appPath, updateZipPath) => {
  284. server.get('/update-file', (req, res) => {
  285. res.download(updateZipPath);
  286. });
  287. server.get('/update-check', (req, res) => {
  288. res.json({
  289. url: `http://localhost:${port}/update-file`,
  290. name: 'My Release Name',
  291. notes: 'Theses are some release notes innit',
  292. pub_date: (new Date()).toString()
  293. });
  294. });
  295. const relaunchPromise = new Promise<void>((resolve) => {
  296. server.get('/update-check/updated/:version', (req, res) => {
  297. res.status(204).send();
  298. resolve();
  299. });
  300. });
  301. const launchResult = await launchApp(appPath, [`http://localhost:${port}/update-check`]);
  302. logOnError(launchResult, () => {
  303. expect(launchResult).to.have.property('code', 0);
  304. expect(launchResult.out).to.include('Update Downloaded');
  305. expect(requests).to.have.lengthOf(2);
  306. expect(requests[0]).to.have.property('url', '/update-check');
  307. expect(requests[1]).to.have.property('url', '/update-file');
  308. expect(requests[0].header('user-agent')).to.include('Electron/');
  309. expect(requests[1].header('user-agent')).to.include('Electron/');
  310. });
  311. await relaunchPromise;
  312. expect(requests).to.have.lengthOf(3);
  313. expect(requests[2].url).to.equal('/update-check/updated/2.0.0');
  314. expect(requests[2].header('user-agent')).to.include('Electron/');
  315. });
  316. });
  317. it('should hit the download endpoint when an update is available and update successfully when the zip is provided even after a different update was staged', async () => {
  318. await withUpdatableApp({
  319. nextVersion: '2.0.0',
  320. startFixture: 'update-stack',
  321. endFixture: 'update-stack'
  322. }, async (appPath, updateZipPath2) => {
  323. await withUpdatableApp({
  324. nextVersion: '3.0.0',
  325. startFixture: 'update-stack',
  326. endFixture: 'update-stack'
  327. }, async (_, updateZipPath3) => {
  328. let updateCount = 0;
  329. server.get('/update-file', (req, res) => {
  330. res.download(updateCount > 1 ? updateZipPath3 : updateZipPath2);
  331. });
  332. server.get('/update-check', (req, res) => {
  333. updateCount++;
  334. res.json({
  335. url: `http://localhost:${port}/update-file`,
  336. name: 'My Release Name',
  337. notes: 'Theses are some release notes innit',
  338. pub_date: (new Date()).toString()
  339. });
  340. });
  341. const relaunchPromise = new Promise<void>((resolve) => {
  342. server.get('/update-check/updated/:version', (req, res) => {
  343. res.status(204).send();
  344. resolve();
  345. });
  346. });
  347. const launchResult = await launchApp(appPath, [`http://localhost:${port}/update-check`]);
  348. logOnError(launchResult, () => {
  349. expect(launchResult).to.have.property('code', 0);
  350. expect(launchResult.out).to.include('Update Downloaded');
  351. expect(requests).to.have.lengthOf(4);
  352. expect(requests[0]).to.have.property('url', '/update-check');
  353. expect(requests[1]).to.have.property('url', '/update-file');
  354. expect(requests[0].header('user-agent')).to.include('Electron/');
  355. expect(requests[1].header('user-agent')).to.include('Electron/');
  356. expect(requests[2]).to.have.property('url', '/update-check');
  357. expect(requests[3]).to.have.property('url', '/update-file');
  358. expect(requests[2].header('user-agent')).to.include('Electron/');
  359. expect(requests[3].header('user-agent')).to.include('Electron/');
  360. });
  361. await relaunchPromise;
  362. expect(requests).to.have.lengthOf(5);
  363. expect(requests[4].url).to.equal('/update-check/updated/3.0.0');
  364. expect(requests[4].header('user-agent')).to.include('Electron/');
  365. });
  366. });
  367. });
  368. it('should update to lower version numbers', async () => {
  369. await withUpdatableApp({
  370. nextVersion: '0.0.1',
  371. startFixture: 'update',
  372. endFixture: 'update'
  373. }, async (appPath, updateZipPath) => {
  374. server.get('/update-file', (req, res) => {
  375. res.download(updateZipPath);
  376. });
  377. server.get('/update-check', (req, res) => {
  378. res.json({
  379. url: `http://localhost:${port}/update-file`,
  380. name: 'My Release Name',
  381. notes: 'Theses are some release notes innit',
  382. pub_date: (new Date()).toString()
  383. });
  384. });
  385. const relaunchPromise = new Promise<void>((resolve) => {
  386. server.get('/update-check/updated/:version', (req, res) => {
  387. res.status(204).send();
  388. resolve();
  389. });
  390. });
  391. const launchResult = await launchApp(appPath, [`http://localhost:${port}/update-check`]);
  392. logOnError(launchResult, () => {
  393. expect(launchResult).to.have.property('code', 0);
  394. expect(launchResult.out).to.include('Update Downloaded');
  395. expect(requests).to.have.lengthOf(2);
  396. expect(requests[0]).to.have.property('url', '/update-check');
  397. expect(requests[1]).to.have.property('url', '/update-file');
  398. expect(requests[0].header('user-agent')).to.include('Electron/');
  399. expect(requests[1].header('user-agent')).to.include('Electron/');
  400. });
  401. await relaunchPromise;
  402. expect(requests).to.have.lengthOf(3);
  403. expect(requests[2].url).to.equal('/update-check/updated/0.0.1');
  404. expect(requests[2].header('user-agent')).to.include('Electron/');
  405. });
  406. });
  407. describe('with ElectronSquirrelPreventDowngrades enabled', () => {
  408. it('should not update to lower version numbers', async () => {
  409. await withUpdatableApp({
  410. nextVersion: '0.0.1',
  411. startFixture: 'update',
  412. endFixture: 'update',
  413. mutateAppPreSign: {
  414. mutationKey: 'prevent-downgrades',
  415. mutate: async (appPath) => {
  416. const infoPath = path.resolve(appPath, 'Contents', 'Info.plist');
  417. await fs.writeFile(
  418. infoPath,
  419. (await fs.readFile(infoPath, 'utf8')).replace('<key>NSSupportsAutomaticGraphicsSwitching</key>', '<key>ElectronSquirrelPreventDowngrades</key><true/><key>NSSupportsAutomaticGraphicsSwitching</key>')
  420. );
  421. }
  422. }
  423. }, async (appPath, updateZipPath) => {
  424. server.get('/update-file', (req, res) => {
  425. res.download(updateZipPath);
  426. });
  427. server.get('/update-check', (req, res) => {
  428. res.json({
  429. url: `http://localhost:${port}/update-file`,
  430. name: 'My Release Name',
  431. notes: 'Theses are some release notes innit',
  432. pub_date: (new Date()).toString()
  433. });
  434. });
  435. const launchResult = await launchApp(appPath, [`http://localhost:${port}/update-check`]);
  436. logOnError(launchResult, () => {
  437. expect(launchResult).to.have.property('code', 1);
  438. expect(launchResult.out).to.include('Cannot update to a bundle with a lower version number');
  439. expect(requests).to.have.lengthOf(2);
  440. expect(requests[0]).to.have.property('url', '/update-check');
  441. expect(requests[1]).to.have.property('url', '/update-file');
  442. expect(requests[0].header('user-agent')).to.include('Electron/');
  443. expect(requests[1].header('user-agent')).to.include('Electron/');
  444. });
  445. });
  446. });
  447. it('should not update to version strings that are not simple Major.Minor.Patch', async () => {
  448. await withUpdatableApp({
  449. nextVersion: '2.0.0-bad',
  450. startFixture: 'update',
  451. endFixture: 'update',
  452. mutateAppPreSign: {
  453. mutationKey: 'prevent-downgrades',
  454. mutate: async (appPath) => {
  455. const infoPath = path.resolve(appPath, 'Contents', 'Info.plist');
  456. await fs.writeFile(
  457. infoPath,
  458. (await fs.readFile(infoPath, 'utf8')).replace('<key>NSSupportsAutomaticGraphicsSwitching</key>', '<key>ElectronSquirrelPreventDowngrades</key><true/><key>NSSupportsAutomaticGraphicsSwitching</key>')
  459. );
  460. }
  461. }
  462. }, async (appPath, updateZipPath) => {
  463. server.get('/update-file', (req, res) => {
  464. res.download(updateZipPath);
  465. });
  466. server.get('/update-check', (req, res) => {
  467. res.json({
  468. url: `http://localhost:${port}/update-file`,
  469. name: 'My Release Name',
  470. notes: 'Theses are some release notes innit',
  471. pub_date: (new Date()).toString()
  472. });
  473. });
  474. const launchResult = await launchApp(appPath, [`http://localhost:${port}/update-check`]);
  475. logOnError(launchResult, () => {
  476. expect(launchResult).to.have.property('code', 1);
  477. expect(launchResult.out).to.include('Cannot update to a bundle with a lower version number');
  478. expect(requests).to.have.lengthOf(2);
  479. expect(requests[0]).to.have.property('url', '/update-check');
  480. expect(requests[1]).to.have.property('url', '/update-file');
  481. expect(requests[0].header('user-agent')).to.include('Electron/');
  482. expect(requests[1].header('user-agent')).to.include('Electron/');
  483. });
  484. });
  485. });
  486. it('should still update to higher version numbers', async () => {
  487. await withUpdatableApp({
  488. nextVersion: '1.0.1',
  489. startFixture: 'update',
  490. endFixture: 'update'
  491. }, async (appPath, updateZipPath) => {
  492. server.get('/update-file', (req, res) => {
  493. res.download(updateZipPath);
  494. });
  495. server.get('/update-check', (req, res) => {
  496. res.json({
  497. url: `http://localhost:${port}/update-file`,
  498. name: 'My Release Name',
  499. notes: 'Theses are some release notes innit',
  500. pub_date: (new Date()).toString()
  501. });
  502. });
  503. const relaunchPromise = new Promise<void>((resolve) => {
  504. server.get('/update-check/updated/:version', (req, res) => {
  505. res.status(204).send();
  506. resolve();
  507. });
  508. });
  509. const launchResult = await launchApp(appPath, [`http://localhost:${port}/update-check`]);
  510. logOnError(launchResult, () => {
  511. expect(launchResult).to.have.property('code', 0);
  512. expect(launchResult.out).to.include('Update Downloaded');
  513. expect(requests).to.have.lengthOf(2);
  514. expect(requests[0]).to.have.property('url', '/update-check');
  515. expect(requests[1]).to.have.property('url', '/update-file');
  516. expect(requests[0].header('user-agent')).to.include('Electron/');
  517. expect(requests[1].header('user-agent')).to.include('Electron/');
  518. });
  519. await relaunchPromise;
  520. expect(requests).to.have.lengthOf(3);
  521. expect(requests[2].url).to.equal('/update-check/updated/1.0.1');
  522. expect(requests[2].header('user-agent')).to.include('Electron/');
  523. });
  524. });
  525. it('should compare version numbers correctly', () => {
  526. expect(autoUpdater.isVersionAllowedForUpdate!('1.0.0', '2.0.0')).to.equal(true);
  527. expect(autoUpdater.isVersionAllowedForUpdate!('1.0.1', '1.0.10')).to.equal(true);
  528. expect(autoUpdater.isVersionAllowedForUpdate!('1.0.10', '1.0.1')).to.equal(false);
  529. expect(autoUpdater.isVersionAllowedForUpdate!('1.31.1', '1.32.0')).to.equal(true);
  530. expect(autoUpdater.isVersionAllowedForUpdate!('1.31.1', '0.32.0')).to.equal(false);
  531. });
  532. });
  533. it('should abort the update if the application is still running when ShipIt kicks off', async () => {
  534. await withUpdatableApp({
  535. nextVersion: '2.0.0',
  536. startFixture: 'update',
  537. endFixture: 'update'
  538. }, async (appPath, updateZipPath) => {
  539. server.get('/update-file', (req, res) => {
  540. res.download(updateZipPath);
  541. });
  542. server.get('/update-check', (req, res) => {
  543. res.json({
  544. url: `http://localhost:${port}/update-file`,
  545. name: 'My Release Name',
  546. notes: 'Theses are some release notes innit',
  547. pub_date: (new Date()).toString()
  548. });
  549. });
  550. enum FlipFlop {
  551. INITIAL,
  552. FLIPPED,
  553. FLOPPED,
  554. }
  555. const shipItFlipFlopPromise = new Promise<void>((resolve) => {
  556. let state = FlipFlop.INITIAL;
  557. const checker = setInterval(async () => {
  558. const running = await getRunningShipIts(appPath);
  559. switch (state) {
  560. case FlipFlop.INITIAL: {
  561. if (running.length) state = FlipFlop.FLIPPED;
  562. break;
  563. }
  564. case FlipFlop.FLIPPED: {
  565. if (!running.length) state = FlipFlop.FLOPPED;
  566. break;
  567. }
  568. }
  569. if (state === FlipFlop.FLOPPED) {
  570. clearInterval(checker);
  571. resolve();
  572. }
  573. }, 500);
  574. });
  575. const launchResult = await launchApp(appPath, [`http://localhost:${port}/update-check`]);
  576. const retainerHandle = spawnAppWithHandle(appPath, ['remain-open']);
  577. logOnError(launchResult, () => {
  578. expect(launchResult).to.have.property('code', 0);
  579. expect(launchResult.out).to.include('Update Downloaded');
  580. expect(requests).to.have.lengthOf(2);
  581. expect(requests[0]).to.have.property('url', '/update-check');
  582. expect(requests[1]).to.have.property('url', '/update-file');
  583. expect(requests[0].header('user-agent')).to.include('Electron/');
  584. expect(requests[1].header('user-agent')).to.include('Electron/');
  585. });
  586. await shipItFlipFlopPromise;
  587. expect(requests).to.have.lengthOf(2, 'should not have relaunched the updated app');
  588. expect(JSON.parse(await fs.readFile(path.resolve(appPath, 'Contents/Resources/app/package.json'), 'utf8')).version).to.equal('1.0.0', 'should still be the old version on disk');
  589. retainerHandle.kill('SIGINT');
  590. });
  591. });
  592. describe('with SquirrelMacEnableDirectContentsWrite enabled', () => {
  593. let previousValue: any;
  594. beforeEach(() => {
  595. previousValue = systemPreferences.getUserDefault('SquirrelMacEnableDirectContentsWrite', 'boolean');
  596. systemPreferences.setUserDefault('SquirrelMacEnableDirectContentsWrite', 'boolean', true as any);
  597. });
  598. afterEach(() => {
  599. systemPreferences.setUserDefault('SquirrelMacEnableDirectContentsWrite', 'boolean', previousValue as any);
  600. });
  601. it('should hit the download endpoint when an update is available and update successfully when the zip is provided leaving the parent directory untouched', async () => {
  602. await withUpdatableApp({
  603. nextVersion: '2.0.0',
  604. startFixture: 'update',
  605. endFixture: 'update'
  606. }, async (appPath, updateZipPath) => {
  607. const randomID = uuid.v4();
  608. cp.spawnSync('xattr', ['-w', 'spec-id', randomID, appPath]);
  609. server.get('/update-file', (req, res) => {
  610. res.download(updateZipPath);
  611. });
  612. server.get('/update-check', (req, res) => {
  613. res.json({
  614. url: `http://localhost:${port}/update-file`,
  615. name: 'My Release Name',
  616. notes: 'Theses are some release notes innit',
  617. pub_date: (new Date()).toString()
  618. });
  619. });
  620. const relaunchPromise = new Promise<void>((resolve) => {
  621. server.get('/update-check/updated/:version', (req, res) => {
  622. res.status(204).send();
  623. resolve();
  624. });
  625. });
  626. const launchResult = await launchApp(appPath, [`http://localhost:${port}/update-check`]);
  627. logOnError(launchResult, () => {
  628. expect(launchResult).to.have.property('code', 0);
  629. expect(launchResult.out).to.include('Update Downloaded');
  630. expect(requests).to.have.lengthOf(2);
  631. expect(requests[0]).to.have.property('url', '/update-check');
  632. expect(requests[1]).to.have.property('url', '/update-file');
  633. expect(requests[0].header('user-agent')).to.include('Electron/');
  634. expect(requests[1].header('user-agent')).to.include('Electron/');
  635. });
  636. await relaunchPromise;
  637. expect(requests).to.have.lengthOf(3);
  638. expect(requests[2].url).to.equal('/update-check/updated/2.0.0');
  639. expect(requests[2].header('user-agent')).to.include('Electron/');
  640. const result = cp.spawnSync('xattr', ['-l', appPath]);
  641. expect(result.stdout.toString()).to.include(`spec-id: ${randomID}`);
  642. });
  643. });
  644. });
  645. it('should hit the download endpoint when an update is available and fail when the zip signature is invalid', async () => {
  646. await withUpdatableApp({
  647. nextVersion: '2.0.0',
  648. startFixture: 'update',
  649. endFixture: 'update',
  650. mutateAppPostSign: {
  651. mutationKey: 'add-resource',
  652. mutate: async (appPath) => {
  653. const resourcesPath = path.resolve(appPath, 'Contents', 'Resources', 'app', 'injected.txt');
  654. await fs.writeFile(resourcesPath, 'demo');
  655. }
  656. }
  657. }, async (appPath, updateZipPath) => {
  658. server.get('/update-file', (req, res) => {
  659. res.download(updateZipPath);
  660. });
  661. server.get('/update-check', (req, res) => {
  662. res.json({
  663. url: `http://localhost:${port}/update-file`,
  664. name: 'My Release Name',
  665. notes: 'Theses are some release notes innit',
  666. pub_date: (new Date()).toString()
  667. });
  668. });
  669. const launchResult = await launchApp(appPath, [`http://localhost:${port}/update-check`]);
  670. logOnError(launchResult, () => {
  671. expect(launchResult).to.have.property('code', 1);
  672. expect(launchResult.out).to.include('Code signature at URL');
  673. expect(launchResult.out).to.include('a sealed resource is missing or invalid');
  674. expect(requests).to.have.lengthOf(2);
  675. expect(requests[0]).to.have.property('url', '/update-check');
  676. expect(requests[1]).to.have.property('url', '/update-file');
  677. expect(requests[0].header('user-agent')).to.include('Electron/');
  678. expect(requests[1].header('user-agent')).to.include('Electron/');
  679. });
  680. });
  681. });
  682. it('should hit the download endpoint when an update is available and fail when the ShipIt binary is a symlink', async () => {
  683. await withUpdatableApp({
  684. nextVersion: '2.0.0',
  685. startFixture: 'update',
  686. endFixture: 'update',
  687. mutateAppPostSign: {
  688. mutationKey: 'modify-shipit',
  689. mutate: async (appPath) => {
  690. const shipItPath = path.resolve(appPath, 'Contents', 'Frameworks', 'Squirrel.framework', 'Resources', 'ShipIt');
  691. await fs.remove(shipItPath);
  692. await fs.symlink('/tmp/ShipIt', shipItPath, 'file');
  693. }
  694. }
  695. }, async (appPath, updateZipPath) => {
  696. server.get('/update-file', (req, res) => {
  697. res.download(updateZipPath);
  698. });
  699. server.get('/update-check', (req, res) => {
  700. res.json({
  701. url: `http://localhost:${port}/update-file`,
  702. name: 'My Release Name',
  703. notes: 'Theses are some release notes innit',
  704. pub_date: (new Date()).toString()
  705. });
  706. });
  707. const launchResult = await launchApp(appPath, [`http://localhost:${port}/update-check`]);
  708. logOnError(launchResult, () => {
  709. expect(launchResult).to.have.property('code', 1);
  710. expect(launchResult.out).to.include('Code signature at URL');
  711. expect(launchResult.out).to.include('a sealed resource is missing or invalid');
  712. expect(requests).to.have.lengthOf(2);
  713. expect(requests[0]).to.have.property('url', '/update-check');
  714. expect(requests[1]).to.have.property('url', '/update-file');
  715. expect(requests[0].header('user-agent')).to.include('Electron/');
  716. expect(requests[1].header('user-agent')).to.include('Electron/');
  717. });
  718. });
  719. });
  720. it('should hit the download endpoint when an update is available and fail when the Electron Framework is modified', async () => {
  721. await withUpdatableApp({
  722. nextVersion: '2.0.0',
  723. startFixture: 'update',
  724. endFixture: 'update',
  725. mutateAppPostSign: {
  726. mutationKey: 'modify-eframework',
  727. mutate: async (appPath) => {
  728. const shipItPath = path.resolve(appPath, 'Contents', 'Frameworks', 'Electron Framework.framework', 'Electron Framework');
  729. await fs.appendFile(shipItPath, Buffer.from('123'));
  730. }
  731. }
  732. }, async (appPath, updateZipPath) => {
  733. server.get('/update-file', (req, res) => {
  734. res.download(updateZipPath);
  735. });
  736. server.get('/update-check', (req, res) => {
  737. res.json({
  738. url: `http://localhost:${port}/update-file`,
  739. name: 'My Release Name',
  740. notes: 'Theses are some release notes innit',
  741. pub_date: (new Date()).toString()
  742. });
  743. });
  744. const launchResult = await launchApp(appPath, [`http://localhost:${port}/update-check`]);
  745. logOnError(launchResult, () => {
  746. expect(launchResult).to.have.property('code', 1);
  747. expect(launchResult.out).to.include('Code signature at URL');
  748. expect(launchResult.out).to.include(' main executable failed strict validation');
  749. expect(requests).to.have.lengthOf(2);
  750. expect(requests[0]).to.have.property('url', '/update-check');
  751. expect(requests[1]).to.have.property('url', '/update-file');
  752. expect(requests[0].header('user-agent')).to.include('Electron/');
  753. expect(requests[1].header('user-agent')).to.include('Electron/');
  754. });
  755. });
  756. });
  757. it('should hit the download endpoint when an update is available and update successfully when the zip is provided with JSON update mode', async () => {
  758. await withUpdatableApp({
  759. nextVersion: '2.0.0',
  760. startFixture: 'update-json',
  761. endFixture: 'update-json'
  762. }, async (appPath, updateZipPath) => {
  763. server.get('/update-file', (req, res) => {
  764. res.download(updateZipPath);
  765. });
  766. server.get('/update-check', (req, res) => {
  767. res.json({
  768. currentRelease: '2.0.0',
  769. releases: [
  770. {
  771. version: '2.0.0',
  772. updateTo: {
  773. version: '2.0.0',
  774. url: `http://localhost:${port}/update-file`,
  775. name: 'My Release Name',
  776. notes: 'Theses are some release notes innit',
  777. pub_date: (new Date()).toString()
  778. }
  779. }
  780. ]
  781. });
  782. });
  783. const relaunchPromise = new Promise<void>((resolve) => {
  784. server.get('/update-check/updated/:version', (req, res) => {
  785. res.status(204).send();
  786. resolve();
  787. });
  788. });
  789. const launchResult = await launchApp(appPath, [`http://localhost:${port}/update-check`]);
  790. logOnError(launchResult, () => {
  791. expect(launchResult).to.have.property('code', 0);
  792. expect(launchResult.out).to.include('Update Downloaded');
  793. expect(requests).to.have.lengthOf(2);
  794. expect(requests[0]).to.have.property('url', '/update-check');
  795. expect(requests[1]).to.have.property('url', '/update-file');
  796. expect(requests[0].header('user-agent')).to.include('Electron/');
  797. expect(requests[1].header('user-agent')).to.include('Electron/');
  798. });
  799. await relaunchPromise;
  800. expect(requests).to.have.lengthOf(3);
  801. expect(requests[2]).to.have.property('url', '/update-check/updated/2.0.0');
  802. expect(requests[2].header('user-agent')).to.include('Electron/');
  803. });
  804. });
  805. it('should hit the download endpoint when an update is available and not update in JSON update mode when the currentRelease is older than the current version', async () => {
  806. await withUpdatableApp({
  807. nextVersion: '0.1.0',
  808. startFixture: 'update-json',
  809. endFixture: 'update-json'
  810. }, async (appPath, updateZipPath) => {
  811. server.get('/update-file', (req, res) => {
  812. res.download(updateZipPath);
  813. });
  814. server.get('/update-check', (req, res) => {
  815. res.json({
  816. currentRelease: '0.1.0',
  817. releases: [
  818. {
  819. version: '0.1.0',
  820. updateTo: {
  821. version: '0.1.0',
  822. url: `http://localhost:${port}/update-file`,
  823. name: 'My Release Name',
  824. notes: 'Theses are some release notes innit',
  825. pub_date: (new Date()).toString()
  826. }
  827. }
  828. ]
  829. });
  830. });
  831. const launchResult = await launchApp(appPath, [`http://localhost:${port}/update-check`]);
  832. logOnError(launchResult, () => {
  833. expect(launchResult).to.have.property('code', 1);
  834. expect(launchResult.out).to.include('No update available');
  835. expect(requests).to.have.lengthOf(1);
  836. expect(requests[0]).to.have.property('url', '/update-check');
  837. expect(requests[0].header('user-agent')).to.include('Electron/');
  838. });
  839. });
  840. });
  841. });
  842. });