api-autoupdater-darwin-spec.ts 35 KB

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