api-autoupdater-darwin-spec.ts 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663
  1. import { expect } from 'chai';
  2. import * as cp from 'child_process';
  3. import * as http from 'http';
  4. import * as express from 'express';
  5. import * as fs from 'fs-extra';
  6. import * as os from 'os';
  7. import * as path from 'path';
  8. import * as psList from 'ps-list';
  9. import { AddressInfo } from 'net';
  10. import { ifdescribe, ifit } from './lib/spec-helpers';
  11. import * as uuid from 'uuid';
  12. import { 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. const getOrCreateUpdateZipPath = async (version: string, fixture: string, mutateAppPostSign?: {
  116. mutate: (appPath: string) => Promise<void>,
  117. mutationKey: string,
  118. }) => {
  119. const key = `${version}-${fixture}-${mutateAppPostSign?.mutationKey || 'no-mutation'}`;
  120. if (!cachedZips[key]) {
  121. let updateZipPath: string;
  122. await withTempDirectory(async (dir) => {
  123. const secondAppPath = await copyApp(dir, fixture);
  124. const appPJPath = path.resolve(secondAppPath, 'Contents', 'Resources', 'app', 'package.json');
  125. await fs.writeFile(
  126. appPJPath,
  127. (await fs.readFile(appPJPath, 'utf8')).replace('1.0.0', version)
  128. );
  129. await signApp(secondAppPath);
  130. await mutateAppPostSign?.mutate(secondAppPath);
  131. updateZipPath = path.resolve(dir, 'update.zip');
  132. await spawn('zip', ['-0', '-r', '--symlinks', updateZipPath, './'], {
  133. cwd: dir
  134. });
  135. }, false);
  136. cachedZips[key] = updateZipPath!;
  137. }
  138. return cachedZips[key];
  139. };
  140. after(() => {
  141. for (const version of Object.keys(cachedZips)) {
  142. cp.spawnSync('rm', ['-r', path.dirname(cachedZips[version])]);
  143. }
  144. });
  145. // On arm64 builds the built app is self-signed by default so the setFeedURL call always works
  146. ifit(process.arch !== 'arm64')('should fail to set the feed URL when the app is not signed', async () => {
  147. await withTempDirectory(async (dir) => {
  148. const appPath = await copyApp(dir);
  149. const launchResult = await launchApp(appPath, ['http://myupdate']);
  150. console.log(launchResult);
  151. expect(launchResult.code).to.equal(1);
  152. expect(launchResult.out).to.include('Could not get code signature for running application');
  153. });
  154. });
  155. it('should cleanly set the feed URL when the app is signed', async () => {
  156. await withTempDirectory(async (dir) => {
  157. const appPath = await copyApp(dir);
  158. await signApp(appPath);
  159. const launchResult = await launchApp(appPath, ['http://myupdate']);
  160. expect(launchResult.code).to.equal(0);
  161. expect(launchResult.out).to.include('Feed URL Set: http://myupdate');
  162. });
  163. });
  164. describe('with update server', () => {
  165. let port = 0;
  166. let server: express.Application = null as any;
  167. let httpServer: http.Server = null as any;
  168. let requests: express.Request[] = [];
  169. beforeEach((done) => {
  170. requests = [];
  171. server = express();
  172. server.use((req, res, next) => {
  173. requests.push(req);
  174. next();
  175. });
  176. httpServer = server.listen(0, '127.0.0.1', () => {
  177. port = (httpServer.address() as AddressInfo).port;
  178. done();
  179. });
  180. });
  181. afterEach(async () => {
  182. if (httpServer) {
  183. await new Promise<void>(resolve => {
  184. httpServer.close(() => {
  185. httpServer = null as any;
  186. server = null as any;
  187. resolve();
  188. });
  189. });
  190. }
  191. });
  192. it('should hit the update endpoint when checkForUpdates is called', async () => {
  193. await withTempDirectory(async (dir) => {
  194. const appPath = await copyApp(dir, 'check');
  195. await signApp(appPath);
  196. server.get('/update-check', (req, res) => {
  197. res.status(204).send();
  198. });
  199. const launchResult = await launchApp(appPath, [`http://localhost:${port}/update-check`]);
  200. logOnError(launchResult, () => {
  201. expect(launchResult.code).to.equal(0);
  202. expect(requests).to.have.lengthOf(1);
  203. expect(requests[0]).to.have.property('url', '/update-check');
  204. expect(requests[0].header('user-agent')).to.include('Electron/');
  205. });
  206. });
  207. });
  208. it('should hit the update endpoint with customer headers when checkForUpdates is called', async () => {
  209. await withTempDirectory(async (dir) => {
  210. const appPath = await copyApp(dir, 'check-with-headers');
  211. await signApp(appPath);
  212. server.get('/update-check', (req, res) => {
  213. res.status(204).send();
  214. });
  215. const launchResult = await launchApp(appPath, [`http://localhost:${port}/update-check`]);
  216. logOnError(launchResult, () => {
  217. expect(launchResult.code).to.equal(0);
  218. expect(requests).to.have.lengthOf(1);
  219. expect(requests[0]).to.have.property('url', '/update-check');
  220. expect(requests[0].header('x-test')).to.equal('this-is-a-test');
  221. });
  222. });
  223. });
  224. it('should hit the download endpoint when an update is available and error if the file is bad', async () => {
  225. await withTempDirectory(async (dir) => {
  226. const appPath = await copyApp(dir, 'update');
  227. await signApp(appPath);
  228. server.get('/update-file', (req, res) => {
  229. res.status(500).send('This is not a file');
  230. });
  231. server.get('/update-check', (req, res) => {
  232. res.json({
  233. url: `http://localhost:${port}/update-file`,
  234. name: 'My Release Name',
  235. notes: 'Theses are some release notes innit',
  236. pub_date: (new Date()).toString()
  237. });
  238. });
  239. const launchResult = await launchApp(appPath, [`http://localhost:${port}/update-check`]);
  240. logOnError(launchResult, () => {
  241. expect(launchResult).to.have.property('code', 1);
  242. expect(launchResult.out).to.include('Update download failed. The server sent an invalid response.');
  243. expect(requests).to.have.lengthOf(2);
  244. expect(requests[0]).to.have.property('url', '/update-check');
  245. expect(requests[1]).to.have.property('url', '/update-file');
  246. expect(requests[0].header('user-agent')).to.include('Electron/');
  247. expect(requests[1].header('user-agent')).to.include('Electron/');
  248. });
  249. });
  250. });
  251. const withUpdatableApp = async (opts: {
  252. nextVersion: string;
  253. startFixture: string;
  254. endFixture: string;
  255. mutateAppPostSign?: {
  256. mutate: (appPath: string) => Promise<void>,
  257. mutationKey: string,
  258. }
  259. }, fn: (appPath: string, zipPath: string) => Promise<void>) => {
  260. await withTempDirectory(async (dir) => {
  261. const appPath = await copyApp(dir, opts.startFixture);
  262. await signApp(appPath);
  263. const updateZipPath = await getOrCreateUpdateZipPath(opts.nextVersion, opts.endFixture, opts.mutateAppPostSign);
  264. await fn(appPath, updateZipPath);
  265. });
  266. };
  267. it('should hit the download endpoint when an update is available and update successfully when the zip is provided', async () => {
  268. await withUpdatableApp({
  269. nextVersion: '2.0.0',
  270. startFixture: 'update',
  271. endFixture: 'update'
  272. }, async (appPath, updateZipPath) => {
  273. server.get('/update-file', (req, res) => {
  274. res.download(updateZipPath);
  275. });
  276. server.get('/update-check', (req, res) => {
  277. res.json({
  278. url: `http://localhost:${port}/update-file`,
  279. name: 'My Release Name',
  280. notes: 'Theses are some release notes innit',
  281. pub_date: (new Date()).toString()
  282. });
  283. });
  284. const relaunchPromise = new Promise<void>((resolve) => {
  285. server.get('/update-check/updated/:version', (req, res) => {
  286. res.status(204).send();
  287. resolve();
  288. });
  289. });
  290. const launchResult = await launchApp(appPath, [`http://localhost:${port}/update-check`]);
  291. logOnError(launchResult, () => {
  292. expect(launchResult).to.have.property('code', 0);
  293. expect(launchResult.out).to.include('Update Downloaded');
  294. expect(requests).to.have.lengthOf(2);
  295. expect(requests[0]).to.have.property('url', '/update-check');
  296. expect(requests[1]).to.have.property('url', '/update-file');
  297. expect(requests[0].header('user-agent')).to.include('Electron/');
  298. expect(requests[1].header('user-agent')).to.include('Electron/');
  299. });
  300. await relaunchPromise;
  301. expect(requests).to.have.lengthOf(3);
  302. expect(requests[2].url).to.equal('/update-check/updated/2.0.0');
  303. expect(requests[2].header('user-agent')).to.include('Electron/');
  304. });
  305. });
  306. it('should abort the update if the application is still running when ShipIt kicks off', async () => {
  307. await withUpdatableApp({
  308. nextVersion: '2.0.0',
  309. startFixture: 'update',
  310. endFixture: 'update'
  311. }, async (appPath, updateZipPath) => {
  312. server.get('/update-file', (req, res) => {
  313. res.download(updateZipPath);
  314. });
  315. server.get('/update-check', (req, res) => {
  316. res.json({
  317. url: `http://localhost:${port}/update-file`,
  318. name: 'My Release Name',
  319. notes: 'Theses are some release notes innit',
  320. pub_date: (new Date()).toString()
  321. });
  322. });
  323. enum FlipFlop {
  324. INITIAL,
  325. FLIPPED,
  326. FLOPPED,
  327. }
  328. const shipItFlipFlopPromise = new Promise<void>((resolve) => {
  329. let state = FlipFlop.INITIAL;
  330. const checker = setInterval(async () => {
  331. const running = await getRunningShipIts(appPath);
  332. switch (state) {
  333. case FlipFlop.INITIAL: {
  334. if (running.length) state = FlipFlop.FLIPPED;
  335. break;
  336. }
  337. case FlipFlop.FLIPPED: {
  338. if (!running.length) state = FlipFlop.FLOPPED;
  339. break;
  340. }
  341. }
  342. if (state === FlipFlop.FLOPPED) {
  343. clearInterval(checker);
  344. resolve();
  345. }
  346. }, 500);
  347. });
  348. const launchResult = await launchApp(appPath, [`http://localhost:${port}/update-check`]);
  349. const retainerHandle = spawnAppWithHandle(appPath, ['remain-open']);
  350. logOnError(launchResult, () => {
  351. expect(launchResult).to.have.property('code', 0);
  352. expect(launchResult.out).to.include('Update Downloaded');
  353. expect(requests).to.have.lengthOf(2);
  354. expect(requests[0]).to.have.property('url', '/update-check');
  355. expect(requests[1]).to.have.property('url', '/update-file');
  356. expect(requests[0].header('user-agent')).to.include('Electron/');
  357. expect(requests[1].header('user-agent')).to.include('Electron/');
  358. });
  359. await shipItFlipFlopPromise;
  360. expect(requests).to.have.lengthOf(2, 'should not have relaunched the updated app');
  361. 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');
  362. retainerHandle.kill('SIGINT');
  363. });
  364. });
  365. describe('with SquirrelMacEnableDirectContentsWrite enabled', () => {
  366. let previousValue: any;
  367. beforeEach(() => {
  368. previousValue = systemPreferences.getUserDefault('SquirrelMacEnableDirectContentsWrite', 'boolean');
  369. systemPreferences.setUserDefault('SquirrelMacEnableDirectContentsWrite', 'boolean', true as any);
  370. });
  371. afterEach(() => {
  372. systemPreferences.setUserDefault('SquirrelMacEnableDirectContentsWrite', 'boolean', previousValue as any);
  373. });
  374. 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 () => {
  375. await withUpdatableApp({
  376. nextVersion: '2.0.0',
  377. startFixture: 'update',
  378. endFixture: 'update'
  379. }, async (appPath, updateZipPath) => {
  380. const randomID = uuid.v4();
  381. cp.spawnSync('xattr', ['-w', 'spec-id', randomID, appPath]);
  382. server.get('/update-file', (req, res) => {
  383. res.download(updateZipPath);
  384. });
  385. server.get('/update-check', (req, res) => {
  386. res.json({
  387. url: `http://localhost:${port}/update-file`,
  388. name: 'My Release Name',
  389. notes: 'Theses are some release notes innit',
  390. pub_date: (new Date()).toString()
  391. });
  392. });
  393. const relaunchPromise = new Promise<void>((resolve) => {
  394. server.get('/update-check/updated/:version', (req, res) => {
  395. res.status(204).send();
  396. resolve();
  397. });
  398. });
  399. const launchResult = await launchApp(appPath, [`http://localhost:${port}/update-check`]);
  400. logOnError(launchResult, () => {
  401. expect(launchResult).to.have.property('code', 0);
  402. expect(launchResult.out).to.include('Update Downloaded');
  403. expect(requests).to.have.lengthOf(2);
  404. expect(requests[0]).to.have.property('url', '/update-check');
  405. expect(requests[1]).to.have.property('url', '/update-file');
  406. expect(requests[0].header('user-agent')).to.include('Electron/');
  407. expect(requests[1].header('user-agent')).to.include('Electron/');
  408. });
  409. await relaunchPromise;
  410. expect(requests).to.have.lengthOf(3);
  411. expect(requests[2].url).to.equal('/update-check/updated/2.0.0');
  412. expect(requests[2].header('user-agent')).to.include('Electron/');
  413. const result = cp.spawnSync('xattr', ['-l', appPath]);
  414. expect(result.stdout.toString()).to.include(`spec-id: ${randomID}`);
  415. });
  416. });
  417. });
  418. it('should hit the download endpoint when an update is available and fail when the zip signature is invalid', async () => {
  419. await withUpdatableApp({
  420. nextVersion: '2.0.0',
  421. startFixture: 'update',
  422. endFixture: 'update',
  423. mutateAppPostSign: {
  424. mutationKey: 'add-resource',
  425. mutate: async (appPath) => {
  426. const resourcesPath = path.resolve(appPath, 'Contents', 'Resources', 'app', 'injected.txt');
  427. await fs.writeFile(resourcesPath, 'demo');
  428. }
  429. }
  430. }, async (appPath, updateZipPath) => {
  431. server.get('/update-file', (req, res) => {
  432. res.download(updateZipPath);
  433. });
  434. server.get('/update-check', (req, res) => {
  435. res.json({
  436. url: `http://localhost:${port}/update-file`,
  437. name: 'My Release Name',
  438. notes: 'Theses are some release notes innit',
  439. pub_date: (new Date()).toString()
  440. });
  441. });
  442. const launchResult = await launchApp(appPath, [`http://localhost:${port}/update-check`]);
  443. logOnError(launchResult, () => {
  444. expect(launchResult).to.have.property('code', 1);
  445. expect(launchResult.out).to.include('Code signature at URL');
  446. expect(launchResult.out).to.include('a sealed resource is missing or invalid');
  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. });
  454. });
  455. it('should hit the download endpoint when an update is available and fail when the ShipIt binary is a symlink', async () => {
  456. await withUpdatableApp({
  457. nextVersion: '2.0.0',
  458. startFixture: 'update',
  459. endFixture: 'update',
  460. mutateAppPostSign: {
  461. mutationKey: 'modify-shipit',
  462. mutate: async (appPath) => {
  463. const shipItPath = path.resolve(appPath, 'Contents', 'Frameworks', 'Squirrel.framework', 'Resources', 'ShipIt');
  464. await fs.remove(shipItPath);
  465. await fs.symlink('/tmp/ShipIt', shipItPath, 'file');
  466. }
  467. }
  468. }, async (appPath, updateZipPath) => {
  469. server.get('/update-file', (req, res) => {
  470. res.download(updateZipPath);
  471. });
  472. server.get('/update-check', (req, res) => {
  473. res.json({
  474. url: `http://localhost:${port}/update-file`,
  475. name: 'My Release Name',
  476. notes: 'Theses are some release notes innit',
  477. pub_date: (new Date()).toString()
  478. });
  479. });
  480. const launchResult = await launchApp(appPath, [`http://localhost:${port}/update-check`]);
  481. logOnError(launchResult, () => {
  482. expect(launchResult).to.have.property('code', 1);
  483. expect(launchResult.out).to.include('Code signature at URL');
  484. expect(launchResult.out).to.include('a sealed resource is missing or invalid');
  485. expect(requests).to.have.lengthOf(2);
  486. expect(requests[0]).to.have.property('url', '/update-check');
  487. expect(requests[1]).to.have.property('url', '/update-file');
  488. expect(requests[0].header('user-agent')).to.include('Electron/');
  489. expect(requests[1].header('user-agent')).to.include('Electron/');
  490. });
  491. });
  492. });
  493. it('should hit the download endpoint when an update is available and fail when the Electron Framework is modified', async () => {
  494. await withUpdatableApp({
  495. nextVersion: '2.0.0',
  496. startFixture: 'update',
  497. endFixture: 'update',
  498. mutateAppPostSign: {
  499. mutationKey: 'modify-eframework',
  500. mutate: async (appPath) => {
  501. const shipItPath = path.resolve(appPath, 'Contents', 'Frameworks', 'Electron Framework.framework', 'Electron Framework');
  502. await fs.appendFile(shipItPath, Buffer.from('123'));
  503. }
  504. }
  505. }, async (appPath, updateZipPath) => {
  506. server.get('/update-file', (req, res) => {
  507. res.download(updateZipPath);
  508. });
  509. server.get('/update-check', (req, res) => {
  510. res.json({
  511. url: `http://localhost:${port}/update-file`,
  512. name: 'My Release Name',
  513. notes: 'Theses are some release notes innit',
  514. pub_date: (new Date()).toString()
  515. });
  516. });
  517. const launchResult = await launchApp(appPath, [`http://localhost:${port}/update-check`]);
  518. logOnError(launchResult, () => {
  519. expect(launchResult).to.have.property('code', 1);
  520. expect(launchResult.out).to.include('Code signature at URL');
  521. expect(launchResult.out).to.include(' main executable failed strict validation');
  522. expect(requests).to.have.lengthOf(2);
  523. expect(requests[0]).to.have.property('url', '/update-check');
  524. expect(requests[1]).to.have.property('url', '/update-file');
  525. expect(requests[0].header('user-agent')).to.include('Electron/');
  526. expect(requests[1].header('user-agent')).to.include('Electron/');
  527. });
  528. });
  529. });
  530. it('should hit the download endpoint when an update is available and update successfully when the zip is provided with JSON update mode', async () => {
  531. await withUpdatableApp({
  532. nextVersion: '2.0.0',
  533. startFixture: 'update-json',
  534. endFixture: 'update-json'
  535. }, async (appPath, updateZipPath) => {
  536. server.get('/update-file', (req, res) => {
  537. res.download(updateZipPath);
  538. });
  539. server.get('/update-check', (req, res) => {
  540. res.json({
  541. currentRelease: '2.0.0',
  542. releases: [
  543. {
  544. version: '2.0.0',
  545. updateTo: {
  546. version: '2.0.0',
  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. ]
  554. });
  555. });
  556. const relaunchPromise = new Promise<void>((resolve) => {
  557. server.get('/update-check/updated/:version', (req, res) => {
  558. res.status(204).send();
  559. resolve();
  560. });
  561. });
  562. const launchResult = await launchApp(appPath, [`http://localhost:${port}/update-check`]);
  563. logOnError(launchResult, () => {
  564. expect(launchResult).to.have.property('code', 0);
  565. expect(launchResult.out).to.include('Update Downloaded');
  566. expect(requests).to.have.lengthOf(2);
  567. expect(requests[0]).to.have.property('url', '/update-check');
  568. expect(requests[1]).to.have.property('url', '/update-file');
  569. expect(requests[0].header('user-agent')).to.include('Electron/');
  570. expect(requests[1].header('user-agent')).to.include('Electron/');
  571. });
  572. await relaunchPromise;
  573. expect(requests).to.have.lengthOf(3);
  574. expect(requests[2]).to.have.property('url', '/update-check/updated/2.0.0');
  575. expect(requests[2].header('user-agent')).to.include('Electron/');
  576. });
  577. });
  578. 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 () => {
  579. await withUpdatableApp({
  580. nextVersion: '0.1.0',
  581. startFixture: 'update-json',
  582. endFixture: 'update-json'
  583. }, async (appPath, updateZipPath) => {
  584. server.get('/update-file', (req, res) => {
  585. res.download(updateZipPath);
  586. });
  587. server.get('/update-check', (req, res) => {
  588. res.json({
  589. currentRelease: '0.1.0',
  590. releases: [
  591. {
  592. version: '0.1.0',
  593. updateTo: {
  594. version: '0.1.0',
  595. url: `http://localhost:${port}/update-file`,
  596. name: 'My Release Name',
  597. notes: 'Theses are some release notes innit',
  598. pub_date: (new Date()).toString()
  599. }
  600. }
  601. ]
  602. });
  603. });
  604. const launchResult = await launchApp(appPath, [`http://localhost:${port}/update-check`]);
  605. logOnError(launchResult, () => {
  606. expect(launchResult).to.have.property('code', 1);
  607. expect(launchResult.out).to.include('No update available');
  608. expect(requests).to.have.lengthOf(1);
  609. expect(requests[0]).to.have.property('url', '/update-check');
  610. expect(requests[0].header('user-agent')).to.include('Electron/');
  611. });
  612. });
  613. });
  614. });
  615. });