123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586 |
- import { expect } from 'chai';
- import * as cp from 'child_process';
- import * as http from 'http';
- import * as express from 'express';
- import * as fs from 'fs-extra';
- import * as os from 'os';
- import * as path from 'path';
- import { AddressInfo } from 'net';
- import { ifdescribe, ifit } from './spec-helpers';
- import * as uuid from 'uuid';
- import { systemPreferences } from 'electron';
- const features = process._linkedBinding('electron_common_features');
- const fixturesPath = path.resolve(__dirname, 'fixtures');
- // We can only test the auto updater on darwin non-component builds
- ifdescribe(process.platform === 'darwin' && !(process.env.CI && process.arch === 'arm64') && !process.mas && !features.isComponentBuild())('autoUpdater behavior', function () {
- this.timeout(120000);
- let identity = '';
- beforeEach(function () {
- const result = cp.spawnSync(path.resolve(__dirname, '../script/codesign/get-trusted-identity.sh'));
- if (result.status !== 0 || result.stdout.toString().trim().length === 0) {
- // Per https://circleci.com/docs/2.0/env-vars:
- // CIRCLE_PR_NUMBER is only present on forked PRs
- if (process.env.CI && !process.env.CIRCLE_PR_NUMBER) {
- throw new Error('No valid signing identity available to run autoUpdater specs');
- }
- this.skip();
- } else {
- identity = result.stdout.toString().trim();
- }
- });
- it('should have a valid code signing identity', () => {
- expect(identity).to.be.a('string').with.lengthOf.at.least(1);
- });
- const copyApp = async (newDir: string, fixture = 'initial') => {
- const appBundlePath = path.resolve(process.execPath, '../../..');
- const newPath = path.resolve(newDir, 'Electron.app');
- cp.spawnSync('cp', ['-R', appBundlePath, path.dirname(newPath)]);
- const appDir = path.resolve(newPath, 'Contents/Resources/app');
- await fs.mkdirp(appDir);
- await fs.copy(path.resolve(fixturesPath, 'auto-update', fixture), appDir);
- const plistPath = path.resolve(newPath, 'Contents', 'Info.plist');
- await fs.writeFile(
- plistPath,
- (await fs.readFile(plistPath, 'utf8')).replace('<key>BuildMachineOSBuild</key>', `<key>NSAppTransportSecurity</key>
- <dict>
- <key>NSAllowsArbitraryLoads</key>
- <true/>
- <key>NSExceptionDomains</key>
- <dict>
- <key>localhost</key>
- <dict>
- <key>NSExceptionAllowsInsecureHTTPLoads</key>
- <true/>
- <key>NSIncludesSubdomains</key>
- <true/>
- </dict>
- </dict>
- </dict><key>BuildMachineOSBuild</key>`)
- );
- return newPath;
- };
- const spawn = (cmd: string, args: string[], opts: any = {}) => {
- let out = '';
- const child = cp.spawn(cmd, args, opts);
- child.stdout.on('data', (chunk: Buffer) => {
- out += chunk.toString();
- });
- child.stderr.on('data', (chunk: Buffer) => {
- out += chunk.toString();
- });
- return new Promise<{ code: number, out: string }>((resolve) => {
- child.on('exit', (code, signal) => {
- expect(signal).to.equal(null);
- resolve({
- code: code!,
- out
- });
- });
- });
- };
- const signApp = (appPath: string) => {
- return spawn('codesign', ['-s', identity, '--deep', '--force', appPath]);
- };
- const launchApp = (appPath: string, args: string[] = []) => {
- return spawn(path.resolve(appPath, 'Contents/MacOS/Electron'), args);
- };
- const withTempDirectory = async (fn: (dir: string) => Promise<void>, autoCleanUp = true) => {
- const dir = await fs.mkdtemp(path.resolve(os.tmpdir(), 'electron-update-spec-'));
- try {
- await fn(dir);
- } finally {
- if (autoCleanUp) {
- cp.spawnSync('rm', ['-r', dir]);
- }
- }
- };
- const logOnError = (what: any, fn: () => void) => {
- try {
- fn();
- } catch (err) {
- console.error(what);
- throw err;
- }
- };
- const cachedZips: Record<string, string> = {};
- const getOrCreateUpdateZipPath = async (version: string, fixture: string, mutateAppPostSign?: {
- mutate: (appPath: string) => Promise<void>,
- mutationKey: string,
- }) => {
- const key = `${version}-${fixture}-${mutateAppPostSign?.mutationKey || 'no-mutation'}`;
- if (!cachedZips[key]) {
- let updateZipPath: string;
- await withTempDirectory(async (dir) => {
- const secondAppPath = await copyApp(dir, fixture);
- const appPJPath = path.resolve(secondAppPath, 'Contents', 'Resources', 'app', 'package.json');
- await fs.writeFile(
- appPJPath,
- (await fs.readFile(appPJPath, 'utf8')).replace('1.0.0', version)
- );
- await signApp(secondAppPath);
- await mutateAppPostSign?.mutate(secondAppPath);
- updateZipPath = path.resolve(dir, 'update.zip');
- await spawn('zip', ['-0', '-r', '--symlinks', updateZipPath, './'], {
- cwd: dir
- });
- }, false);
- cachedZips[key] = updateZipPath!;
- }
- return cachedZips[key];
- };
- after(() => {
- for (const version of Object.keys(cachedZips)) {
- cp.spawnSync('rm', ['-r', path.dirname(cachedZips[version])]);
- }
- });
- // On arm64 builds the built app is self-signed by default so the setFeedURL call always works
- ifit(process.arch !== 'arm64')('should fail to set the feed URL when the app is not signed', async () => {
- await withTempDirectory(async (dir) => {
- const appPath = await copyApp(dir);
- const launchResult = await launchApp(appPath, ['http://myupdate']);
- console.log(launchResult);
- expect(launchResult.code).to.equal(1);
- expect(launchResult.out).to.include('Could not get code signature for running application');
- });
- });
- it('should cleanly set the feed URL when the app is signed', async () => {
- await withTempDirectory(async (dir) => {
- const appPath = await copyApp(dir);
- await signApp(appPath);
- const launchResult = await launchApp(appPath, ['http://myupdate']);
- expect(launchResult.code).to.equal(0);
- expect(launchResult.out).to.include('Feed URL Set: http://myupdate');
- });
- });
- describe('with update server', () => {
- let port = 0;
- let server: express.Application = null as any;
- let httpServer: http.Server = null as any;
- let requests: express.Request[] = [];
- beforeEach((done) => {
- requests = [];
- server = express();
- server.use((req, res, next) => {
- requests.push(req);
- next();
- });
- httpServer = server.listen(0, '127.0.0.1', () => {
- port = (httpServer.address() as AddressInfo).port;
- done();
- });
- });
- afterEach(async () => {
- if (httpServer) {
- await new Promise<void>(resolve => {
- httpServer.close(() => {
- httpServer = null as any;
- server = null as any;
- resolve();
- });
- });
- }
- });
- it('should hit the update endpoint when checkForUpdates is called', async () => {
- await withTempDirectory(async (dir) => {
- const appPath = await copyApp(dir, 'check');
- await signApp(appPath);
- server.get('/update-check', (req, res) => {
- res.status(204).send();
- });
- const launchResult = await launchApp(appPath, [`http://localhost:${port}/update-check`]);
- logOnError(launchResult, () => {
- expect(launchResult.code).to.equal(0);
- expect(requests).to.have.lengthOf(1);
- expect(requests[0]).to.have.property('url', '/update-check');
- expect(requests[0].header('user-agent')).to.include('Electron/');
- });
- });
- });
- it('should hit the update endpoint with customer headers when checkForUpdates is called', async () => {
- await withTempDirectory(async (dir) => {
- const appPath = await copyApp(dir, 'check-with-headers');
- await signApp(appPath);
- server.get('/update-check', (req, res) => {
- res.status(204).send();
- });
- const launchResult = await launchApp(appPath, [`http://localhost:${port}/update-check`]);
- logOnError(launchResult, () => {
- expect(launchResult.code).to.equal(0);
- expect(requests).to.have.lengthOf(1);
- expect(requests[0]).to.have.property('url', '/update-check');
- expect(requests[0].header('x-test')).to.equal('this-is-a-test');
- });
- });
- });
- it('should hit the download endpoint when an update is available and error if the file is bad', async () => {
- await withTempDirectory(async (dir) => {
- const appPath = await copyApp(dir, 'update');
- await signApp(appPath);
- server.get('/update-file', (req, res) => {
- res.status(500).send('This is not a file');
- });
- server.get('/update-check', (req, res) => {
- res.json({
- url: `http://localhost:${port}/update-file`,
- name: 'My Release Name',
- notes: 'Theses are some release notes innit',
- pub_date: (new Date()).toString()
- });
- });
- const launchResult = await launchApp(appPath, [`http://localhost:${port}/update-check`]);
- logOnError(launchResult, () => {
- expect(launchResult).to.have.property('code', 1);
- expect(launchResult.out).to.include('Update download failed. The server sent an invalid response.');
- expect(requests).to.have.lengthOf(2);
- expect(requests[0]).to.have.property('url', '/update-check');
- expect(requests[1]).to.have.property('url', '/update-file');
- expect(requests[0].header('user-agent')).to.include('Electron/');
- expect(requests[1].header('user-agent')).to.include('Electron/');
- });
- });
- });
- const withUpdatableApp = async (opts: {
- nextVersion: string;
- startFixture: string;
- endFixture: string;
- mutateAppPostSign?: {
- mutate: (appPath: string) => Promise<void>,
- mutationKey: string,
- }
- }, fn: (appPath: string, zipPath: string) => Promise<void>) => {
- await withTempDirectory(async (dir) => {
- const appPath = await copyApp(dir, opts.startFixture);
- await signApp(appPath);
- const updateZipPath = await getOrCreateUpdateZipPath(opts.nextVersion, opts.endFixture, opts.mutateAppPostSign);
- await fn(appPath, updateZipPath);
- });
- };
- it('should hit the download endpoint when an update is available and update successfully when the zip is provided', async () => {
- await withUpdatableApp({
- nextVersion: '2.0.0',
- startFixture: 'update',
- endFixture: 'update'
- }, async (appPath, updateZipPath) => {
- server.get('/update-file', (req, res) => {
- res.download(updateZipPath);
- });
- server.get('/update-check', (req, res) => {
- res.json({
- url: `http://localhost:${port}/update-file`,
- name: 'My Release Name',
- notes: 'Theses are some release notes innit',
- pub_date: (new Date()).toString()
- });
- });
- const relaunchPromise = new Promise<void>((resolve) => {
- server.get('/update-check/updated/:version', (req, res) => {
- res.status(204).send();
- resolve();
- });
- });
- const launchResult = await launchApp(appPath, [`http://localhost:${port}/update-check`]);
- logOnError(launchResult, () => {
- expect(launchResult).to.have.property('code', 0);
- expect(launchResult.out).to.include('Update Downloaded');
- expect(requests).to.have.lengthOf(2);
- expect(requests[0]).to.have.property('url', '/update-check');
- expect(requests[1]).to.have.property('url', '/update-file');
- expect(requests[0].header('user-agent')).to.include('Electron/');
- expect(requests[1].header('user-agent')).to.include('Electron/');
- });
- await relaunchPromise;
- expect(requests).to.have.lengthOf(3);
- expect(requests[2].url).to.equal('/update-check/updated/2.0.0');
- expect(requests[2].header('user-agent')).to.include('Electron/');
- });
- });
- describe('with SquirrelMacEnableDirectContentsWrite enabled', () => {
- let previousValue: any;
- beforeEach(() => {
- previousValue = systemPreferences.getUserDefault('SquirrelMacEnableDirectContentsWrite', 'boolean');
- systemPreferences.setUserDefault('SquirrelMacEnableDirectContentsWrite', 'boolean', true as any);
- });
- afterEach(() => {
- systemPreferences.setUserDefault('SquirrelMacEnableDirectContentsWrite', 'boolean', previousValue as any);
- });
- 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 () => {
- await withUpdatableApp({
- nextVersion: '2.0.0',
- startFixture: 'update',
- endFixture: 'update'
- }, async (appPath, updateZipPath) => {
- const randomID = uuid.v4();
- cp.spawnSync('xattr', ['-w', 'spec-id', randomID, appPath]);
- server.get('/update-file', (req, res) => {
- res.download(updateZipPath);
- });
- server.get('/update-check', (req, res) => {
- res.json({
- url: `http://localhost:${port}/update-file`,
- name: 'My Release Name',
- notes: 'Theses are some release notes innit',
- pub_date: (new Date()).toString()
- });
- });
- const relaunchPromise = new Promise<void>((resolve) => {
- server.get('/update-check/updated/:version', (req, res) => {
- res.status(204).send();
- resolve();
- });
- });
- const launchResult = await launchApp(appPath, [`http://localhost:${port}/update-check`]);
- logOnError(launchResult, () => {
- expect(launchResult).to.have.property('code', 0);
- expect(launchResult.out).to.include('Update Downloaded');
- expect(requests).to.have.lengthOf(2);
- expect(requests[0]).to.have.property('url', '/update-check');
- expect(requests[1]).to.have.property('url', '/update-file');
- expect(requests[0].header('user-agent')).to.include('Electron/');
- expect(requests[1].header('user-agent')).to.include('Electron/');
- });
- await relaunchPromise;
- expect(requests).to.have.lengthOf(3);
- expect(requests[2].url).to.equal('/update-check/updated/2.0.0');
- expect(requests[2].header('user-agent')).to.include('Electron/');
- const result = cp.spawnSync('xattr', ['-l', appPath]);
- expect(result.stdout.toString()).to.include(`spec-id: ${randomID}`);
- });
- });
- });
- it('should hit the download endpoint when an update is available and fail when the zip signature is invalid', async () => {
- await withUpdatableApp({
- nextVersion: '2.0.0',
- startFixture: 'update',
- endFixture: 'update',
- mutateAppPostSign: {
- mutationKey: 'add-resource',
- mutate: async (appPath) => {
- const resourcesPath = path.resolve(appPath, 'Contents', 'Resources', 'app', 'injected.txt');
- await fs.writeFile(resourcesPath, 'demo');
- }
- }
- }, async (appPath, updateZipPath) => {
- server.get('/update-file', (req, res) => {
- res.download(updateZipPath);
- });
- server.get('/update-check', (req, res) => {
- res.json({
- url: `http://localhost:${port}/update-file`,
- name: 'My Release Name',
- notes: 'Theses are some release notes innit',
- pub_date: (new Date()).toString()
- });
- });
- const launchResult = await launchApp(appPath, [`http://localhost:${port}/update-check`]);
- logOnError(launchResult, () => {
- expect(launchResult).to.have.property('code', 1);
- expect(launchResult.out).to.include('Code signature at URL');
- expect(launchResult.out).to.include('a sealed resource is missing or invalid');
- expect(requests).to.have.lengthOf(2);
- expect(requests[0]).to.have.property('url', '/update-check');
- expect(requests[1]).to.have.property('url', '/update-file');
- expect(requests[0].header('user-agent')).to.include('Electron/');
- expect(requests[1].header('user-agent')).to.include('Electron/');
- });
- });
- });
- it('should hit the download endpoint when an update is available and fail when the ShipIt binary is a symlink', async () => {
- await withUpdatableApp({
- nextVersion: '2.0.0',
- startFixture: 'update',
- endFixture: 'update',
- mutateAppPostSign: {
- mutationKey: 'modify-shipit',
- mutate: async (appPath) => {
- const shipItPath = path.resolve(appPath, 'Contents', 'Frameworks', 'Squirrel.framework', 'Resources', 'ShipIt');
- await fs.remove(shipItPath);
- await fs.symlink('/tmp/ShipIt', shipItPath, 'file');
- }
- }
- }, async (appPath, updateZipPath) => {
- server.get('/update-file', (req, res) => {
- res.download(updateZipPath);
- });
- server.get('/update-check', (req, res) => {
- res.json({
- url: `http://localhost:${port}/update-file`,
- name: 'My Release Name',
- notes: 'Theses are some release notes innit',
- pub_date: (new Date()).toString()
- });
- });
- const launchResult = await launchApp(appPath, [`http://localhost:${port}/update-check`]);
- logOnError(launchResult, () => {
- expect(launchResult).to.have.property('code', 1);
- expect(launchResult.out).to.include('Code signature at URL');
- expect(launchResult.out).to.include('a sealed resource is missing or invalid');
- expect(requests).to.have.lengthOf(2);
- expect(requests[0]).to.have.property('url', '/update-check');
- expect(requests[1]).to.have.property('url', '/update-file');
- expect(requests[0].header('user-agent')).to.include('Electron/');
- expect(requests[1].header('user-agent')).to.include('Electron/');
- });
- });
- });
- it('should hit the download endpoint when an update is available and fail when the Electron Framework is modified', async () => {
- await withUpdatableApp({
- nextVersion: '2.0.0',
- startFixture: 'update',
- endFixture: 'update',
- mutateAppPostSign: {
- mutationKey: 'modify-eframework',
- mutate: async (appPath) => {
- const shipItPath = path.resolve(appPath, 'Contents', 'Frameworks', 'Electron Framework.framework', 'Electron Framework');
- await fs.appendFile(shipItPath, Buffer.from('123'));
- }
- }
- }, async (appPath, updateZipPath) => {
- server.get('/update-file', (req, res) => {
- res.download(updateZipPath);
- });
- server.get('/update-check', (req, res) => {
- res.json({
- url: `http://localhost:${port}/update-file`,
- name: 'My Release Name',
- notes: 'Theses are some release notes innit',
- pub_date: (new Date()).toString()
- });
- });
- const launchResult = await launchApp(appPath, [`http://localhost:${port}/update-check`]);
- logOnError(launchResult, () => {
- expect(launchResult).to.have.property('code', 1);
- expect(launchResult.out).to.include('Code signature at URL');
- expect(launchResult.out).to.include(' main executable failed strict validation');
- expect(requests).to.have.lengthOf(2);
- expect(requests[0]).to.have.property('url', '/update-check');
- expect(requests[1]).to.have.property('url', '/update-file');
- expect(requests[0].header('user-agent')).to.include('Electron/');
- expect(requests[1].header('user-agent')).to.include('Electron/');
- });
- });
- });
- it('should hit the download endpoint when an update is available and update successfully when the zip is provided with JSON update mode', async () => {
- await withUpdatableApp({
- nextVersion: '2.0.0',
- startFixture: 'update-json',
- endFixture: 'update-json'
- }, async (appPath, updateZipPath) => {
- server.get('/update-file', (req, res) => {
- res.download(updateZipPath);
- });
- server.get('/update-check', (req, res) => {
- res.json({
- currentRelease: '2.0.0',
- releases: [
- {
- version: '2.0.0',
- updateTo: {
- version: '2.0.0',
- url: `http://localhost:${port}/update-file`,
- name: 'My Release Name',
- notes: 'Theses are some release notes innit',
- pub_date: (new Date()).toString()
- }
- }
- ]
- });
- });
- const relaunchPromise = new Promise<void>((resolve) => {
- server.get('/update-check/updated/:version', (req, res) => {
- res.status(204).send();
- resolve();
- });
- });
- const launchResult = await launchApp(appPath, [`http://localhost:${port}/update-check`]);
- logOnError(launchResult, () => {
- expect(launchResult).to.have.property('code', 0);
- expect(launchResult.out).to.include('Update Downloaded');
- expect(requests).to.have.lengthOf(2);
- expect(requests[0]).to.have.property('url', '/update-check');
- expect(requests[1]).to.have.property('url', '/update-file');
- expect(requests[0].header('user-agent')).to.include('Electron/');
- expect(requests[1].header('user-agent')).to.include('Electron/');
- });
- await relaunchPromise;
- expect(requests).to.have.lengthOf(3);
- expect(requests[2]).to.have.property('url', '/update-check/updated/2.0.0');
- expect(requests[2].header('user-agent')).to.include('Electron/');
- });
- });
- 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 () => {
- await withUpdatableApp({
- nextVersion: '0.1.0',
- startFixture: 'update-json',
- endFixture: 'update-json'
- }, async (appPath, updateZipPath) => {
- server.get('/update-file', (req, res) => {
- res.download(updateZipPath);
- });
- server.get('/update-check', (req, res) => {
- res.json({
- currentRelease: '0.1.0',
- releases: [
- {
- version: '0.1.0',
- updateTo: {
- version: '0.1.0',
- url: `http://localhost:${port}/update-file`,
- name: 'My Release Name',
- notes: 'Theses are some release notes innit',
- pub_date: (new Date()).toString()
- }
- }
- ]
- });
- });
- const launchResult = await launchApp(appPath, [`http://localhost:${port}/update-check`]);
- logOnError(launchResult, () => {
- expect(launchResult).to.have.property('code', 1);
- expect(launchResult.out).to.include('No update available');
- expect(requests).to.have.lengthOf(1);
- expect(requests[0]).to.have.property('url', '/update-check');
- expect(requests[0].header('user-agent')).to.include('Electron/');
- });
- });
- });
- });
- });
|