api-autoupdater-darwin-spec.ts 35 KB

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